| | |
| | | customizeTitle: proEntitlement.isUnlocked ? tr("hotkeys.customize") : tr("pro.card.badge"), |
| | | restoreTitle: tr("hotkeys.restoreDefault"), |
| | | isRecording: recordingHotkeyKind == .main, |
| | | isProLocked: !proEntitlement.isUnlocked, |
| | | canRestore: proEntitlement.isUnlocked && LauncherHotkeySettings.hasCustomHotkey(for: .main), |
| | | onCustomize: { startHotkeyRecording(for: .main) }, |
| | | onRestore: { restoreDefaultHotkey(for: .main) } |
| | |
| | | customizeTitle: proEntitlement.isUnlocked ? tr("hotkeys.customize") : tr("pro.card.badge"), |
| | | restoreTitle: tr("hotkeys.restoreDefault"), |
| | | isRecording: recordingHotkeyKind == .quickSearch, |
| | | isProLocked: !proEntitlement.isUnlocked, |
| | | canRestore: proEntitlement.isUnlocked && LauncherHotkeySettings.hasCustomHotkey(for: .quickSearch), |
| | | onCustomize: { startHotkeyRecording(for: .quickSearch) }, |
| | | onRestore: { restoreDefaultHotkey(for: .quickSearch) } |
| | |
| | | let statusTone: HotkeyStatusTone |
| | | |
| | | var body: some View { |
| | | VStack(alignment: .leading, spacing: 12) { |
| | | HStack(alignment: .top, spacing: 12) { |
| | | HStack(alignment: .center, spacing: 18) { |
| | | VStack(alignment: .leading, spacing: 3) { |
| | | Text(title) |
| | | .font(.system(size: 13, weight: .semibold)) |
| | |
| | | } |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | |
| | | VStack(alignment: .leading, spacing: 4) { |
| | | HStack(alignment: .center, spacing: 12) { |
| | | Text(displayText) |
| | | .font(.system(size: 15, weight: .semibold)) |
| | | .lineLimit(1) |
| | | .minimumScaleFactor(0.85) |
| | | Text(statusText) |
| | | .font(.system(size: 11, weight: .semibold)) |
| | | .foregroundStyle(statusTone.foreground) |
| | | .padding(.horizontal, 8) |
| | | .padding(.vertical, 3) |
| | | .background(Capsule().fill(statusTone.background)) |
| | | .frame(width: 156, alignment: .leading) |
| | | |
| | | HotkeyStatusPill(text: statusText, tone: statusTone) |
| | | } |
| | | .frame(width: 180, alignment: .leading) |
| | | } |
| | | .frame(width: 310, alignment: .leading) |
| | | } |
| | | .padding(12) |
| | | .background( |
| | |
| | | let customizeTitle: String |
| | | let restoreTitle: String |
| | | let isRecording: Bool |
| | | let isProLocked: Bool |
| | | let canRestore: Bool |
| | | let onCustomize: () -> Void |
| | | let onRestore: () -> Void |
| | | |
| | | var body: some View { |
| | | VStack(alignment: .leading, spacing: 12) { |
| | | HStack(alignment: .top, spacing: 12) { |
| | | HStack(alignment: .center, spacing: 18) { |
| | | VStack(alignment: .leading, spacing: 3) { |
| | | Text(title) |
| | | .font(.system(size: 13, weight: .semibold)) |
| | |
| | | } |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | |
| | | VStack(alignment: .leading, spacing: 7) { |
| | | HStack(alignment: .center, spacing: 12) { |
| | | VStack(alignment: .leading, spacing: 6) { |
| | | Text(isRecording ? "…" : displayText) |
| | | .font(.system(size: 15, weight: .semibold)) |
| | | .lineLimit(1) |
| | | .minimumScaleFactor(0.85) |
| | | |
| | | HStack(spacing: 6) { |
| | | Text(statusText) |
| | | .font(.system(size: 11, weight: .semibold)) |
| | | .foregroundStyle(statusTone.foreground) |
| | | .padding(.horizontal, 8) |
| | | .padding(.vertical, 3) |
| | | .background(Capsule().fill(statusTone.background)) |
| | | HotkeyStatusPill(text: statusText, tone: statusTone) |
| | | |
| | | if isRecording { |
| | | Text(tr("hotkeys.recordingInline")) |
| | |
| | | .foregroundStyle(.blue) |
| | | } |
| | | } |
| | | } |
| | | .frame(width: 156, alignment: .leading) |
| | | |
| | | HStack(spacing: 8) { |
| | | Button(customizeTitle, action: onCustomize) |
| | | .buttonStyle(.borderedProminent) |
| | | .controlSize(.small) |
| | | HotkeyPrimaryActionButton( |
| | | title: customizeTitle, |
| | | isProLocked: isProLocked, |
| | | action: onCustomize |
| | | ) |
| | | |
| | | Button(restoreTitle, action: onRestore) |
| | | .buttonStyle(.bordered) |
| | | .controlSize(.small) |
| | | .disabled(!canRestore) |
| | | if canRestore { |
| | | HotkeySecondaryActionButton( |
| | | title: restoreTitle, |
| | | action: onRestore |
| | | ) |
| | | } |
| | | } |
| | | .frame(width: 230, alignment: .leading) |
| | | } |
| | | .frame(width: 310, alignment: .leading) |
| | | } |
| | | .padding(12) |
| | | .background( |
| | |
| | | } |
| | | } |
| | | |
| | | private struct HotkeyStatusPill: View { |
| | | let text: String |
| | | let tone: HotkeyStatusTone |
| | | |
| | | var body: some View { |
| | | Text(text) |
| | | .font(.system(size: 11, weight: .semibold)) |
| | | .foregroundStyle(tone.foreground) |
| | | .lineLimit(1) |
| | | .padding(.horizontal, 8) |
| | | .padding(.vertical, 3) |
| | | .background(Capsule().fill(tone.background)) |
| | | } |
| | | } |
| | | |
| | | private struct HotkeyPrimaryActionButton: View { |
| | | let title: String |
| | | let isProLocked: Bool |
| | | let action: () -> Void |
| | | |
| | | var body: some View { |
| | | Button(action: action) { |
| | | if isProLocked { |
| | | ProStatusPill(text: title, style: .locked, compact: true) |
| | | } else { |
| | | Text(title) |
| | | .font(.system(size: 11, weight: .semibold)) |
| | | .foregroundStyle(Color.white) |
| | | .lineLimit(1) |
| | | .padding(.horizontal, 12) |
| | | .padding(.vertical, 6) |
| | | .background( |
| | | Capsule(style: .continuous) |
| | | .fill(Color.accentColor) |
| | | ) |
| | | } |
| | | } |
| | | .buttonStyle(.plain) |
| | | .contentShape(Capsule(style: .continuous)) |
| | | } |
| | | } |
| | | |
| | | private struct HotkeySecondaryActionButton: View { |
| | | let title: String |
| | | let action: () -> Void |
| | | |
| | | var body: some View { |
| | | Button(action: action) { |
| | | Text(title) |
| | | .font(.system(size: 11, weight: .semibold)) |
| | | .foregroundStyle(.secondary) |
| | | .lineLimit(1) |
| | | .padding(.horizontal, 12) |
| | | .padding(.vertical, 6) |
| | | .background( |
| | | Capsule(style: .continuous) |
| | | .fill(Color.secondary.opacity(0.12)) |
| | | ) |
| | | } |
| | | .buttonStyle(.plain) |
| | | .contentShape(Capsule(style: .continuous)) |
| | | } |
| | | } |
| | | |
| | | private struct SettingsConfirmationView: View { |
| | | let title: String |
| | | let message: String |