import SwiftUI
|
|
enum ProStatusPillStyle {
|
case locked
|
case preview
|
case unlocked
|
case legacy
|
case neutral
|
}
|
|
struct ProStatusPill: View {
|
let text: String
|
let style: ProStatusPillStyle
|
var compact = false
|
|
var body: some View {
|
HStack(spacing: compact ? 4 : 5) {
|
if showsCrown {
|
Image(systemName: "crown.fill")
|
.font(.system(size: compact ? 9 : 10, weight: .bold))
|
}
|
|
Text(text)
|
}
|
.font(.system(size: compact ? 10 : 11, weight: .semibold))
|
.foregroundStyle(foregroundColor)
|
.lineLimit(1)
|
.padding(.horizontal, compact ? 8 : 10)
|
.padding(.vertical, compact ? 4 : 5)
|
.background(
|
Capsule(style: .continuous)
|
.fill(backgroundColor)
|
)
|
.overlay(
|
Capsule(style: .continuous)
|
.stroke(strokeColor, lineWidth: 1)
|
)
|
}
|
|
private var foregroundColor: Color {
|
switch style {
|
case .locked:
|
return Color(red: 0.56, green: 0.35, blue: 0.03)
|
case .preview:
|
return Color(red: 0.30, green: 0.22, blue: 0.08)
|
case .unlocked, .legacy:
|
return Color(red: 0.56, green: 0.35, blue: 0.03)
|
case .neutral:
|
return .secondary
|
}
|
}
|
|
private var backgroundColor: Color {
|
switch style {
|
case .locked:
|
return Color(red: 1.0, green: 0.78, blue: 0.22).opacity(0.18)
|
case .preview:
|
return Color.orange.opacity(0.14)
|
case .unlocked, .legacy:
|
return Color(red: 1.0, green: 0.78, blue: 0.22).opacity(0.18)
|
case .neutral:
|
return Color.secondary.opacity(0.10)
|
}
|
}
|
|
private var strokeColor: Color {
|
switch style {
|
case .locked:
|
return Color(red: 0.93, green: 0.62, blue: 0.10).opacity(0.42)
|
case .preview:
|
return Color.orange.opacity(0.28)
|
case .unlocked, .legacy:
|
return Color(red: 0.93, green: 0.62, blue: 0.10).opacity(0.42)
|
case .neutral:
|
return Color.secondary.opacity(0.18)
|
}
|
}
|
|
private var showsCrown: Bool {
|
switch style {
|
case .locked, .unlocked, .legacy:
|
return text.localizedCaseInsensitiveContains("Pro")
|
case .preview, .neutral:
|
return false
|
}
|
}
|
}
|
|
extension ProFeature {
|
var promptMessageKey: String {
|
switch self {
|
case .premiumThemes:
|
return "pro.prompt.themes"
|
case .layoutImport:
|
return "pro.prompt.import"
|
case .layoutExport:
|
return "pro.prompt.export"
|
case .unlimitedNotes:
|
return "pro.prompt.notes"
|
case .persistentAppSorting:
|
return "pro.prompt.sorting"
|
}
|
}
|
}
|
|
extension ProOperationState {
|
var statusMessageKey: String? {
|
switch self {
|
case .idle:
|
return nil
|
case .loading:
|
return "pro.operation.loading"
|
case .purchasing:
|
return "pro.operation.purchasing"
|
case .restoring:
|
return "pro.operation.restoring"
|
case .pending:
|
return "pro.operation.pending"
|
case .cancelled:
|
return "pro.operation.cancelled"
|
case .restoreNotFound:
|
return "pro.operation.restoreNotFound"
|
case .failed(let messageKey):
|
return messageKey
|
}
|
}
|
}
|
|
struct ProUpgradePromptView: View {
|
let title: String
|
let message: String
|
let benefitText: String
|
let operationText: String?
|
let unlockTitle: String
|
let restoreTitle: String
|
let isBusy: Bool
|
let onClose: () -> Void
|
let onUnlock: () -> Void
|
let onRestore: () -> Void
|
|
var body: some View {
|
VStack(alignment: .leading, spacing: 0) {
|
HStack(alignment: .top, spacing: 12) {
|
VStack(alignment: .leading, spacing: 10) {
|
HStack(spacing: 8) {
|
Text(title)
|
.font(.system(size: 26, weight: .bold))
|
.foregroundStyle(.primary)
|
ProStatusPill(text: tr("pro.card.badge"), style: .locked)
|
}
|
|
Text(message)
|
.font(.system(size: 14, weight: .medium))
|
.foregroundStyle(Color.primary.opacity(0.78))
|
.lineSpacing(2)
|
.fixedSize(horizontal: false, vertical: true)
|
}
|
|
Spacer(minLength: 16)
|
|
Button(action: onClose) {
|
Image(systemName: "xmark")
|
.font(.system(size: 12, weight: .bold))
|
.foregroundStyle(.secondary)
|
.frame(width: 28, height: 28)
|
.background(
|
Circle()
|
.fill(Color.primary.opacity(0.08))
|
)
|
}
|
.buttonStyle(.plain)
|
.help(tr("settings.cancel"))
|
}
|
|
HStack(alignment: .top, spacing: 10) {
|
Image(systemName: "sparkles")
|
.font(.system(size: 14, weight: .semibold))
|
.foregroundStyle(Color.accentColor)
|
.frame(width: 18, height: 18)
|
Text(benefitText)
|
.font(.system(size: 13, weight: .medium))
|
.foregroundStyle(Color.primary.opacity(0.76))
|
.lineSpacing(2)
|
.fixedSize(horizontal: false, vertical: true)
|
}
|
.padding(.top, 18)
|
|
if let operationText {
|
Text(operationText)
|
.font(.system(size: 12, weight: .semibold))
|
.foregroundStyle(Color.secondary)
|
.padding(.top, 14)
|
}
|
|
HStack(spacing: 10) {
|
Button(tr("settings.cancel"), action: onClose)
|
.buttonStyle(.bordered)
|
|
Spacer(minLength: 0)
|
|
Button(restoreTitle, action: onRestore)
|
.buttonStyle(.bordered)
|
.disabled(isBusy)
|
|
Button(unlockTitle, action: onUnlock)
|
.buttonStyle(.borderedProminent)
|
.disabled(isBusy)
|
}
|
.padding(.top, 20)
|
}
|
.padding(22)
|
.frame(width: 460)
|
.background(
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
.fill(Color(nsColor: .windowBackgroundColor))
|
)
|
.overlay(
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
.stroke(Color.primary.opacity(0.14), lineWidth: 1)
|
)
|
.shadow(color: .black.opacity(0.24), radius: 24, x: 0, y: 12)
|
}
|
}
|