mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 13:51:37 +00:00
* feat(ios): guide first launch through pairing explainer and permissions * fix(ios): keep first-run flow resumable and surface limited grants in onboarding * chore(ios): regenerate native i18n after rebase * chore(android): project refreshed native translations into strings.xml * fix(ios): move i18n wrapper contract to shared permission row and sync catalog
143 lines
5.5 KiB
Swift
143 lines
5.5 KiB
Swift
import SwiftUI
|
|
|
|
/// Capsule action used by permission rows: filled for the initial "Allow",
|
|
/// bordered for repair actions like "Open Settings" or "Upgrade".
|
|
struct DevicePermissionActionButtonStyle: ButtonStyle {
|
|
let prominent: Bool
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.font(OpenClawType.footnoteSemiBold)
|
|
.padding(.horizontal, 14)
|
|
.frame(height: 32)
|
|
.foregroundStyle(
|
|
self.prominent
|
|
? OpenClawBrand.activationPrimaryActionText
|
|
: OpenClawBrand.activationPrimaryAction)
|
|
.background {
|
|
if self.prominent {
|
|
Capsule(style: .continuous)
|
|
.fill(OpenClawBrand.activationPrimaryGradient)
|
|
} else {
|
|
Capsule(style: .continuous)
|
|
.fill(OpenClawBrand.activationNeutralSurface)
|
|
}
|
|
}
|
|
.overlay {
|
|
Capsule(style: .continuous)
|
|
.stroke(
|
|
self.prominent
|
|
? Color.white.opacity(0.26)
|
|
: OpenClawBrand.activationNeutralStroke,
|
|
lineWidth: 0.5)
|
|
}
|
|
.opacity(configuration.isPressed ? 0.86 : 1)
|
|
.scaleEffect(configuration.isPressed ? 0.97 : 1)
|
|
.animation(.smooth(duration: 0.12), value: configuration.isPressed)
|
|
}
|
|
}
|
|
|
|
/// One device permission with an icon tile, explanation, and a single clear
|
|
/// affordance: Allow when unset, a green check when granted, a repair action otherwise.
|
|
struct DevicePermissionRow: View {
|
|
let identifierPrefix: String
|
|
let identifier: String
|
|
let symbol: String
|
|
let tint: Color
|
|
let title: LocalizedStringResource
|
|
let detail: LocalizedStringResource
|
|
let grant: DevicePermissionGrant
|
|
var isRequesting: Bool = false
|
|
var statusLabel: LocalizedStringResource?
|
|
var actionTitle: LocalizedStringResource?
|
|
var action: (() -> Void)?
|
|
|
|
var body: some View {
|
|
HStack(alignment: .center, spacing: 12) {
|
|
self.iconTile
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(self.title)
|
|
.font(OpenClawType.subheadSemiBold)
|
|
Text(self.detail)
|
|
.font(OpenClawType.footnote)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
|
|
Spacer(minLength: 8)
|
|
|
|
self.trailingControl
|
|
}
|
|
.padding(.vertical, 6)
|
|
}
|
|
|
|
private var isGrantedTile: Bool {
|
|
self.grant == .granted || self.grant == .limited
|
|
}
|
|
|
|
private var iconTile: some View {
|
|
Image(systemName: self.symbol)
|
|
.font(OpenClawType.subheadSemiBold)
|
|
.foregroundStyle(self.isGrantedTile ? Color.white : self.tint)
|
|
.frame(width: 36, height: 36)
|
|
.background {
|
|
RoundedRectangle(cornerRadius: 11, style: .continuous)
|
|
.fill(self.isGrantedTile ? AnyShapeStyle(self.tint) : AnyShapeStyle(self.tint.opacity(0.14)))
|
|
}
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var trailingControl: some View {
|
|
if self.isRequesting {
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
.controlSize(.small)
|
|
.frame(height: 32)
|
|
} else {
|
|
VStack(alignment: .trailing, spacing: 4) {
|
|
if let actionTitle = self.actionTitle, let action = self.action {
|
|
Button(action: action) {
|
|
Text(actionTitle)
|
|
.font(OpenClawType.footnoteSemiBold)
|
|
}
|
|
.buttonStyle(DevicePermissionActionButtonStyle(prominent: self.grant == .notRequested))
|
|
.accessibilityIdentifier("\(self.identifierPrefix)-\(self.identifier)-action")
|
|
} else if self.grant == .granted || self.grant == .limited {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(OpenClawType.title3SemiBold)
|
|
.foregroundStyle(OpenClawBrand.ok)
|
|
.accessibilityLabel(Text(self.statusLabel ?? LocalizedStringResource("Allowed")))
|
|
.accessibilityIdentifier("\(self.identifierPrefix)-\(self.identifier)-status")
|
|
}
|
|
|
|
if let statusCaption {
|
|
Text(statusCaption)
|
|
.font(OpenClawType.caption2Medium)
|
|
.foregroundStyle(self.statusCaptionColor)
|
|
.accessibilityIdentifier("\(self.identifierPrefix)-\(self.identifier)-status")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shown under the action for partial or blocked states so the row explains
|
|
/// why a repair action (Upgrade / Open Settings) is offered.
|
|
private var statusCaption: LocalizedStringResource? {
|
|
guard self.actionTitle != nil else { return nil }
|
|
switch self.grant {
|
|
case .limited:
|
|
return self.statusLabel ?? LocalizedStringResource("Limited")
|
|
case .denied:
|
|
return self.statusLabel ?? LocalizedStringResource("Off in Settings")
|
|
case .granted, .notRequested:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private var statusCaptionColor: Color {
|
|
self.grant == .denied ? OpenClawBrand.danger : OpenClawBrand.warn
|
|
}
|
|
}
|