Explorar o código

fix: iOS theme

Tienson Qin hai 3 semanas
pai
achega
50a1ddbd92

+ 1 - 0
ios/App/App/AppViewController.swift

@@ -17,6 +17,7 @@ import UIKit
         bridge?.registerPluginInstance(NativeBottomSheetPlugin())
         bridge?.registerPluginInstance(NativeEditorToolbarPlugin())
         bridge?.registerPluginInstance(NativeSelectionActionBarPlugin())
+        bridge?.registerPluginInstance(Utils())
     }
 
     public override func viewDidLoad() {

+ 32 - 15
ios/App/App/NativeEditorToolbarPlugin.swift

@@ -21,6 +21,10 @@ private class NativeEditorToolbarView: UIView {
     /// Used to prevent an old dismiss animation from removing a newly-presented bar.
     private var dismissGeneration: Int = 0
 
+    /// Store actions so we can reconfigure when theme (light/dark) changes.
+    private var storedActions: [NativeEditorAction] = []
+    private var storedTrailingAction: NativeEditorAction?
+
     private let blurView: UIVisualEffectView = {
         let effect = UIBlurEffect(style: .systemChromeMaterial)
         let view = UIVisualEffectView(effect: effect)
@@ -113,7 +117,11 @@ private class NativeEditorToolbarView: UIView {
         dismissGeneration += 1
         layer.removeAllAnimations()
 
-        // We ignore tintColor/backgroundColor – they’re now driven by theme.
+        // Store actions so we can re-apply them when theme changes
+        storedActions = actions
+        storedTrailingAction = trailingAction
+
+        // We ignore tintColor/backgroundColor – they’re driven by theme.
         configure(actions: actions,
                   trailingAction: trailingAction)
         attachIfNeeded(to: host)
@@ -149,6 +157,24 @@ private class NativeEditorToolbarView: UIView {
         })
     }
 
+    // MARK: - Theme / trait changes
+
+    /// Returns the theme-appropriate tint (light: black, dark: white).
+    private func currentTintColor() -> UIColor {
+        return traitCollection.userInterfaceStyle == .dark ? .white : .black
+    }
+
+    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
+        super.traitCollectionDidChange(previousTraitCollection)
+
+        guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else {
+            return
+        }
+
+        // Reconfigure with new tint when light/dark changes
+        configure(actions: storedActions, trailingAction: storedTrailingAction)
+    }
+
     // MARK: - Private helpers
 
     private func setupView() {
@@ -205,15 +231,6 @@ private class NativeEditorToolbarView: UIView {
         trailingButton.addTarget(self, action: #selector(handleTrailingTap(_:)), for: .touchUpInside)
     }
 
-    /// Returns the theme-appropriate tint (light: black, dark: white).
-    private func currentTintColor() -> UIColor {
-        if #available(iOS 12.0, *) {
-            return traitCollection.userInterfaceStyle == .dark ? .white : .black
-        } else {
-            return .black
-        }
-    }
-
     private func configure(actions: [NativeEditorAction],
                            trailingAction: NativeEditorAction?) {
         let tint = currentTintColor()
@@ -308,7 +325,7 @@ private class NativeEditorToolbarView: UIView {
         config.title = nil
         config.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 6, bottom: 4, trailing: 6)
         config.preferredSymbolConfigurationForImage =
-          UIImage.SymbolConfiguration(pointSize: 17, weight: .regular)
+            UIImage.SymbolConfiguration(pointSize: 17, weight: .regular)
         config.background = .clear()
 
         let button = UIButton(configuration: config, primaryAction: nil)
@@ -316,11 +333,11 @@ private class NativeEditorToolbarView: UIView {
 
         let symbolName = action.systemIcon ?? "circle"
 
-        // 🔑 Try custom SF Symbol as systemName first, then fall back to asset by name.
+        // Try custom SF Symbol as systemName first, then fall back to asset by name.
         let image =
-          UIImage(systemName: symbolName) ??
-          UIImage(named: symbolName) ??
-          UIImage(systemName: "circle")
+            UIImage(systemName: symbolName) ??
+            UIImage(named: symbolName) ??
+            UIImage(systemName: "circle")
 
         button.setImage(image, for: .normal)
 

+ 1 - 0
ios/App/App/SceneDelegate.swift

@@ -40,6 +40,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
         // 2) Wrap in SwiftUI root (LiquidTabsRootView)
         let rootView = LiquidTabsRootView(navController: nav)
         let hosting = UIHostingController(rootView: rootView)
+        hosting.view.backgroundColor = UIColor.logseqBackground
 
         // 3) Standard UIWindowScene setup
         let window = UIWindow(windowScene: windowScene)

+ 22 - 15
ios/App/App/Theme.swift

@@ -2,34 +2,41 @@ import UIKit
 import SwiftUI
 
 extension UIColor {
+    // Base colors: never dynamic, never consult trait collections.
     static let logseqLight = UIColor(red: 0xfc/255, green: 0xfc/255, blue: 0xfc/255, alpha: 1)
     static let logseqDark  = UIColor(red: 0x00/255, green: 0x2b/255, blue: 0x36/255, alpha: 1)
 
-    // New: Tint colors converted from HSL
     static let logseqTintLight = UIColor(
-        red: 23/255,   // ~0.090
-        green: 129/255, // ~0.506
-        blue: 225/255,  // ~0.882
+        red: 23/255,
+        green: 129/255,
+        blue: 225/255,
         alpha: 1.0
     )
 
     static let logseqTintDark = UIColor(
-        red: 245/255,   // ~0.961
-        green: 247/255, // ~0.969
-        blue: 250/255,  // ~0.980
+        red: 245/255,
+        green: 247/255,
+        blue: 250/255,
         alpha: 1.0
     )
 
-    static var logseqBackground: UIColor {
-        UITraitCollection.current.userInterfaceStyle == .dark ? logseqDark : logseqLight
-    }
+    // Dynamic variants: **static let**, so the closure is created once,
+    // and it *only* reads trait.userInterfaceStyle.
+    static let logseqBackground: UIColor = {
+        UIColor { trait in
+            trait.userInterfaceStyle == .dark ? logseqDark : logseqLight
+        }
+    }()
 
-    static var logseqTint: UIColor {
-        UITraitCollection.current.userInterfaceStyle == .dark ? logseqTintDark : logseqTintLight
-    }
+    static let logseqTint: UIColor = {
+        UIColor { trait in
+            trait.userInterfaceStyle == .dark ? logseqTintDark : logseqTintLight
+        }
+    }()
 }
 
 extension Color {
-    static var logseqBackground: Color { Color(uiColor: .logseqBackground) }
-    static var logseqTint: Color { Color(uiColor: .logseqTint) }
+    // SwiftUI uses the dynamic versions
+    static let logseqBackground = Color(uiColor: .logseqBackground)
+    static let logseqTint       = Color(uiColor: .logseqTint)
 }

+ 3 - 9
ios/App/App/UILocalPlugin.swift

@@ -12,11 +12,7 @@ import NaturalLanguage
 import Drops
 
 func isDarkMode() -> Bool {
-    if #available(iOS 12.0, *) {
-        return UITraitCollection.current.userInterfaceStyle == .dark
-    } else {
-        return false
-    }
+    return UITraitCollection.current.userInterfaceStyle == .dark
 }
 
 func isOnlyDayDifferentOrSame(date1: Foundation.Date, date2: Date) -> Bool {
@@ -120,10 +116,8 @@ class DatePickerDialogViewController: UIViewController {
     override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
         super.traitCollectionDidChange(previousTraitCollection)
 
-        if #available(iOS 12.0, *) {
-            if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle {
-                dialogView.backgroundColor = .logseqBackground
-            }
+        if traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle {
+            dialogView.backgroundColor = .logseqBackground
         }
     }
 

+ 1 - 0
ios/App/App/Utils.m

@@ -11,4 +11,5 @@
 CAP_PLUGIN(Utils, "Utils",
     CAP_PLUGIN_METHOD(isZoomed, CAPPluginReturnPromise);
     CAP_PLUGIN_METHOD(getDocumentRoot, CAPPluginReturnPromise);
+    CAP_PLUGIN_METHOD(setInterfaceStyle, CAPPluginReturnPromise);
 )

+ 53 - 0
ios/App/App/Utils.swift

@@ -7,9 +7,11 @@
 
 import Foundation
 import Capacitor
+import UIKit
 
 @objc(Utils)
 public class Utils: CAPPlugin {
+  private var currentInterfaceStyle: UIUserInterfaceStyle = .unspecified
 
   @objc func isZoomed(_ call: CAPPluginCall) {
 
@@ -31,4 +33,55 @@ public class Utils: CAPPlugin {
       call.resolve(["documentRoot": ""])
     }
   }
+
+  @objc func setInterfaceStyle(_ call: CAPPluginCall) {
+    let mode = call.getString("mode")?.lowercased() ?? "system"
+    let followSystem = call.getBool("system") ?? (mode == "system")
+
+    let style: UIUserInterfaceStyle
+    if followSystem {
+      style = .unspecified
+    } else {
+      style = (mode == "dark") ? .dark : .light
+    }
+
+    DispatchQueue.main.async {
+      self.applyInterfaceStyle(style)
+      call.resolve()
+    }
+  }
+
+  private func applyInterfaceStyle(_ style: UIUserInterfaceStyle) {
+    guard style != currentInterfaceStyle else { return }
+    currentInterfaceStyle = style
+
+    let app = UIApplication.shared
+
+    let applyToWindow: (UIWindow) -> Void = { window in
+      window.overrideUserInterfaceStyle = style
+      window.rootViewController?.overrideUserInterfaceStyle = style
+    }
+
+    // Propagate to all active windows (handles multi-scene).
+    let targetScenes = app.connectedScenes
+      .compactMap { $0 as? UIWindowScene }
+      .filter { $0.activationState == .foregroundActive || $0.activationState == .foregroundInactive }
+
+    let windows = targetScenes.flatMap { $0.windows }
+    if windows.isEmpty {
+      app.windows.forEach(applyToWindow)
+    } else {
+      windows.forEach(applyToWindow)
+    }
+
+    // Bridge VC + WKWebView
+    bridge?.viewController?.overrideUserInterfaceStyle = style
+    bridge?.webView?.overrideUserInterfaceStyle = style
+
+    // UINavigationController root (if available)
+    if let nav = (app.delegate as? AppDelegate)?.navController {
+      nav.overrideUserInterfaceStyle = style
+      nav.viewControllers.forEach { $0.overrideUserInterfaceStyle = style }
+    }
+  }
 }

+ 7 - 0
src/main/frontend/mobile/util.cljs

@@ -38,6 +38,13 @@
 (defn hide-splash []
   (.hide SplashScreen))
 
+(defn set-ios-interface-style!
+  [mode system?]
+  (when (native-ios?)
+    (p/do!
+     (.setInterfaceStyle ^js ios-utils (clj->js {:mode mode
+                                                 :system system?})))))
+
 (defn get-idevice-model
   []
   (when (native-ios?)

+ 20 - 12
src/main/frontend/state.cljs

@@ -1424,30 +1424,36 @@ Similar to re-frame subscriptions"
     (set-editor-last-pos! new-pos)))
 
 (defn set-theme-mode!
-  [mode]
-  (when (mobile-util/native-platform?)
-    (if (= mode "light")
-      (util/set-theme-light)
-      (util/set-theme-dark)))
-  (set-state! :ui/theme mode)
-  (storage/set :ui/theme mode))
+  ([mode] (set-theme-mode! mode (:ui/system-theme? @state)))
+  ([mode system-theme?]
+   (when (mobile-util/native-platform?)
+     (if (= mode "light")
+       (util/set-theme-light)
+       (util/set-theme-dark)))
+   (when (mobile-util/native-ios?)
+     (mobile-util/set-ios-interface-style! mode system-theme?))
+   (set-state! :ui/theme mode)
+   (storage/set :ui/theme mode)))
 
 (defn sync-system-theme!
   []
-  (when (= (:ui/theme @state) "system")
+  (when (:ui/system-theme? @state)
     (let [system-dark? (.-matches (js/window.matchMedia "(prefers-color-scheme: dark)"))]
-      (set-theme-mode! (if system-dark? "dark" "light"))
+      (set-theme-mode! (if system-dark? "dark" "light") true)
       (set-state! :ui/system-theme? true)
       (storage/set :ui/system-theme? true))))
 
 (defn use-theme-mode!
   [theme-mode]
   (if (= theme-mode "system")
-    (sync-system-theme!)
     (do
-      (set-theme-mode! theme-mode)
+      (set-state! :ui/system-theme? true)
+      (storage/set :ui/system-theme? true)
+      (sync-system-theme!))
+    (do
       (set-state! :ui/system-theme? false)
-      (storage/set :ui/system-theme? false))))
+      (storage/set :ui/system-theme? false)
+      (set-theme-mode! theme-mode false))))
 
 (defn- toggle-theme
   [theme]
@@ -1469,6 +1475,8 @@ Similar to re-frame subscriptions"
   []
   (let [mode (or (storage/get :ui/theme) "light")
         system-theme? (storage/get :ui/system-theme?)]
+    (when (mobile-util/native-ios?)
+      (mobile-util/set-ios-interface-style! mode system-theme?))
     (when (and (not system-theme?)
                (mobile-util/native-platform?))
       (if (= mode "light")