From 9be752ea94dddc0f63c78a09b78b14e48964a9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 13 Dec 2025 19:30:33 +0800 Subject: [PATCH] Minor fixes --- .../Views/Abstract/ViewModifiers.swift | 4 +-- .../Cards/ProfileSelectorButton.swift | 2 +- .../Components/StartStopButton.swift | 28 ++++++++++++++++++- .../Views/Log/LogTextView.swift | 17 +++++++++-- .../Views/Profile/ProfileSheetHelpers.swift | 2 +- Library/Network/CommandClient.swift | 19 ++++++------- Library/Network/ExtensionErrors.swift | 8 ++++++ 7 files changed, 62 insertions(+), 18 deletions(-) diff --git a/ApplicationLibrary/Views/Abstract/ViewModifiers.swift b/ApplicationLibrary/Views/Abstract/ViewModifiers.swift index ed59fb9..8fac572 100644 --- a/ApplicationLibrary/Views/Abstract/ViewModifiers.swift +++ b/ApplicationLibrary/Views/Abstract/ViewModifiers.swift @@ -5,7 +5,7 @@ public extension View { func presentationDetentsIfAvailable() -> some View { #if os(iOS) || os(tvOS) if #available(iOS 16.0, tvOS 17.0, *) { - self.presentationDetents([.large]) + presentationDetents([.large]) .presentationDragIndicator(.visible) } else { self @@ -31,7 +31,7 @@ public extension View { ActionButtonWrapper { self } #else if #available(iOS 26.0, macOS 26.0, *) { - self.frame(width: 44, height: 32) + frame(width: 44, height: 32) .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 8)) } else { frame(width: 44, height: 32) diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift index f3733d4..e92bb77 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfileSelectorButton.swift @@ -81,7 +81,7 @@ private extension View { @ViewBuilder func selectorBackground() -> some View { if #available(iOS 26.0, macOS 26.0, tvOS 26.0, *) { - self.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 12)) + glassEffect(.regular.interactive(), in: .rect(cornerRadius: 12)) } else { background( RoundedRectangle(cornerRadius: 12) diff --git a/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift b/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift index 32e2d3b..8434444 100644 --- a/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift +++ b/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift @@ -41,6 +41,7 @@ public struct StartStopButton: View { @EnvironmentObject private var profile: ExtensionProfile @State private var alert: AlertState? @State private var currentTime = Date() + @State private var isStarting = false private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -109,6 +110,21 @@ public struct StartStopButton: View { .onReceive(timer) { _ in currentTime = Date() } + .onChangeCompat(of: profile.status) { status in + if isStarting { + if status == .disconnected { + isStarting = false + if #available(iOS 16.0, macOS 13.0, tvOS 17.0, *) { + Task { + await checkStartupError() + } + } + } else if status.isConnectedStrict { + isStarting = false + environments.commandClient.connect() + } + } + } } #if os(iOS) @@ -136,16 +152,26 @@ public struct StartStopButton: View { } } + @available(iOS 16.0, macOS 13.0, tvOS 17.0, *) + private func checkStartupError() async { + do { + try await profile.fetchLastDisconnectError() + } catch { + alert = AlertState(title: String(localized: "Service Error"), message: error.localizedDescription) + } + } + private nonisolated func switchProfile(_ isEnabled: Bool) async { do { if isEnabled { + await MainActor.run { isStarting = true } try await profile.start() - await environments.commandClient.connect() } else { try await profile.stop() } } catch { await MainActor.run { + isStarting = false alert = AlertState(error: error) } } diff --git a/ApplicationLibrary/Views/Log/LogTextView.swift b/ApplicationLibrary/Views/Log/LogTextView.swift index 9095c77..fb910bf 100644 --- a/ApplicationLibrary/Views/Log/LogTextView.swift +++ b/ApplicationLibrary/Views/Log/LogTextView.swift @@ -147,8 +147,8 @@ class LogCoordinator { ScrollView { LogUITextView(logs: logs, searchText: searchText) .font(font) - // Explicit height ensures navigation bar large title collapse animation works correctly with UITextView - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .topLeading) .padding() .id("logContent") } @@ -226,6 +226,15 @@ class LogCoordinator { // Update cache to full content context.coordinator.cachedAttributedString = NSAttributedString(attributedString: textStorage) } + + textView.invalidateIntrinsicContentSize() + } + + @available(iOS 16.0, *) + func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context _: Context) -> CGSize? { + guard let width = proposal.width, width > 0 else { return nil } + let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)) + return CGSize(width: width, height: size.height) } func makeCoordinator() -> LogCoordinator { @@ -302,6 +311,10 @@ class LogCoordinator { context.coordinator.cachedAttributedString = NSAttributedString(attributedString: textStorage) } + if let textContainer = textView.textContainer { + textView.layoutManager?.ensureLayout(for: textContainer) + } + let shouldScroll = shouldAutoScroll && logs.count != lastCount if shouldScroll { DispatchQueue.main.async { diff --git a/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift b/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift index 8ffb7b9..0c7fc04 100644 --- a/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift +++ b/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift @@ -87,7 +87,7 @@ public struct NavigationSheet: View { @ViewBuilder func sheetDetent(_ size: SheetSize) -> some View { if #available(iOS 16.0, tvOS 17.0, *) { - self.presentationDetents([size.presentationDetent]) + presentationDetents([size.presentationDetent]) .presentationDragIndicator(.visible) } else { self diff --git a/Library/Network/CommandClient.swift b/Library/Network/CommandClient.swift index 52ee192..3976dfc 100644 --- a/Library/Network/CommandClient.swift +++ b/Library/Network/CommandClient.swift @@ -112,6 +112,7 @@ public class CommandClient: ObservableObject { } private func flushPendingLogs() { + logBatchTimer = nil guard !pendingLogs.isEmpty else { return } // Batch append all pending logs @@ -254,19 +255,15 @@ public class CommandClient: ObservableObject { guard !newLogs.isEmpty else { return } DispatchQueue.main.async { [self] in - // Add to pending batch commandClient.pendingLogs.append(contentsOf: newLogs) - - // Cancel existing timer - commandClient.logBatchTimer?.cancel() - - // Schedule batch flush - let workItem = DispatchWorkItem { [weak commandClient] in - guard let commandClient else { return } - commandClient.flushPendingLogs() + if commandClient.logBatchTimer == nil { + let workItem = DispatchWorkItem { [weak commandClient] in + guard let commandClient else { return } + commandClient.flushPendingLogs() + } + commandClient.logBatchTimer = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + commandClient.logBatchInterval, execute: workItem) } - commandClient.logBatchTimer = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + commandClient.logBatchInterval, execute: workItem) } } diff --git a/Library/Network/ExtensionErrors.swift b/Library/Network/ExtensionErrors.swift index 3b62707..82a0c67 100644 --- a/Library/Network/ExtensionErrors.swift +++ b/Library/Network/ExtensionErrors.swift @@ -17,3 +17,11 @@ extension ExtensionStartupError: LocalizedError { message } } + +extension ExtensionStartupError: CustomNSError { + public static var errorDomain: String { "ExtensionStartupError" } + public var errorCode: Int { 1 } + public var errorUserInfo: [String: Any] { + [NSLocalizedDescriptionKey: message] + } +}