Minor fixes
This commit is contained in:
@@ -5,7 +5,7 @@ public extension View {
|
|||||||
func presentationDetentsIfAvailable() -> some View {
|
func presentationDetentsIfAvailable() -> some View {
|
||||||
#if os(iOS) || os(tvOS)
|
#if os(iOS) || os(tvOS)
|
||||||
if #available(iOS 16.0, tvOS 17.0, *) {
|
if #available(iOS 16.0, tvOS 17.0, *) {
|
||||||
self.presentationDetents([.large])
|
presentationDetents([.large])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
@@ -31,7 +31,7 @@ public extension View {
|
|||||||
ActionButtonWrapper { self }
|
ActionButtonWrapper { self }
|
||||||
#else
|
#else
|
||||||
if #available(iOS 26.0, macOS 26.0, *) {
|
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))
|
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 8))
|
||||||
} else {
|
} else {
|
||||||
frame(width: 44, height: 32)
|
frame(width: 44, height: 32)
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ private extension View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func selectorBackground() -> some View {
|
func selectorBackground() -> some View {
|
||||||
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, *) {
|
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 {
|
} else {
|
||||||
background(
|
background(
|
||||||
RoundedRectangle(cornerRadius: 12)
|
RoundedRectangle(cornerRadius: 12)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public struct StartStopButton: View {
|
|||||||
@EnvironmentObject private var profile: ExtensionProfile
|
@EnvironmentObject private var profile: ExtensionProfile
|
||||||
@State private var alert: AlertState?
|
@State private var alert: AlertState?
|
||||||
@State private var currentTime = Date()
|
@State private var currentTime = Date()
|
||||||
|
@State private var isStarting = false
|
||||||
|
|
||||||
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
@@ -109,6 +110,21 @@ public struct StartStopButton: View {
|
|||||||
.onReceive(timer) { _ in
|
.onReceive(timer) { _ in
|
||||||
currentTime = Date()
|
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)
|
#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 {
|
private nonisolated func switchProfile(_ isEnabled: Bool) async {
|
||||||
do {
|
do {
|
||||||
if isEnabled {
|
if isEnabled {
|
||||||
|
await MainActor.run { isStarting = true }
|
||||||
try await profile.start()
|
try await profile.start()
|
||||||
await environments.commandClient.connect()
|
|
||||||
} else {
|
} else {
|
||||||
try await profile.stop()
|
try await profile.stop()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
isStarting = false
|
||||||
alert = AlertState(error: error)
|
alert = AlertState(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ class LogCoordinator {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
LogUITextView(logs: logs, searchText: searchText)
|
LogUITextView(logs: logs, searchText: searchText)
|
||||||
.font(font)
|
.font(font)
|
||||||
// Explicit height ensures navigation bar large title collapse animation works correctly with UITextView
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||||
.padding()
|
.padding()
|
||||||
.id("logContent")
|
.id("logContent")
|
||||||
}
|
}
|
||||||
@@ -226,6 +226,15 @@ class LogCoordinator {
|
|||||||
// Update cache to full content
|
// Update cache to full content
|
||||||
context.coordinator.cachedAttributedString = NSAttributedString(attributedString: textStorage)
|
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 {
|
func makeCoordinator() -> LogCoordinator {
|
||||||
@@ -302,6 +311,10 @@ class LogCoordinator {
|
|||||||
context.coordinator.cachedAttributedString = NSAttributedString(attributedString: textStorage)
|
context.coordinator.cachedAttributedString = NSAttributedString(attributedString: textStorage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let textContainer = textView.textContainer {
|
||||||
|
textView.layoutManager?.ensureLayout(for: textContainer)
|
||||||
|
}
|
||||||
|
|
||||||
let shouldScroll = shouldAutoScroll && logs.count != lastCount
|
let shouldScroll = shouldAutoScroll && logs.count != lastCount
|
||||||
if shouldScroll {
|
if shouldScroll {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ public struct NavigationSheet<Content: View>: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func sheetDetent(_ size: SheetSize) -> some View {
|
func sheetDetent(_ size: SheetSize) -> some View {
|
||||||
if #available(iOS 16.0, tvOS 17.0, *) {
|
if #available(iOS 16.0, tvOS 17.0, *) {
|
||||||
self.presentationDetents([size.presentationDetent])
|
presentationDetents([size.presentationDetent])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
} else {
|
} else {
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ public class CommandClient: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func flushPendingLogs() {
|
private func flushPendingLogs() {
|
||||||
|
logBatchTimer = nil
|
||||||
guard !pendingLogs.isEmpty else { return }
|
guard !pendingLogs.isEmpty else { return }
|
||||||
|
|
||||||
// Batch append all pending logs
|
// Batch append all pending logs
|
||||||
@@ -254,13 +255,8 @@ public class CommandClient: ObservableObject {
|
|||||||
guard !newLogs.isEmpty else { return }
|
guard !newLogs.isEmpty else { return }
|
||||||
|
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
// Add to pending batch
|
|
||||||
commandClient.pendingLogs.append(contentsOf: newLogs)
|
commandClient.pendingLogs.append(contentsOf: newLogs)
|
||||||
|
if commandClient.logBatchTimer == nil {
|
||||||
// Cancel existing timer
|
|
||||||
commandClient.logBatchTimer?.cancel()
|
|
||||||
|
|
||||||
// Schedule batch flush
|
|
||||||
let workItem = DispatchWorkItem { [weak commandClient] in
|
let workItem = DispatchWorkItem { [weak commandClient] in
|
||||||
guard let commandClient else { return }
|
guard let commandClient else { return }
|
||||||
commandClient.flushPendingLogs()
|
commandClient.flushPendingLogs()
|
||||||
@@ -269,6 +265,7 @@ public class CommandClient: ObservableObject {
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + commandClient.logBatchInterval, execute: workItem)
|
DispatchQueue.main.asyncAfter(deadline: .now() + commandClient.logBatchInterval, execute: workItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeStatus(_ message: LibboxStatusMessage?) {
|
func writeStatus(_ message: LibboxStatusMessage?) {
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
|
|||||||
@@ -17,3 +17,11 @@ extension ExtensionStartupError: LocalizedError {
|
|||||||
message
|
message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ExtensionStartupError: CustomNSError {
|
||||||
|
public static var errorDomain: String { "ExtensionStartupError" }
|
||||||
|
public var errorCode: Int { 1 }
|
||||||
|
public var errorUserInfo: [String: Any] {
|
||||||
|
[NSLocalizedDescriptionKey: message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user