Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
994
apps/ios/Sources/Design/SettingsProTabSections.swift
Normal file
994
apps/ios/Sources/Design/SettingsProTabSections.swift
Normal file
@@ -0,0 +1,994 @@
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
|
||||
/// iOS Settings-style icon: white glyph on a solid rounded-square, sized for a List row.
|
||||
struct SettingsIcon: View {
|
||||
let systemName: String
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: self.systemName)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(RoundedRectangle(cornerRadius: 7, style: .continuous).fill(self.color))
|
||||
}
|
||||
}
|
||||
|
||||
private struct AppearanceSettingsRow: View {
|
||||
@Environment(AppAppearanceModel.self) private var appearanceModel
|
||||
|
||||
private var preference: AppAppearancePreference {
|
||||
self.appearanceModel.preference
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationLink {
|
||||
AppearanceSettingsScreen()
|
||||
} label: {
|
||||
self.rowLabel
|
||||
}
|
||||
.accessibilityIdentifier("settings-appearance-row")
|
||||
.accessibilityLabel("Appearance")
|
||||
.accessibilityValue(self.preference.label)
|
||||
.accessibilityHint("Choose system, light, or dark appearance")
|
||||
}
|
||||
|
||||
private var rowLabel: some View {
|
||||
HStack(spacing: 12) {
|
||||
ProIconBadge(
|
||||
systemName: "circle.lefthalf.filled",
|
||||
color: .secondary)
|
||||
|
||||
Text("Appearance")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
|
||||
Spacer(minLength: 8)
|
||||
|
||||
Text(self.preference.label)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AppearanceSettingsScreen: View {
|
||||
@Environment(AppAppearanceModel.self) private var appearanceModel
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
ForEach(AppAppearancePreference.allCases) { preference in
|
||||
Button {
|
||||
self.select(preference)
|
||||
} label: {
|
||||
Label {
|
||||
HStack {
|
||||
Text(preference.label)
|
||||
.font(OpenClawType.body)
|
||||
Spacer()
|
||||
if preference == self.appearanceModel.preference {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: preference.systemImage)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(.primary)
|
||||
.accessibilityIdentifier("settings-appearance-\(preference.rawValue)")
|
||||
.accessibilityValue(
|
||||
preference == self.appearanceModel.preference ? "Selected" : "")
|
||||
}
|
||||
} footer: {
|
||||
Text("System follows this device’s appearance setting.")
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Appearance")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func select(_ preference: AppAppearancePreference) {
|
||||
guard preference != self.appearanceModel.preference else { return }
|
||||
self.dismiss()
|
||||
Task { @MainActor in
|
||||
// Changing the root scheme while an iPad split-view destination is active can
|
||||
// leave that destination blank. Apply only after the native pop transition.
|
||||
try? await Task.sleep(for: .milliseconds(500))
|
||||
self.appearanceModel.select(preference)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsProTab {
|
||||
var appearanceRow: some View {
|
||||
AppearanceSettingsRow()
|
||||
}
|
||||
|
||||
var gatewaySection: some View {
|
||||
Section("Gateway") {
|
||||
NavigationLink(value: SettingsRoute.gateway) {
|
||||
self.gatewayConnectionRow
|
||||
}
|
||||
LabeledContent("Address", value: self.gatewayAddress)
|
||||
LabeledContent("Server", value: self.gatewayServer)
|
||||
LabeledContent("Agents", value: "\(self.appModel.gatewayAgents.count)")
|
||||
self.gatewayActions
|
||||
}
|
||||
}
|
||||
|
||||
var gatewayConnectionRow: some View {
|
||||
LabeledContent {
|
||||
Text(self.gatewayStatusDetail)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(self.gatewayStatusColor)
|
||||
} label: {
|
||||
Text("Connection")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var settingsListSection: some View {
|
||||
Section {
|
||||
self.settingsListRow(
|
||||
icon: "checkmark.shield.fill",
|
||||
iconColor: self.pendingApproval == nil ? .green : .orange,
|
||||
title: "Approvals",
|
||||
route: .approvals,
|
||||
badgeValue: self.pendingApproval == nil ? nil : "1")
|
||||
self.settingsListRow(
|
||||
icon: "person.2.fill",
|
||||
iconColor: .blue,
|
||||
title: "Permissions",
|
||||
route: .permissions)
|
||||
self.settingsListRow(
|
||||
icon: "point.3.connected.trianglepath.dotted",
|
||||
iconColor: .purple,
|
||||
title: "Channels",
|
||||
route: .channels)
|
||||
self.settingsListRow(
|
||||
icon: "waveform",
|
||||
iconColor: .pink,
|
||||
title: "Voice & Talk",
|
||||
route: .voice)
|
||||
}
|
||||
|
||||
Section {
|
||||
self.appearanceRow
|
||||
self.settingsListRow(
|
||||
icon: "stethoscope",
|
||||
iconColor: .teal,
|
||||
title: "Diagnostics",
|
||||
route: .diagnostics)
|
||||
self.settingsListRow(
|
||||
icon: "hand.raised.fill",
|
||||
iconColor: .indigo,
|
||||
title: "Privacy",
|
||||
route: .privacy)
|
||||
self.settingsListRow(
|
||||
icon: "bell.fill",
|
||||
iconColor: .red,
|
||||
title: "Notifications",
|
||||
route: .notifications)
|
||||
self.settingsListRow(
|
||||
icon: "info.circle.fill",
|
||||
iconColor: .gray,
|
||||
title: "About",
|
||||
route: .about)
|
||||
} header: {
|
||||
Text("Device")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
self.settingsListRow(
|
||||
icon: "doc.text",
|
||||
iconColor: .gray,
|
||||
title: "Licenses",
|
||||
route: .licenses)
|
||||
.accessibilityIdentifier("settings-licenses-row")
|
||||
}
|
||||
}
|
||||
|
||||
func settingsListRow(
|
||||
icon: String,
|
||||
iconColor: Color,
|
||||
title: String,
|
||||
route: SettingsRoute,
|
||||
badgeValue: String? = nil) -> some View
|
||||
{
|
||||
NavigationLink(value: route) {
|
||||
Label {
|
||||
Text(title)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
} icon: {
|
||||
SettingsIcon(systemName: icon, color: iconColor)
|
||||
}
|
||||
}
|
||||
.badge(badgeValue.map { Text($0).font(OpenClawType.captionSemiBold) })
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func destination(for route: SettingsRoute) -> some View {
|
||||
switch route {
|
||||
case .channels:
|
||||
SettingsChannelsDestination()
|
||||
.navigationTitle(title(for: route))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
default:
|
||||
List {
|
||||
switch route {
|
||||
case .gateway:
|
||||
self.gatewayDestination
|
||||
case .approvals:
|
||||
self.approvalsDestination
|
||||
case .permissions:
|
||||
self.permissionsDestination
|
||||
case .voice:
|
||||
self.voiceDestination
|
||||
case .diagnostics:
|
||||
self.diagnosticsDestination
|
||||
case .privacy:
|
||||
self.privacyDestination
|
||||
case .notifications:
|
||||
self.notificationsDestination
|
||||
case .about:
|
||||
self.aboutDestination
|
||||
case .licenses:
|
||||
self.licensesDestination
|
||||
case .channels:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
.navigationTitle(title(for: route))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
if let headerLeadingAction {
|
||||
ToolbarItem(placement: .topBarLeading) {
|
||||
OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text(title(for: route))
|
||||
.font(OpenClawType.headline)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var gatewayDestination: some View {
|
||||
Group {
|
||||
self.detailStatusCard(
|
||||
icon: "antenna.radiowaves.left.and.right",
|
||||
title: "Gateway",
|
||||
detail: self.gatewayStatusDetail,
|
||||
value: self.gatewayStatusValue,
|
||||
color: self.gatewayStatusColor)
|
||||
|
||||
self.detailListCard {
|
||||
self.detailRow("Address", value: self.gatewayAddress)
|
||||
self.detailRow("Server", value: self.gatewayServer)
|
||||
self.detailRow("Discovered", value: "\(self.gatewayController.gateways.count)")
|
||||
self.detailRow("Default Agent", value: self.appModel.activeAgentName)
|
||||
self.detailRow("Agents", value: "\(self.appModel.gatewayAgents.count)")
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
Task { await self.reconnectGateway() }
|
||||
} label: {
|
||||
Label("Reconnect", systemImage: "arrow.triangle.2.circlepath")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.disabled(self.isReconnectingGateway || self.appModel.isAppleReviewDemoModeEnabled)
|
||||
Button {
|
||||
Task { await self.runDiagnostics() }
|
||||
} label: {
|
||||
Label("Diagnose", systemImage: "cross.case")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.disabled(self.isRefreshingGateway)
|
||||
}
|
||||
|
||||
self.manualGatewayCard
|
||||
self.deviceIdentityCard
|
||||
self.agentSelectionCard
|
||||
self.gatewaySetupCard
|
||||
self.discoveredGatewaysCard
|
||||
self.gatewayAdvancedCard
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
|
||||
var approvalsDestination: some View {
|
||||
Group {
|
||||
self.detailStatusCard(
|
||||
icon: "checkmark.shield.fill",
|
||||
title: "Approvals",
|
||||
detail: self.notificationsNeedAttention
|
||||
? "Out-of-app approval alerts need notification permission."
|
||||
: (self.pendingApproval == nil ? "No gateway actions are waiting for review." :
|
||||
"Review the pending gateway action."),
|
||||
value: self.notificationsNeedAttention
|
||||
? "Alerts Off"
|
||||
: (self.pendingApproval == nil ? "clear" : "1 waiting"),
|
||||
color: self.notificationsNeedAttention ? OpenClawBrand.warn :
|
||||
(self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn))
|
||||
|
||||
if self.notificationsNeedAttention {
|
||||
self.approvalNotificationsWarningCard
|
||||
}
|
||||
|
||||
self.approvalsReviewCard
|
||||
}
|
||||
}
|
||||
|
||||
var approvalNotificationsWarningCard: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Notifications are off")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text("Enable Notifications to receive approval alerts while OpenClaw is not open.")
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
if self.directRoute == nil {
|
||||
Button {
|
||||
self.openNotificationsRouteFromApprovals()
|
||||
} label: {
|
||||
Label("Open Notifications", systemImage: "bell.badge")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var approvalsReviewCard: some View {
|
||||
if let pendingApproval {
|
||||
Section {
|
||||
ForEach(self.approvalItems, id: \.id) { item in
|
||||
SettingsApprovalRow(item: item)
|
||||
}
|
||||
if let errorText = self.appModel.pendingExecApprovalPromptErrorText {
|
||||
Text(errorText)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(OpenClawBrand.danger)
|
||||
}
|
||||
Button {
|
||||
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-once") }
|
||||
} label: {
|
||||
Label("Allow", systemImage: "checkmark")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.disabled(self.appModel.pendingExecApprovalPromptResolving)
|
||||
if pendingApproval.allowsAllowAlways {
|
||||
Button {
|
||||
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "allow-always") }
|
||||
} label: {
|
||||
Label("Always Allow", systemImage: "checkmark.shield")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.disabled(self.appModel.pendingExecApprovalPromptResolving)
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
Task { await self.appModel.resolvePendingExecApprovalPrompt(decision: "deny") }
|
||||
} label: {
|
||||
Label("Deny", systemImage: "xmark")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.disabled(self.appModel.pendingExecApprovalPromptResolving)
|
||||
}
|
||||
} else {
|
||||
Section {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("No approvals waiting")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.approvalEmptyDetail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
.foregroundStyle(OpenClawBrand.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var permissionsDestination: some View {
|
||||
Group {
|
||||
self.toggleCard(
|
||||
title: "Camera",
|
||||
isOn: self.$cameraEnabled)
|
||||
|
||||
self.locationModeCard
|
||||
|
||||
self.toggleCard(
|
||||
title: "Keep Awake",
|
||||
isOn: self.$preventSleep)
|
||||
|
||||
self.privacyAccessCard
|
||||
}
|
||||
}
|
||||
|
||||
var voiceDestination: some View {
|
||||
Group {
|
||||
self.detailStatusCard(
|
||||
icon: "waveform",
|
||||
title: "Voice & Talk",
|
||||
detail: self.appModel.talkMode.gatewayTalkVoiceModeTitle,
|
||||
value: self.voiceDetail,
|
||||
color: self.talkEnabled || self.voiceWakeEnabled ? OpenClawBrand.accent : .secondary)
|
||||
|
||||
self.voiceFeatureCard
|
||||
self.talkVoiceSettingsCard
|
||||
self.shareSettingsCard
|
||||
}
|
||||
}
|
||||
|
||||
var diagnosticsDestination: some View {
|
||||
Group {
|
||||
self.detailStatusCard(
|
||||
icon: "checklist.checked",
|
||||
title: "Health Check",
|
||||
detail: "Run app, permission, and gateway-adjacent checks without editing setup.",
|
||||
value: self.diagnosticsHealthValue,
|
||||
color: self.gatewayDiagnosticConnected ? OpenClawBrand.ok : OpenClawBrand.warn)
|
||||
|
||||
Section {
|
||||
Button {
|
||||
Task { await self.runDiagnostics() }
|
||||
} label: {
|
||||
Label("Run Diagnostics", systemImage: "cross.case")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
.disabled(self.isRefreshingGateway)
|
||||
}
|
||||
|
||||
self.diagnosticChecksCard
|
||||
|
||||
self.detailListCard {
|
||||
self.detailRow("Device", value: DeviceInfoHelper.deviceFamily())
|
||||
self.detailRow("Platform", value: DeviceInfoHelper.platformStringForDisplay())
|
||||
self.detailRow("App", value: DeviceInfoHelper.openClawVersionString())
|
||||
self.detailRow("Model", value: DeviceInfoHelper.modelIdentifier())
|
||||
}
|
||||
|
||||
self.diagnosticsAdvancedCard
|
||||
}
|
||||
}
|
||||
|
||||
var privacyDestination: some View {
|
||||
Group {
|
||||
self.detailStatusCard(
|
||||
icon: "hand.raised",
|
||||
title: "Privacy",
|
||||
detail: "Control what device context OpenClaw can expose to the gateway.",
|
||||
value: self.privacyDetail,
|
||||
color: .secondary)
|
||||
|
||||
self.toggleCard(
|
||||
title: "Camera Access",
|
||||
isOn: self.$cameraEnabled)
|
||||
|
||||
self.locationModeCard
|
||||
|
||||
self.toggleCard(
|
||||
title: "Background Listening",
|
||||
isOn: self.$talkBackgroundEnabled)
|
||||
|
||||
self.privacyAccessCard
|
||||
}
|
||||
}
|
||||
|
||||
var notificationsDestination: some View {
|
||||
Group {
|
||||
self.detailStatusCard(
|
||||
icon: "bell",
|
||||
title: "Notifications",
|
||||
detail: self.notificationStatusDetail,
|
||||
value: self.notificationStatusText,
|
||||
color: self.notificationStatus.color)
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Button {
|
||||
self.handleNotificationAction()
|
||||
} label: {
|
||||
Label(
|
||||
self.notificationActionText,
|
||||
systemImage: self.notificationStatus.actionIcon)
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
.disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization)
|
||||
|
||||
Text(self.notificationStatusDetail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Divider()
|
||||
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "network")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.frame(width: 22, height: 22)
|
||||
Text(self.notificationRelayDetail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var gatewayActions: some View {
|
||||
Group {
|
||||
self.gatewayActionButton(
|
||||
title: "Reconnect",
|
||||
icon: "arrow.triangle.2.circlepath",
|
||||
color: OpenClawBrand.accent,
|
||||
isBusy: self.isReconnectingGateway,
|
||||
isDisabled: self.appModel.isAppleReviewDemoModeEnabled)
|
||||
{
|
||||
Task { await self.reconnectGateway() }
|
||||
}
|
||||
|
||||
self.gatewayActionButton(
|
||||
title: "Diagnose",
|
||||
icon: "cross.case",
|
||||
color: OpenClawBrand.accent,
|
||||
isBusy: self.isRefreshingGateway)
|
||||
{
|
||||
Task { await self.runDiagnostics() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder var licensesDestination: some View {
|
||||
let documents = LicenseDocumentLoader.bundledDocuments()
|
||||
if documents.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No Licenses Bundled",
|
||||
systemImage: "doc.text",
|
||||
description: Text("License files are not available in this build."))
|
||||
.font(OpenClawType.body)
|
||||
} else {
|
||||
Section {
|
||||
ForEach(documents) { document in
|
||||
NavigationLink {
|
||||
LicenseDocumentDetailView(document: document)
|
||||
} label: {
|
||||
Label {
|
||||
Text(document.title)
|
||||
.font(OpenClawType.subhead)
|
||||
} icon: {
|
||||
SettingsIcon(systemName: "doc.text", color: .gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
Text("OpenClaw appreciates its partners in the open-source community.")
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
.accessibilityIdentifier("settings-licenses-list")
|
||||
}
|
||||
}
|
||||
|
||||
/// Native inset-grouped action row (plain tinted text, no pill chrome).
|
||||
func gatewayActionButton(
|
||||
title: String,
|
||||
icon: String,
|
||||
color: Color,
|
||||
isBusy: Bool,
|
||||
isDisabled: Bool = false,
|
||||
action: @escaping () -> Void) -> some View
|
||||
{
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Label(title, systemImage: icon)
|
||||
.font(OpenClawType.body)
|
||||
Spacer()
|
||||
if isBusy {
|
||||
ProgressView().controlSize(.small)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(color)
|
||||
.disabled(isBusy || isDisabled)
|
||||
.accessibilityLabel(title)
|
||||
}
|
||||
|
||||
var aboutDestination: some View {
|
||||
// Concise public details only; deep hardware identifiers live in Diagnostics.
|
||||
detailListCard {
|
||||
self.detailRow("OpenClaw app version", value: DeviceInfoHelper.openClawVersionString())
|
||||
self.detailRow("Device", value: DeviceInfoHelper.deviceFamily())
|
||||
self.detailRow("iOS", value: DeviceInfoHelper.iOSVersionStringForDisplay())
|
||||
}
|
||||
}
|
||||
|
||||
func toggleCard(title: String, isOn: Binding<Bool>) -> some View {
|
||||
Section {
|
||||
Toggle(isOn: isOn) {
|
||||
Text(title)
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var locationModeCard: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(spacing: 12) {
|
||||
SettingsIcon(
|
||||
systemName: "location",
|
||||
color: self.locationModeRaw == OpenClawLocationMode.off.rawValue ? .secondary : OpenClawBrand
|
||||
.accent)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Location")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text("Controls whether location can be shared with gateway tools.")
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
if self.isChangingLocationMode {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Location", selection: self.$locationModeRaw) {
|
||||
Text("Off")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.tag(OpenClawLocationMode.off.rawValue)
|
||||
Text("While Using")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.tag(OpenClawLocationMode.whileUsing.rawValue)
|
||||
Text("Always")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.tag(OpenClawLocationMode.always.rawValue)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.disabled(self.isChangingLocationMode)
|
||||
|
||||
Text(self.locationPermissionDetailText)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(
|
||||
self.locationPermissionSummary.needsAttention ? OpenClawBrand.warn : .secondary)
|
||||
|
||||
if let locationPermissionWarningText {
|
||||
Text(locationPermissionWarningText)
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var agentSelectionCard: some View {
|
||||
Section {
|
||||
Picker("Default Agent", selection: self.$selectedAgentPickerId) {
|
||||
Text("Default").font(OpenClawType.body).tag("")
|
||||
let defaultId = (self.appModel.gatewayDefaultAgentId ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
ForEach(self.appModel.gatewayAgents.filter { $0.id != defaultId }, id: \.id) { agent in
|
||||
let name = (agent.name ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
Text(name.isEmpty ? agent.id : name).font(OpenClawType.body).tag(agent.id)
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
} footer: {
|
||||
Text("Used for new Chat and Talk sessions.")
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
var gatewaySetupCard: some View {
|
||||
Section {
|
||||
TextField("Paste setup code", text: self.$setupCode)
|
||||
.font(OpenClawType.body)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
self.gatewayActionButton(
|
||||
title: "Scan QR",
|
||||
icon: "qrcode.viewfinder",
|
||||
color: OpenClawBrand.accent,
|
||||
isBusy: false,
|
||||
isDisabled: self.connectingGatewayID != nil)
|
||||
{
|
||||
self.openGatewayQRScanner()
|
||||
}
|
||||
|
||||
self.gatewayActionButton(
|
||||
title: "Connect",
|
||||
icon: "bolt.horizontal.circle",
|
||||
color: OpenClawBrand.accent,
|
||||
isBusy: self.connectingGatewayID == "manual",
|
||||
isDisabled: !self.canApplyGatewaySetup || self.connectingGatewayID != nil)
|
||||
{
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
}
|
||||
} header: {
|
||||
Text("Setup Code")
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
} footer: {
|
||||
if let warning = self.tailnetWarningText {
|
||||
Text(warning).font(OpenClawType.footnote).foregroundStyle(OpenClawBrand.warn)
|
||||
} else if let status = self.setupStatusLine {
|
||||
Text(status)
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var discoveredGatewaysCard: some View {
|
||||
Section("Discovered Gateways") {
|
||||
if self.gatewayController.gateways.isEmpty {
|
||||
Text("No gateways found yet. Use manual setup if Bonjour is blocked.")
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(self.gatewayController.gateways) { gateway in
|
||||
self.discoveredGatewayRow(gateway)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func discoveredGatewayRow(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> some View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(verbatim: gateway.name)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(verbatim: self.gatewayDetailLines(gateway).joined(separator: " • "))
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Button {
|
||||
Task { await self.connect(gateway) }
|
||||
} label: {
|
||||
if self.connectingGatewayID == gateway.id {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Text("Connect")
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(self.connectingGatewayID != nil)
|
||||
}
|
||||
}
|
||||
|
||||
var manualGatewayCard: some View {
|
||||
Section("Manual Gateway") {
|
||||
Toggle(isOn: self.$manualGatewayEnabled) {
|
||||
Text("Use Manual Gateway")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
TextField("Host", text: self.$manualGatewayHost)
|
||||
.font(OpenClawType.body)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
TextField("Port", text: self.manualPortBinding)
|
||||
.font(OpenClawType.body)
|
||||
.keyboardType(.numberPad)
|
||||
Toggle(isOn: self.$manualGatewayTLS) {
|
||||
Text("Use TLS")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
self.gatewayActionButton(
|
||||
title: "Connect Manual",
|
||||
icon: "network",
|
||||
color: OpenClawBrand.accent,
|
||||
isBusy: self.connectingGatewayID == "manual",
|
||||
isDisabled: self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|| !self.manualPortIsValid)
|
||||
{
|
||||
Task { await self.connectManual() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var gatewayAdvancedCard: some View {
|
||||
Section {
|
||||
Toggle(isOn: self.$gatewayAutoConnect) {
|
||||
Text("Auto-connect on launch")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
self.gatewaySecureField("Gateway Auth Token", text: self.$gatewayToken)
|
||||
self.gatewaySecureField("Gateway Password", text: self.$gatewayPassword)
|
||||
Button(role: .destructive) {
|
||||
self.showResetOnboardingAlert = true
|
||||
} label: {
|
||||
Label("Reset Onboarding", systemImage: "arrow.counterclockwise")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func gatewaySecureField(_ placeholder: String, text: Binding<String>) -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
SecureField("", text: text)
|
||||
.font(OpenClawType.subhead)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.accessibilityLabel(placeholder)
|
||||
if text.wrappedValue.isEmpty {
|
||||
Text(placeholder)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.foregroundStyle(.tertiary)
|
||||
.padding(.horizontal, 8)
|
||||
.allowsHitTesting(false)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
|
||||
var voiceFeatureCard: some View {
|
||||
Section {
|
||||
self.settingsToggle("Voice Wake", isOn: self.$voiceWakeEnabled) { enabled in
|
||||
self.appModel.setVoiceWakeEnabled(enabled)
|
||||
}
|
||||
self.settingsToggle("Talk Mode", isOn: self.$talkEnabled) { enabled in
|
||||
guard !self.appModel.isAppleReviewDemoModeEnabled else {
|
||||
self.talkEnabled = false
|
||||
return
|
||||
}
|
||||
self.appModel.setTalkEnabled(enabled)
|
||||
}
|
||||
.disabled(self.appModel.isAppleReviewDemoModeEnabled)
|
||||
Picker("Speech Language", selection: self.$talkSpeechLocale) {
|
||||
ForEach(TalkSpeechLocale.supportedOptions()) { option in
|
||||
Text(option.label).font(OpenClawType.body).tag(option.id)
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
self.settingsToggle("Background Listening", isOn: self.$talkBackgroundEnabled)
|
||||
self.settingsToggle("Speakerphone", isOn: self.talkSpeakerphoneBinding)
|
||||
NavigationLink {
|
||||
VoiceWakeWordsSettingsView()
|
||||
} label: {
|
||||
self.simpleSettingsRow(
|
||||
title: "Wake Words",
|
||||
value: VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var talkVoiceSettingsCard: some View {
|
||||
Group {
|
||||
if self.gatewayConnected,
|
||||
let issue = self.appModel.talkMode.gatewayTalkCurrentFallbackIssue
|
||||
{
|
||||
Section {
|
||||
TalkRuntimeIssueBanner(
|
||||
issue: issue,
|
||||
onOpenSettings: nil,
|
||||
onShowDetails: {
|
||||
self.showTalkIssueDetails = true
|
||||
})
|
||||
}
|
||||
}
|
||||
Section("Voice") {
|
||||
Picker("Provider", selection: self.talkProviderSelectionBinding) {
|
||||
ForEach(TalkModeProviderSelection.allCases) { option in
|
||||
Text(option.label).font(OpenClawType.body).tag(option.rawValue)
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
if self.shouldShowRealtimeVoicePicker {
|
||||
Picker("Realtime Voice", selection: self.talkRealtimeVoiceSelectionBinding) {
|
||||
Text("Gateway Default").font(OpenClawType.body).tag("")
|
||||
ForEach(TalkModeRealtimeVoiceSelection.voices, id: \.self) { voice in
|
||||
Text(TalkModeRealtimeVoiceSelection.label(for: voice)).font(OpenClawType.body).tag(voice)
|
||||
}
|
||||
}
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
self.detailRow("Voice Mode", value: self.appModel.talkMode.gatewayTalkVoiceModeTitle)
|
||||
self.detailRow("Active Voice", value: self.gatewayTalkActiveVoiceDetail)
|
||||
if let issue = self.gatewayTalkLastIssueDetail {
|
||||
self.detailRow("Last Voice Issue", value: issue)
|
||||
}
|
||||
self.detailRow("Transport", value: self.appModel.talkMode.gatewayTalkTransportLabel)
|
||||
self.detailRow("API Key", value: self.talkApiKeyStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var shareSettingsCard: some View {
|
||||
Section {
|
||||
Toggle(isOn: self.$talkButtonEnabled) {
|
||||
Text("Show Talk Control")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
TextField("Default Share Instruction", text: self.$defaultShareInstruction, axis: .vertical)
|
||||
.font(OpenClawType.body)
|
||||
.lineLimit(2...5)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
Button {
|
||||
Task { await self.appModel.runSharePipelineSelfTest() }
|
||||
} label: {
|
||||
Label("Run Share Self-Test", systemImage: "checkmark.seal")
|
||||
.font(OpenClawType.body)
|
||||
}
|
||||
} footer: {
|
||||
Text(self.appModel.lastShareEventText)
|
||||
.font(OpenClawType.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
var privacyAccessCard: some View {
|
||||
Section {
|
||||
PrivacyAccessSectionView()
|
||||
}
|
||||
}
|
||||
|
||||
var diagnosticsAdvancedCard: some View {
|
||||
Section {
|
||||
self.settingsToggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled) { enabled in
|
||||
self.gatewayController.setDiscoveryDebugLoggingEnabled(enabled)
|
||||
}
|
||||
self.settingsToggle("Debug Screen Status", isOn: self.$canvasDebugStatusEnabled)
|
||||
NavigationLink {
|
||||
GatewayDiscoveryDebugLogView()
|
||||
} label: {
|
||||
self.simpleSettingsRow(title: "Discovery Logs", value: self.gatewayController.discoveryStatusText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var deviceIdentityCard: some View {
|
||||
Section("Device") {
|
||||
TextField("Device Name", text: self.$displayName)
|
||||
.font(OpenClawType.body)
|
||||
self.detailRow("Instance ID", value: self.instanceId)
|
||||
}
|
||||
}
|
||||
|
||||
func settingsToggle(
|
||||
_ title: String,
|
||||
isOn: Binding<Bool>,
|
||||
onChange: ((Bool) -> Void)? = nil) -> some View
|
||||
{
|
||||
Toggle(isOn: isOn) {
|
||||
Text(title)
|
||||
.font(OpenClawType.subhead)
|
||||
}
|
||||
.onChange(of: isOn.wrappedValue) { _, enabled in
|
||||
onChange?(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func simpleSettingsRow(title: String, value: String) -> some View {
|
||||
LabeledContent(title) {
|
||||
Text(value)
|
||||
.font(OpenClawType.subhead)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user