mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(watchos): localize inbox runtime copy
This commit is contained in:
@@ -59,7 +59,11 @@ final class WatchDirectNode {
|
||||
let detail: String
|
||||
|
||||
var errorDescription: String? {
|
||||
self.detail.isEmpty ? "Gateway HTTP error (\(self.status))" : self.detail
|
||||
self.detail.isEmpty
|
||||
? String(
|
||||
format: String(localized: "Gateway HTTP error (%@)"),
|
||||
self.status.formatted())
|
||||
: self.detail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +89,8 @@ final class WatchDirectNode {
|
||||
|
||||
private(set) var isEnabled: Bool
|
||||
private(set) var isConnected = false
|
||||
private(set) var statusText = "Use iPhone Settings to enable direct connection."
|
||||
private(set) var statusText = String(
|
||||
localized: "Use iPhone Settings to enable direct connection.")
|
||||
private(set) var endpointText: String?
|
||||
|
||||
var isConfigured: Bool {
|
||||
@@ -112,7 +117,9 @@ final class WatchDirectNode {
|
||||
}
|
||||
self.endpointText = self.configuration.map(Self.endpointText)
|
||||
if self.configuration != nil {
|
||||
self.statusText = self.isEnabled ? "Ready to connect" : "Direct connection is off"
|
||||
self.statusText = self.isEnabled
|
||||
? String(localized: "Ready to connect")
|
||||
: String(localized: "Direct connection is off")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +128,8 @@ final class WatchDirectNode {
|
||||
let oldestAcceptedMs = nowMs - Self.maximumSetupAgeMs
|
||||
let newestAcceptedMs = nowMs + Self.maximumSetupClockSkewMs
|
||||
guard (oldestAcceptedMs...newestAcceptedMs).contains(sentAtMs) else {
|
||||
self.statusText = "Ignored an expired direct connection setup. Send setup again from iPhone."
|
||||
self.statusText = String(
|
||||
localized: "Ignored an expired direct connection setup. Send setup again from iPhone.")
|
||||
return
|
||||
}
|
||||
let newestInstalledSetupMs = configuration?.setupSentAtMs ?? 0
|
||||
@@ -133,7 +141,8 @@ final class WatchDirectNode {
|
||||
link.password == nil,
|
||||
let secureEndpoint = link.connectionEndpoints.first(where: \.tls)
|
||||
else {
|
||||
self.statusText = "Direct mode requires a trusted HTTPS Gateway endpoint."
|
||||
self.statusText = String(
|
||||
localized: "Direct mode requires a trusted HTTPS Gateway endpoint.")
|
||||
return
|
||||
}
|
||||
let secureEndpoints = link.connectionEndpoints.filter(\.tls)
|
||||
@@ -151,7 +160,7 @@ final class WatchDirectNode {
|
||||
gatewayID: Self.gatewayID(for: secureLink),
|
||||
setupSentAtMs: sentAtMs)
|
||||
guard Self.saveConfiguration(configuration) else {
|
||||
self.statusText = "Could not save direct connection securely."
|
||||
self.statusText = String(localized: "Could not save direct connection securely.")
|
||||
return
|
||||
}
|
||||
if let previousConfiguration,
|
||||
@@ -168,7 +177,7 @@ final class WatchDirectNode {
|
||||
self.disconnectActiveSession()
|
||||
self.configuration = configuration
|
||||
self.endpointText = Self.endpointText(configuration)
|
||||
self.statusText = "Setup received. Connecting…"
|
||||
self.statusText = String(localized: "Setup received. Connecting…")
|
||||
self.setEnabled(true)
|
||||
}
|
||||
|
||||
@@ -184,8 +193,8 @@ final class WatchDirectNode {
|
||||
self.connectTask = nil
|
||||
self.isConnected = false
|
||||
self.statusText = self.isConfigured
|
||||
? "Direct connection is off"
|
||||
: "Use iPhone Settings to enable direct connection."
|
||||
? String(localized: "Direct connection is off")
|
||||
: String(localized: "Use iPhone Settings to enable direct connection.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +222,7 @@ final class WatchDirectNode {
|
||||
self.connectTask = nil
|
||||
self.isConnected = false
|
||||
if self.isEnabled, self.isConfigured {
|
||||
self.statusText = "Reconnects when OpenClaw is active"
|
||||
self.statusText = String(localized: "Reconnects when OpenClaw is active")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +275,10 @@ final class WatchDirectNode {
|
||||
}
|
||||
guard self.isCurrentConnection(generation, configuration: configuration) else { return }
|
||||
self.statusText = lastError.map {
|
||||
"Direct connection failed: \($0.localizedDescription)"
|
||||
} ?? "No usable Gateway endpoint"
|
||||
String(
|
||||
format: String(localized: "Direct connection failed: %@"),
|
||||
$0.localizedDescription)
|
||||
} ?? String(localized: "No usable Gateway endpoint")
|
||||
do {
|
||||
try await Task.sleep(for: .seconds(3))
|
||||
} catch {
|
||||
@@ -283,7 +294,7 @@ final class WatchDirectNode {
|
||||
generation: Int) async throws
|
||||
{
|
||||
try self.requireCurrentConnection(generation, configuration: configuration)
|
||||
self.statusText = "Connecting directly…"
|
||||
self.statusText = String(localized: "Connecting directly…")
|
||||
guard let identity = DeviceIdentityStore.loadOrCreatePersisted(profile: .primary) else {
|
||||
throw HTTPError(status: 0, detail: "Could not save the watch device identity")
|
||||
}
|
||||
@@ -344,7 +355,7 @@ final class WatchDirectNode {
|
||||
self.activeSession = session
|
||||
defer { releaseActiveSession(session) }
|
||||
self.isConnected = true
|
||||
self.statusText = "Connected directly"
|
||||
self.statusText = String(localized: "Connected directly")
|
||||
while self.isCurrentConnection(generation, configuration: configuration) {
|
||||
let pollData = try await request(
|
||||
baseURL: baseURL,
|
||||
|
||||
@@ -254,22 +254,26 @@ import WatchKit
|
||||
}
|
||||
|
||||
var gatewaySummaryText: String {
|
||||
guard let appSnapshot else { return "Waiting for iPhone" }
|
||||
return appSnapshot.gatewayConnected ? "Connected" : appSnapshot.gatewayStatusText
|
||||
guard let appSnapshot else { return String(localized: "Waiting for iPhone") }
|
||||
return appSnapshot.gatewayConnected
|
||||
? String(localized: "Connected")
|
||||
: appSnapshot.gatewayStatusText
|
||||
}
|
||||
|
||||
var talkSummaryText: String {
|
||||
guard let appSnapshot else { return "Not synced" }
|
||||
guard let appSnapshot else { return String(localized: "Not synced") }
|
||||
if appSnapshot.talkListening {
|
||||
return "Listening"
|
||||
return String(localized: "Listening")
|
||||
}
|
||||
if appSnapshot.talkSpeaking {
|
||||
return "Speaking"
|
||||
return String(localized: "Speaking")
|
||||
}
|
||||
if appSnapshot.talkEnabled {
|
||||
return appSnapshot.talkStatusText.isEmpty ? "Ready" : appSnapshot.talkStatusText
|
||||
return appSnapshot.talkStatusText.isEmpty
|
||||
? String(localized: "Ready")
|
||||
: appSnapshot.talkStatusText
|
||||
}
|
||||
return "Off"
|
||||
return String(localized: "Off")
|
||||
}
|
||||
|
||||
func beginExecApprovalReviewLoading() {
|
||||
@@ -278,7 +282,7 @@ import WatchKit
|
||||
return
|
||||
}
|
||||
self.isExecApprovalReviewLoading = true
|
||||
self.execApprovalReviewStatusText = "Loading approval from iPhone…"
|
||||
self.execApprovalReviewStatusText = String(localized: "Loading approval from iPhone…")
|
||||
self.execApprovalReviewStatusAt = Date()
|
||||
}
|
||||
|
||||
@@ -398,7 +402,7 @@ import WatchKit
|
||||
Task {
|
||||
await self.postLocalNotification(
|
||||
identifier: notificationIdentifier,
|
||||
title: "Exec approval required",
|
||||
title: String(localized: "Exec approval required"),
|
||||
body: message.approval.commandPreview ?? message.approval.commandText,
|
||||
risk: message.approval.risk?.rawValue,
|
||||
stillCurrent: {
|
||||
@@ -504,7 +508,7 @@ import WatchKit
|
||||
_ = self.recordExecApprovalTerminal(
|
||||
approvalId: record.approvalID,
|
||||
gatewayStableID: record.approval.gatewayStableID,
|
||||
outcomeText: "Approval resolved elsewhere",
|
||||
outcomeText: String(localized: "Approval resolved elsewhere"),
|
||||
authoritativeOutcome: false)
|
||||
return false
|
||||
}
|
||||
@@ -606,17 +610,19 @@ import WatchKit
|
||||
}
|
||||
|
||||
func markAppSnapshotRequestStarted() {
|
||||
self.appSnapshotStatusText = "Refreshing from iPhone…"
|
||||
self.appSnapshotStatusText = String(localized: "Refreshing from iPhone…")
|
||||
self.persistState()
|
||||
}
|
||||
|
||||
func markAppSnapshotRequestResult(_ result: WatchReplySendResult) {
|
||||
if let errorMessage = result.errorMessage, !errorMessage.isEmpty {
|
||||
self.appSnapshotStatusText = "Refresh failed: \(errorMessage)"
|
||||
self.appSnapshotStatusText = String(
|
||||
format: String(localized: "Refresh failed: %@"),
|
||||
errorMessage)
|
||||
} else if result.deliveredImmediately {
|
||||
self.appSnapshotStatusText = "Refresh requested"
|
||||
self.appSnapshotStatusText = String(localized: "Refresh requested")
|
||||
} else if result.queuedForDelivery {
|
||||
self.appSnapshotStatusText = "Refresh queued"
|
||||
self.appSnapshotStatusText = String(localized: "Refresh queued")
|
||||
} else {
|
||||
self.appSnapshotStatusText = nil
|
||||
}
|
||||
@@ -639,25 +645,33 @@ import WatchKit
|
||||
}
|
||||
|
||||
func markAppCommandSending(_ command: WatchAppCommand) {
|
||||
self.appCommandStatusText = "Sending \(Self.commandLabel(command))…"
|
||||
self.appCommandStatusText = String(
|
||||
format: String(localized: "Sending %@…"),
|
||||
Self.commandLabel(command))
|
||||
self.persistState()
|
||||
}
|
||||
|
||||
func markAppCommandBlocked(_ command: WatchAppCommand, reason: String) {
|
||||
self.appCommandStatusText = "\(Self.commandLabel(command)): \(reason)"
|
||||
self.appCommandStatusText = String(
|
||||
format: String(localized: "%@: %@"),
|
||||
Self.commandLabel(command),
|
||||
reason)
|
||||
self.persistState()
|
||||
}
|
||||
|
||||
func markAppCommandResult(_ result: WatchReplySendResult, command: WatchAppCommand) {
|
||||
let label = Self.commandLabel(command)
|
||||
if let errorMessage = result.errorMessage, !errorMessage.isEmpty {
|
||||
self.appCommandStatusText = "\(label) failed: \(errorMessage)"
|
||||
self.appCommandStatusText = String(
|
||||
format: String(localized: "%@ failed: %@"),
|
||||
label,
|
||||
errorMessage)
|
||||
} else if result.deliveredImmediately {
|
||||
self.appCommandStatusText = "\(label): sent"
|
||||
self.appCommandStatusText = String(format: String(localized: "%@: sent"), label)
|
||||
} else if result.queuedForDelivery {
|
||||
self.appCommandStatusText = "\(label): queued"
|
||||
self.appCommandStatusText = String(format: String(localized: "%@: queued"), label)
|
||||
} else {
|
||||
self.appCommandStatusText = "\(label): sent"
|
||||
self.appCommandStatusText = String(format: String(localized: "%@: sent"), label)
|
||||
}
|
||||
self.persistState()
|
||||
}
|
||||
@@ -675,11 +689,11 @@ extension WatchInboxStore {
|
||||
} else {
|
||||
switch message.decision {
|
||||
case .allowOnce:
|
||||
"Allowed once"
|
||||
String(localized: "Allowed once")
|
||||
case .deny:
|
||||
"Denied"
|
||||
String(localized: "Denied")
|
||||
case nil:
|
||||
"Approval resolved"
|
||||
String(localized: "Approval resolved")
|
||||
}
|
||||
}
|
||||
let terminalOutcomeText = self.recordExecApprovalTerminal(
|
||||
@@ -697,15 +711,15 @@ extension WatchInboxStore {
|
||||
guard self.routeGatewayPayload(.execApprovalExpired(message: message)) else { return }
|
||||
let statusText = switch message.reason {
|
||||
case .expired:
|
||||
"Approval expired"
|
||||
String(localized: "Approval expired")
|
||||
case .notFound:
|
||||
"Approval no longer available"
|
||||
String(localized: "Approval no longer available")
|
||||
case .resolved:
|
||||
"Approval resolved elsewhere"
|
||||
String(localized: "Approval resolved elsewhere")
|
||||
case .replaced:
|
||||
"Approval replaced"
|
||||
String(localized: "Approval replaced")
|
||||
case .unavailable:
|
||||
"Approval unavailable"
|
||||
String(localized: "Approval unavailable")
|
||||
}
|
||||
let terminalOutcomeText = self.recordExecApprovalTerminal(
|
||||
approvalId: message.approvalId,
|
||||
@@ -773,7 +787,9 @@ extension WatchInboxStore {
|
||||
self.execApprovals[index].isResolving = true
|
||||
self.execApprovals[index].pendingDecision = decision
|
||||
self.execApprovals[index].activeResolutionAttemptID = attemptID
|
||||
self.execApprovals[index].statusText = "Sending \(Self.decisionLabel(decision))…"
|
||||
self.execApprovals[index].statusText = String(
|
||||
format: String(localized: "Sending %@…"),
|
||||
Self.decisionLabel(decision))
|
||||
self.execApprovals[index].statusAt = Date()
|
||||
self.persistState()
|
||||
return attemptID
|
||||
@@ -802,16 +818,21 @@ extension WatchInboxStore {
|
||||
switch result.delivery {
|
||||
case .delivered:
|
||||
self.execApprovals[index].isResolving = true
|
||||
self.execApprovals[index].statusText = "\(Self.decisionLabel(decision)): sent"
|
||||
self.execApprovals[index].statusText = String(
|
||||
format: String(localized: "%@: sent"),
|
||||
Self.decisionLabel(decision))
|
||||
case .queued:
|
||||
self.execApprovals[index].isResolving = true
|
||||
self.execApprovals[index].statusText = "\(Self.decisionLabel(decision)): queued"
|
||||
self.execApprovals[index].statusText = String(
|
||||
format: String(localized: "%@: queued"),
|
||||
Self.decisionLabel(decision))
|
||||
case .notSent:
|
||||
// Only a definitive pre-dispatch failure unlocks locally. Uncertain sends stay
|
||||
// frozen until a canonical retry reset or terminal event arrives.
|
||||
self.execApprovals[index].isResolving = false
|
||||
self.execApprovals[index].activeResolutionAttemptID = nil
|
||||
self.execApprovals[index].statusText = "Couldn't reach iPhone. Tap to retry."
|
||||
self.execApprovals[index].statusText = String(
|
||||
localized: "Couldn't reach iPhone. Tap to retry.")
|
||||
}
|
||||
self.execApprovals[index].pendingDecision = result.delivery == .notSent ? nil : decision
|
||||
self.execApprovals[index].statusAt = Date()
|
||||
@@ -1478,7 +1499,9 @@ extension WatchInboxStore {
|
||||
|
||||
func markReplySending(actionLabel: String) {
|
||||
self.isReplySending = true
|
||||
self.replyStatusText = "Sending \(actionLabel)…"
|
||||
self.replyStatusText = String(
|
||||
format: String(localized: "Sending %@…"),
|
||||
actionLabel)
|
||||
self.replyStatusAt = Date()
|
||||
self.persistState()
|
||||
}
|
||||
@@ -1486,13 +1509,21 @@ extension WatchInboxStore {
|
||||
func markReplyResult(_ result: WatchReplySendResult, actionLabel: String) {
|
||||
self.isReplySending = false
|
||||
if let errorMessage = result.errorMessage, !errorMessage.isEmpty {
|
||||
self.replyStatusText = "Failed: \(errorMessage)"
|
||||
self.replyStatusText = String(
|
||||
format: String(localized: "Failed: %@"),
|
||||
errorMessage)
|
||||
} else if result.deliveredImmediately {
|
||||
self.replyStatusText = "\(actionLabel): sent"
|
||||
self.replyStatusText = String(
|
||||
format: String(localized: "%@: sent"),
|
||||
actionLabel)
|
||||
} else if result.queuedForDelivery {
|
||||
self.replyStatusText = "\(actionLabel): queued"
|
||||
self.replyStatusText = String(
|
||||
format: String(localized: "%@: queued"),
|
||||
actionLabel)
|
||||
} else {
|
||||
self.replyStatusText = "\(actionLabel): sent"
|
||||
self.replyStatusText = String(
|
||||
format: String(localized: "%@: sent"),
|
||||
actionLabel)
|
||||
}
|
||||
self.replyStatusAt = Date()
|
||||
self.persistState()
|
||||
@@ -1529,24 +1560,24 @@ extension WatchInboxStore {
|
||||
private static func decisionLabel(_ decision: WatchExecApprovalDecision) -> String {
|
||||
switch decision {
|
||||
case .allowOnce:
|
||||
"Allow Once"
|
||||
String(localized: "Allow Once")
|
||||
case .deny:
|
||||
"Deny"
|
||||
String(localized: "Deny")
|
||||
}
|
||||
}
|
||||
|
||||
private static func commandLabel(_ command: WatchAppCommand) -> String {
|
||||
switch command {
|
||||
case .refresh:
|
||||
"Refresh"
|
||||
String(localized: "Refresh")
|
||||
case .openChat:
|
||||
"Open Chat"
|
||||
String(localized: "Open Chat")
|
||||
case .sendChat:
|
||||
"Chat"
|
||||
String(localized: "Chat")
|
||||
case .startTalk:
|
||||
"Start Talk"
|
||||
String(localized: "Start Talk")
|
||||
case .stopTalk:
|
||||
"Stop Talk"
|
||||
String(localized: "Stop Talk")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import WatchKit
|
||||
|
||||
private enum WatchTextValue: ExpressibleByStringLiteral {
|
||||
case localized(LocalizedStringKey)
|
||||
case verbatim(String)
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
self = .localized(LocalizedStringKey(value))
|
||||
}
|
||||
|
||||
static func localized(_ value: String) -> Self {
|
||||
.localized(LocalizedStringKey(value))
|
||||
}
|
||||
|
||||
var text: Text {
|
||||
switch self {
|
||||
case let .localized(key):
|
||||
Text(key)
|
||||
case let .verbatim(value):
|
||||
Text(verbatim: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WatchInboxView: View {
|
||||
var store: WatchInboxStore
|
||||
var directNode: WatchDirectNode
|
||||
@@ -78,8 +101,8 @@ private struct WatchControlSurfaceView: View {
|
||||
self.pageRail
|
||||
WatchFaceHeader(
|
||||
section: "Now",
|
||||
title: self.greetingText,
|
||||
subtitle: self.connectionLine,
|
||||
title: .verbatim(self.greetingText),
|
||||
subtitle: .verbatim(self.connectionLine),
|
||||
avatarImageSource: self.avatarImageSource,
|
||||
avatarText: self.avatarText)
|
||||
|
||||
@@ -87,10 +110,10 @@ private struct WatchControlSurfaceView: View {
|
||||
self.primaryDestination
|
||||
} label: {
|
||||
WatchHeroCard(
|
||||
label: self.primaryLabel,
|
||||
title: self.primaryTitle,
|
||||
subtitle: self.primarySubtitle,
|
||||
accessory: self.store.talkSummaryText)
|
||||
label: .verbatim(self.primaryLabel),
|
||||
title: .verbatim(self.primaryTitle),
|
||||
subtitle: .verbatim(self.primarySubtitle),
|
||||
accessory: .localized(self.store.talkSummaryText))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@@ -116,7 +139,7 @@ private struct WatchControlSurfaceView: View {
|
||||
WatchFaceHeader(
|
||||
section: "Inbox",
|
||||
title: "What needs you",
|
||||
subtitle: self.inboxSubtitle,
|
||||
subtitle: .verbatim(self.inboxSubtitle),
|
||||
avatarImageSource: self.avatarImageSource,
|
||||
avatarText: self.avatarText)
|
||||
|
||||
@@ -133,7 +156,10 @@ private struct WatchControlSurfaceView: View {
|
||||
WatchHeroCard(
|
||||
label: "Clear",
|
||||
title: "Caught up",
|
||||
subtitle: self.store.hasAppSnapshot ? "No chats or approvals need you" : "Waiting for iPhone sync",
|
||||
subtitle: .localized(
|
||||
self.store.hasAppSnapshot
|
||||
? "No chats or approvals need you"
|
||||
: "Waiting for iPhone sync"),
|
||||
accessory: "Ready")
|
||||
}
|
||||
|
||||
@@ -153,9 +179,9 @@ private struct WatchControlSurfaceView: View {
|
||||
} label: {
|
||||
WatchStackCard(
|
||||
label: "Chat",
|
||||
title: self.chatPreviewTitle,
|
||||
subtitle: self.chatPreviewSubtitle,
|
||||
badge: "\(self.chatCount)",
|
||||
title: .verbatim(self.chatPreviewTitle),
|
||||
subtitle: .verbatim(self.chatPreviewSubtitle),
|
||||
badge: self.chatCount.formatted(),
|
||||
isProminent: self.approvalCount == 0)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -169,9 +195,9 @@ private struct WatchControlSurfaceView: View {
|
||||
} label: {
|
||||
WatchStackCard(
|
||||
label: "Approvals",
|
||||
title: self.approvalHeadline,
|
||||
subtitle: self.approvalSubtitle,
|
||||
badge: "\(self.approvalCount)",
|
||||
title: .verbatim(self.approvalHeadline),
|
||||
subtitle: .verbatim(self.approvalSubtitle),
|
||||
badge: self.approvalCount.formatted(),
|
||||
isProminent: true)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -181,13 +207,13 @@ private struct WatchControlSurfaceView: View {
|
||||
@ViewBuilder private var inboxPromptBlock: some View {
|
||||
if self.store.hasMessagePrompt {
|
||||
WatchHeroCard(
|
||||
label: self.store.kind ?? "Latest",
|
||||
title: self.store.title,
|
||||
subtitle: self.store.body,
|
||||
accessory: self.updatedText)
|
||||
label: .verbatim(self.store.kind ?? String(localized: "Latest")),
|
||||
title: .verbatim(self.store.title),
|
||||
subtitle: .verbatim(self.store.body),
|
||||
accessory: .verbatim(self.updatedText))
|
||||
|
||||
if let details = promptDetails {
|
||||
WatchDetailText(text: details)
|
||||
WatchDetailText(text: .verbatim(details))
|
||||
}
|
||||
|
||||
ForEach(self.store.actions) { action in
|
||||
@@ -217,15 +243,17 @@ private struct WatchControlSurfaceView: View {
|
||||
|
||||
private var inboxSubtitle: String {
|
||||
if self.approvalCount > 0 {
|
||||
return "Approval waiting"
|
||||
return String(localized: "Approval waiting")
|
||||
}
|
||||
if self.chatCount > 0 {
|
||||
return self.chatStatusText
|
||||
}
|
||||
if self.store.hasMessagePrompt {
|
||||
return self.store.kind ?? "Latest update"
|
||||
return self.store.kind ?? String(localized: "Latest update")
|
||||
}
|
||||
return self.store.hasAppSnapshot ? "Nothing waiting" : "Waiting for iPhone"
|
||||
return self.store.hasAppSnapshot
|
||||
? String(localized: "Nothing waiting")
|
||||
: String(localized: "Waiting for iPhone")
|
||||
}
|
||||
|
||||
private var approvalsFace: some View {
|
||||
@@ -233,17 +261,17 @@ private struct WatchControlSurfaceView: View {
|
||||
self.pageRail
|
||||
WatchFaceHeader(
|
||||
section: "Approvals",
|
||||
title: self.approvalHeadline,
|
||||
subtitle: self.approvalHeaderSubtitle,
|
||||
title: .verbatim(self.approvalHeadline),
|
||||
subtitle: .verbatim(self.approvalHeaderSubtitle),
|
||||
avatarImageSource: self.avatarImageSource,
|
||||
avatarText: self.avatarText)
|
||||
|
||||
if let record = self.store.activeExecApproval {
|
||||
WatchHeroCard(
|
||||
label: "Approval needed",
|
||||
title: record.approval.commandPreview ?? record.approval.commandText,
|
||||
subtitle: self.approvalDecisionSubtitle(record),
|
||||
accessory: self.approvalAccessory(record))
|
||||
title: .verbatim(record.approval.commandPreview ?? record.approval.commandText),
|
||||
subtitle: .verbatim(self.approvalDecisionSubtitle(record)),
|
||||
accessory: .verbatim(self.approvalAccessory(record)))
|
||||
|
||||
if let warningText = WatchExecApprovalDisplay.warningText(record.approval.warningText) {
|
||||
WatchApprovalWarning(text: warningText)
|
||||
@@ -269,13 +297,17 @@ private struct WatchControlSurfaceView: View {
|
||||
WatchHeroCard(
|
||||
label: "Loading",
|
||||
title: "Loading approval",
|
||||
subtitle: self.store.execApprovalReviewStatusText ?? "Waiting for your iPhone",
|
||||
subtitle: .verbatim(
|
||||
self.store.execApprovalReviewStatusText
|
||||
?? String(localized: "Waiting for your iPhone")),
|
||||
accessory: "Syncing")
|
||||
} else if self.approvalCount > 0 || self.store.shouldShowExecApprovalReviewStatus {
|
||||
WatchHeroCard(
|
||||
label: "Unavailable",
|
||||
title: "Approval not loaded",
|
||||
subtitle: self.store.execApprovalReviewStatusText ?? "Approval details have not loaded",
|
||||
subtitle: .verbatim(
|
||||
self.store.execApprovalReviewStatusText
|
||||
?? String(localized: "Approval details have not loaded")),
|
||||
accessory: "Retry")
|
||||
|
||||
WatchSecondaryButton(title: "Review again") {
|
||||
@@ -285,7 +317,9 @@ private struct WatchControlSurfaceView: View {
|
||||
WatchHeroCard(
|
||||
label: "Clear",
|
||||
title: "No approvals waiting",
|
||||
subtitle: self.store.lastExecApprovalOutcomeText ?? "You are caught up",
|
||||
subtitle: .verbatim(
|
||||
self.store.lastExecApprovalOutcomeText
|
||||
?? String(localized: "You are caught up")),
|
||||
accessory: "Ready")
|
||||
}
|
||||
|
||||
@@ -305,18 +339,26 @@ private struct WatchControlSurfaceView: View {
|
||||
self.pageRail
|
||||
WatchFaceHeader(
|
||||
section: "Connection",
|
||||
title: self.directNode.isConnected ? "Watch node online" : "Direct Gateway",
|
||||
subtitle: self.directNode.statusText,
|
||||
title: .verbatim(self.directNode.isConnected
|
||||
? String(localized: "Watch node online")
|
||||
: String(localized: "Direct Gateway")),
|
||||
subtitle: .verbatim(self.directNode.statusText),
|
||||
avatarImageSource: self.avatarImageSource,
|
||||
avatarText: self.avatarText)
|
||||
|
||||
WatchHeroCard(
|
||||
label: self.directNode.isConnected ? "Direct" : "Setup",
|
||||
title: self.directNode.endpointText ?? "Enable from iPhone",
|
||||
subtitle: self.directNode.isConfigured
|
||||
? "Uses Wi-Fi or cellular while OpenClaw is active"
|
||||
: "Open iPhone Settings → Apple Watch",
|
||||
accessory: self.directNode.isConnected ? "Online" : "Offline")
|
||||
label: .verbatim(self.directNode.isConnected
|
||||
? String(localized: "Direct")
|
||||
: String(localized: "Setup")),
|
||||
title: .verbatim(
|
||||
self.directNode.endpointText ?? String(localized: "Enable from iPhone")),
|
||||
subtitle: .localized(
|
||||
self.directNode.isConfigured
|
||||
? "Uses Wi-Fi or cellular while OpenClaw is active"
|
||||
: "Open iPhone Settings → Apple Watch"),
|
||||
accessory: .verbatim(self.directNode.isConnected
|
||||
? String(localized: "Online")
|
||||
: String(localized: "Offline")))
|
||||
|
||||
WatchDetailText(
|
||||
text: """
|
||||
@@ -382,23 +424,27 @@ private struct WatchControlSurfaceView: View {
|
||||
}
|
||||
|
||||
private var chatCountText: String {
|
||||
self.chatCount == 0 ? "0" : "\(self.chatCount)"
|
||||
self.chatCount.formatted()
|
||||
}
|
||||
|
||||
private var approvalCountText: String {
|
||||
self.approvalCount == 0 ? "0" : "\(self.approvalCount)"
|
||||
self.approvalCount.formatted()
|
||||
}
|
||||
|
||||
private var connectionLine: String {
|
||||
if let snapshot = store.appSnapshot {
|
||||
return snapshot.gatewayConnected ? "AI agent online" : "Reconnect on iPhone"
|
||||
return snapshot.gatewayConnected
|
||||
? String(localized: "AI agent online")
|
||||
: String(localized: "Reconnect on iPhone")
|
||||
}
|
||||
return "Pair iPhone"
|
||||
return String(localized: "Pair iPhone")
|
||||
}
|
||||
|
||||
private var primaryLabel: String {
|
||||
if self.store.activeExecApproval != nil { return "Next up" }
|
||||
return self.store.appSnapshot?.gatewayConnected == true ? "Running" : "Pairing"
|
||||
if self.store.activeExecApproval != nil { return String(localized: "Next up") }
|
||||
return self.store.appSnapshot?.gatewayConnected == true
|
||||
? String(localized: "Running")
|
||||
: String(localized: "Pairing")
|
||||
}
|
||||
|
||||
private var primaryTitle: String {
|
||||
@@ -413,66 +459,80 @@ private struct WatchControlSurfaceView: View {
|
||||
|
||||
private var primarySubtitle: String {
|
||||
if self.store.activeExecApproval != nil {
|
||||
return "Approval waiting on your wrist"
|
||||
return String(localized: "Approval waiting on your wrist")
|
||||
}
|
||||
if self.chatCount > 0 {
|
||||
return self.chatStatusText
|
||||
}
|
||||
return self.store.hasAppSnapshot ? "Ready for quick actions" : "Waiting for iPhone sync"
|
||||
return self.store.hasAppSnapshot
|
||||
? String(localized: "Ready for quick actions")
|
||||
: String(localized: "Waiting for iPhone sync")
|
||||
}
|
||||
|
||||
private var approvalHeadline: String {
|
||||
self.approvalCount == 1 ? "1 approval waiting" : "\(self.approvalCount) approvals"
|
||||
if self.approvalCount == 1 {
|
||||
return String(localized: "1 approval waiting")
|
||||
}
|
||||
return String(
|
||||
format: String(localized: "%@ approvals waiting"),
|
||||
self.approvalCount.formatted())
|
||||
}
|
||||
|
||||
private var approvalSubtitle: String {
|
||||
guard let record = store.activeExecApproval else { return "No approvals waiting" }
|
||||
guard let record = store.activeExecApproval else {
|
||||
return String(localized: "No approvals waiting")
|
||||
}
|
||||
return record.approval.commandPreview ?? record.approval.commandText
|
||||
}
|
||||
|
||||
private var approvalHeaderSubtitle: String {
|
||||
self.approvalCount > 0 ? "Decide from watch" : "No approvals"
|
||||
self.approvalCount > 0
|
||||
? String(localized: "Decide from watch")
|
||||
: String(localized: "No approvals")
|
||||
}
|
||||
|
||||
private func approvalDecisionSubtitle(_ record: WatchExecApprovalRecord) -> String {
|
||||
var parts: [String] = []
|
||||
if let expiresText = expiryText(record.approval.expiresAtMs) {
|
||||
parts.append("Expires in \(expiresText)")
|
||||
parts.append(
|
||||
String(
|
||||
format: String(localized: "Expires in %@"),
|
||||
expiresText))
|
||||
}
|
||||
if let host = record.approval.host, !host.isEmpty {
|
||||
parts.append(host)
|
||||
}
|
||||
if parts.isEmpty {
|
||||
parts.append("Review before it runs")
|
||||
parts.append(String(localized: "Review before it runs"))
|
||||
}
|
||||
return parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
private func approvalAccessory(_ record: WatchExecApprovalRecord) -> String {
|
||||
if record.isResolving {
|
||||
return "Sending"
|
||||
return String(localized: "Sending")
|
||||
}
|
||||
if let risk = approvalRiskText(record.approval.risk) {
|
||||
return risk
|
||||
}
|
||||
return "Review"
|
||||
return String(localized: "Review")
|
||||
}
|
||||
|
||||
private func approvalRiskText(_ risk: WatchRiskLevel?) -> String? {
|
||||
switch risk {
|
||||
case .high:
|
||||
"High risk"
|
||||
String(localized: "High risk")
|
||||
case .medium:
|
||||
"Medium risk"
|
||||
String(localized: "Medium risk")
|
||||
case .low:
|
||||
"Low risk"
|
||||
String(localized: "Low risk")
|
||||
case nil:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
private var chatPreviewTitle: String {
|
||||
guard let item = chatItems.last else { return "No chat synced" }
|
||||
guard let item = chatItems.last else { return String(localized: "No chat synced") }
|
||||
return self.roleTitle(item.role)
|
||||
}
|
||||
|
||||
@@ -485,9 +545,16 @@ private struct WatchControlSurfaceView: View {
|
||||
return status
|
||||
}
|
||||
if self.chatCount > 0 {
|
||||
return self.chatCount == 1 ? "1 recent message" : "\(self.chatCount) recent messages"
|
||||
if self.chatCount == 1 {
|
||||
return String(localized: "1 recent message")
|
||||
}
|
||||
return String(
|
||||
format: String(localized: "%@ recent messages"),
|
||||
self.chatCount.formatted())
|
||||
}
|
||||
return self.store.hasAppSnapshot ? "No messages synced" : "Waiting for iPhone"
|
||||
return self.store.hasAppSnapshot
|
||||
? String(localized: "No messages synced")
|
||||
: String(localized: "Waiting for iPhone")
|
||||
}
|
||||
|
||||
private var chatSendStatusText: String? {
|
||||
@@ -502,9 +569,9 @@ private struct WatchControlSurfaceView: View {
|
||||
return greetingTextOverride
|
||||
}
|
||||
let hour = Calendar.current.component(.hour, from: Date())
|
||||
if hour < 12 { return "Good morning" }
|
||||
if hour < 18 { return "Good afternoon" }
|
||||
return "Good evening"
|
||||
if hour < 12 { return String(localized: "Good morning") }
|
||||
if hour < 18 { return String(localized: "Good afternoon") }
|
||||
return String(localized: "Good evening")
|
||||
}
|
||||
|
||||
private var statusLine: String {
|
||||
@@ -517,20 +584,22 @@ private struct WatchControlSurfaceView: View {
|
||||
if let replyStatus = store.replyStatusText, !replyStatus.isEmpty {
|
||||
return replyStatus
|
||||
}
|
||||
return self.store.hasAppSnapshot ? "Synced" : "Waiting for iPhone"
|
||||
return self.store.hasAppSnapshot
|
||||
? String(localized: "Synced")
|
||||
: String(localized: "Waiting for iPhone")
|
||||
}
|
||||
|
||||
private var updatedText: String {
|
||||
guard let updatedAt = store.updatedAt else { return "Just now" }
|
||||
guard let updatedAt = store.updatedAt else { return String(localized: "Just now") }
|
||||
return updatedAt.formatted(date: .omitted, time: .shortened)
|
||||
}
|
||||
|
||||
private func roleTitle(_ role: String) -> String {
|
||||
switch role.lowercased() {
|
||||
case "user":
|
||||
"You"
|
||||
String(localized: "You")
|
||||
case "system":
|
||||
"System"
|
||||
String(localized: "System")
|
||||
default:
|
||||
"OpenClaw"
|
||||
}
|
||||
@@ -539,11 +608,11 @@ private struct WatchControlSurfaceView: View {
|
||||
private func actionSubtitle(_ action: WatchPromptAction) -> String {
|
||||
switch action.style?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
|
||||
case "destructive":
|
||||
"Requires confirmation"
|
||||
String(localized: "Requires confirmation")
|
||||
case "cancel":
|
||||
"Dismiss this update"
|
||||
String(localized: "Dismiss this update")
|
||||
default:
|
||||
"Send from watch"
|
||||
String(localized: "Send from watch")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,9 +620,11 @@ private struct WatchControlSurfaceView: View {
|
||||
guard let expiresAtMs else { return nil }
|
||||
let deltaSeconds = max(0, (expiresAtMs - Int64(Date().timeIntervalSince1970 * 1000)) / 1000)
|
||||
if deltaSeconds < 60 {
|
||||
return "<1m"
|
||||
return String(localized: "less than 1 min")
|
||||
}
|
||||
return "\(deltaSeconds / 60)m"
|
||||
return String(
|
||||
format: String(localized: "%@ min"),
|
||||
(deltaSeconds / 60).formatted())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,9 +756,9 @@ private struct WatchClawAvatar: View {
|
||||
}
|
||||
|
||||
private struct WatchFaceHeader: View {
|
||||
let section: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let section: WatchTextValue
|
||||
let title: WatchTextValue
|
||||
let subtitle: WatchTextValue
|
||||
var avatarImageSource: String?
|
||||
var avatarText: String?
|
||||
|
||||
@@ -698,15 +769,15 @@ private struct WatchFaceHeader: View {
|
||||
imageSource: self.avatarImageSource,
|
||||
text: self.avatarText)
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
Text(self.section)
|
||||
self.section.text
|
||||
.font(WatchClawType.label(size: 10, weight: .bold))
|
||||
.foregroundStyle(WatchClawStyle.accent)
|
||||
.lineLimit(1)
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(WatchClawType.title(size: 18))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
Text(self.subtitle)
|
||||
self.subtitle.text
|
||||
.font(WatchClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
@@ -716,29 +787,29 @@ private struct WatchFaceHeader: View {
|
||||
}
|
||||
|
||||
private struct WatchHeroCard: View {
|
||||
let label: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let accessory: String
|
||||
let label: WatchTextValue
|
||||
let title: WatchTextValue
|
||||
let subtitle: WatchTextValue
|
||||
let accessory: WatchTextValue
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .center) {
|
||||
Text(self.label)
|
||||
self.label.text
|
||||
.font(WatchClawType.label(size: 10, weight: .bold))
|
||||
.foregroundStyle(WatchClawStyle.accent)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 4)
|
||||
Text(self.accessory)
|
||||
self.accessory.text
|
||||
.font(WatchClawType.label(size: 10, weight: .semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(WatchClawType.title(size: 19))
|
||||
.lineLimit(3)
|
||||
.minimumScaleFactor(0.75)
|
||||
Text(self.subtitle)
|
||||
self.subtitle.text
|
||||
.font(WatchClawType.body(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
@@ -757,10 +828,10 @@ private struct WatchHeroCard: View {
|
||||
}
|
||||
|
||||
private struct WatchDetailText: View {
|
||||
let text: String
|
||||
let text: WatchTextValue
|
||||
|
||||
var body: some View {
|
||||
Text(self.text)
|
||||
self.text.text
|
||||
.font(WatchClawType.body(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(5)
|
||||
@@ -801,7 +872,7 @@ private struct WatchCompactStatusStrip: View {
|
||||
}
|
||||
|
||||
private struct WatchCompactMetric: View {
|
||||
let label: String
|
||||
let label: LocalizedStringKey
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
@@ -898,23 +969,23 @@ private struct WatchSecondaryButton: View {
|
||||
}
|
||||
|
||||
private struct WatchStackCard: View {
|
||||
let label: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
let label: WatchTextValue
|
||||
let title: WatchTextValue
|
||||
let subtitle: WatchTextValue
|
||||
let badge: String?
|
||||
var isProminent = false
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.label)
|
||||
self.label.text
|
||||
.font(WatchClawType.label(size: 10, weight: .bold))
|
||||
.foregroundStyle(WatchClawStyle.accent)
|
||||
.lineLimit(1)
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(WatchClawType.title(size: 17))
|
||||
.lineLimit(1)
|
||||
Text(self.subtitle)
|
||||
self.subtitle.text
|
||||
.font(WatchClawType.body(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
@@ -959,8 +1030,8 @@ private struct WatchActionCard: View {
|
||||
Button(action: self.action) {
|
||||
WatchStackCard(
|
||||
label: "OpenClaw",
|
||||
title: self.title,
|
||||
subtitle: self.subtitle,
|
||||
title: .verbatim(self.title),
|
||||
subtitle: .verbatim(self.subtitle),
|
||||
badge: nil)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -1053,7 +1124,7 @@ private enum WatchExecApprovalDisplay {
|
||||
if !statusText.isEmpty {
|
||||
return statusText
|
||||
}
|
||||
return record.isResolving ? "Sending decision..." : nil
|
||||
return record.isResolving ? String(localized: "Sending decision...") : nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,9 +1176,9 @@ private struct WatchChatBubble: View {
|
||||
private var roleTitle: String {
|
||||
switch self.item.role.lowercased() {
|
||||
case "user":
|
||||
"You"
|
||||
String(localized: "You")
|
||||
case "system":
|
||||
"System"
|
||||
String(localized: "System")
|
||||
default:
|
||||
"OpenClaw"
|
||||
}
|
||||
@@ -1207,10 +1278,10 @@ private struct WatchChatTimelineView: View {
|
||||
|
||||
private var voiceStatusText: String? {
|
||||
if self.speechPlayback.isSpeaking {
|
||||
return "Speaking reply…"
|
||||
return String(localized: "Speaking reply…")
|
||||
}
|
||||
if self.voiceTurnTracker.isAwaitingReply {
|
||||
return "Waiting for spoken reply…"
|
||||
return String(localized: "Waiting for spoken reply…")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1367,12 +1438,12 @@ private struct WatchChatComposer: View {
|
||||
|
||||
private var voiceButtonAccessibilityLabel: String {
|
||||
if self.isSpeaking {
|
||||
return "Stop speaking"
|
||||
return String(localized: "Stop speaking")
|
||||
}
|
||||
if self.isAwaitingVoiceReply {
|
||||
return "Cancel voice turn"
|
||||
return String(localized: "Cancel voice turn")
|
||||
}
|
||||
return "Start voice turn"
|
||||
return String(localized: "Start voice turn")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1417,7 +1488,9 @@ private struct WatchExecApprovalListView: View {
|
||||
WatchHeroCard(
|
||||
label: "Clear",
|
||||
title: "No approvals waiting",
|
||||
subtitle: self.store.lastExecApprovalOutcomeText ?? "You are caught up",
|
||||
subtitle: .verbatim(
|
||||
self.store.lastExecApprovalOutcomeText
|
||||
?? String(localized: "You are caught up")),
|
||||
accessory: "Ready")
|
||||
} else {
|
||||
ForEach(self.store.sortedExecApprovals) { record in
|
||||
@@ -1429,8 +1502,8 @@ private struct WatchExecApprovalListView: View {
|
||||
} label: {
|
||||
WatchStackCard(
|
||||
label: "Approval",
|
||||
title: record.approval.commandPreview ?? record.approval.commandText,
|
||||
subtitle: self.metadataLine(for: record),
|
||||
title: .verbatim(record.approval.commandPreview ?? record.approval.commandText),
|
||||
subtitle: .verbatim(self.metadataLine(for: record)),
|
||||
badge: nil)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -1457,16 +1530,18 @@ private struct WatchExecApprovalListView: View {
|
||||
if let statusText = record.statusText, !statusText.isEmpty {
|
||||
parts.append(statusText)
|
||||
}
|
||||
return parts.isEmpty ? "Pending review" : parts.joined(separator: " · ")
|
||||
return parts.isEmpty ? String(localized: "Pending review") : parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
private static func expiresText(_ expiresAtMs: Int64?) -> String? {
|
||||
guard let expiresAtMs else { return nil }
|
||||
let deltaSeconds = max(0, (expiresAtMs - Int64(Date().timeIntervalSince1970 * 1000)) / 1000)
|
||||
if deltaSeconds < 60 {
|
||||
return "Expires in <1m"
|
||||
return String(localized: "Expires in less than 1 min")
|
||||
}
|
||||
return "Expires in \(deltaSeconds / 60)m"
|
||||
return String(
|
||||
format: String(localized: "Expires in %@ min"),
|
||||
(deltaSeconds / 60).formatted())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1478,11 +1553,15 @@ private struct WatchExecApprovalDetailView: View {
|
||||
var body: some View {
|
||||
WatchDetailScroll(title: "Review Command") {
|
||||
WatchHeroCard(
|
||||
label: self.riskText(self.currentRecord?.approval.risk ?? self.record.approval.risk) ?? "Review",
|
||||
label: .verbatim(
|
||||
self.riskText(self.currentRecord?.approval.risk ?? self.record.approval.risk)
|
||||
?? String(localized: "Review")),
|
||||
title: "Command execution",
|
||||
subtitle: self.metadataSummary,
|
||||
accessory: Self
|
||||
.expiresText(self.currentRecord?.approval.expiresAtMs ?? self.record.approval.expiresAtMs) ?? "Now")
|
||||
subtitle: .verbatim(self.metadataSummary),
|
||||
accessory: .verbatim(
|
||||
Self.expiresText(
|
||||
self.currentRecord?.approval.expiresAtMs ?? self.record.approval.expiresAtMs)
|
||||
?? String(localized: "Now")))
|
||||
|
||||
WatchApprovalCommandReview(commandText: self.commandText)
|
||||
|
||||
@@ -1552,17 +1631,19 @@ private struct WatchExecApprovalDetailView: View {
|
||||
if let agentId = approval.agentId, !agentId.isEmpty {
|
||||
parts.append(agentId)
|
||||
}
|
||||
return parts.isEmpty ? "Review command below" : parts.joined(separator: " · ")
|
||||
return parts.isEmpty
|
||||
? String(localized: "Review command below")
|
||||
: parts.joined(separator: " · ")
|
||||
}
|
||||
|
||||
private func riskText(_ risk: WatchRiskLevel?) -> String? {
|
||||
switch risk {
|
||||
case .high:
|
||||
"High risk"
|
||||
String(localized: "High risk")
|
||||
case .medium:
|
||||
"Medium risk"
|
||||
String(localized: "Medium risk")
|
||||
case .low:
|
||||
"Low risk"
|
||||
String(localized: "Low risk")
|
||||
case nil:
|
||||
nil
|
||||
}
|
||||
@@ -1572,14 +1653,16 @@ private struct WatchExecApprovalDetailView: View {
|
||||
guard let expiresAtMs else { return nil }
|
||||
let deltaSeconds = max(0, (expiresAtMs - Int64(Date().timeIntervalSince1970 * 1000)) / 1000)
|
||||
if deltaSeconds < 60 {
|
||||
return "<1 minute"
|
||||
return String(localized: "less than 1 minute")
|
||||
}
|
||||
return "\(deltaSeconds / 60) minutes"
|
||||
return String(
|
||||
format: String(localized: "%@ minutes"),
|
||||
(deltaSeconds / 60).formatted())
|
||||
}
|
||||
}
|
||||
|
||||
private struct WatchDetailScroll<Content: View>: View {
|
||||
let title: String
|
||||
let title: LocalizedStringKey
|
||||
@ViewBuilder var content: Content
|
||||
|
||||
var body: some View {
|
||||
|
||||
Reference in New Issue
Block a user