| | |
| | | @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles |
| | | @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips |
| | | @AppStorage(AppGridTheme.storageKey) private var appGridThemeID = AppDefaults.appGridThemeID |
| | | @AppStorage(AppGridTheme.deepBlueTuningStorageKey) private var deepBlueThemeTuning = AppGridTheme.deepBlue.defaultColorTuning |
| | | @AppStorage(AppGridTheme.colorfulTuningStorageKey) private var colorfulThemeTuning = AppGridTheme.colorful.defaultColorTuning |
| | | @AppStorage("mainHotkeyRegistrationState") private var mainHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue |
| | | @AppStorage("quickSearchHotkeyRegistrationState") private var quickSearchHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue |
| | | @ObservedObject private var proEntitlement = ProEntitlementCenter.shared |
| | |
| | | appGridThemeID = theme.rawValue |
| | | return |
| | | } |
| | | proEntitlement.startThemePreview(for: theme) |
| | | proEntitlement.startThemePreview(for: theme, tuning: currentThemeTuning(for: theme)) |
| | | } |
| | | |
| | | private var activeColorTuningTheme: AppGridTheme? { |
| | | let theme = effectiveAppGridTheme |
| | | return theme.supportsColorTuning ? theme : nil |
| | | } |
| | | |
| | | private func currentThemeTuning(for theme: AppGridTheme) -> Double { |
| | | if let preview = proEntitlement.themePreviewState, |
| | | preview.theme == theme { |
| | | return theme.normalizedTuning(preview.themeTuning) |
| | | } |
| | | switch theme { |
| | | case .deepBlue: |
| | | return theme.normalizedTuning(deepBlueThemeTuning) |
| | | case .colorful: |
| | | return theme.normalizedTuning(colorfulThemeTuning) |
| | | case .defaultLight, .black, .pink, .purple, .green, .blue: |
| | | return theme.defaultColorTuning |
| | | } |
| | | } |
| | | |
| | | private func persistThemeTuning(_ tuning: Double?, for theme: AppGridTheme) { |
| | | let normalized = theme.normalizedTuning(tuning) |
| | | switch theme { |
| | | case .deepBlue: |
| | | deepBlueThemeTuning = normalized |
| | | case .colorful: |
| | | colorfulThemeTuning = normalized |
| | | case .defaultLight, .black, .pink, .purple, .green, .blue: |
| | | break |
| | | } |
| | | } |
| | | |
| | | private func updateThemeTuning(_ value: Double, for theme: AppGridTheme) { |
| | | let normalized = theme.normalizedTuning(value) |
| | | if proEntitlement.isUnlocked || theme.isFreeTheme { |
| | | persistThemeTuning(normalized, for: theme) |
| | | return |
| | | } |
| | | if proEntitlement.themePreviewState?.theme == theme { |
| | | proEntitlement.updateThemePreviewTuning(normalized, for: theme) |
| | | } else { |
| | | proEntitlement.startThemePreview(for: theme, tuning: normalized) |
| | | } |
| | | } |
| | | |
| | | private func themeTuningBinding(for theme: AppGridTheme) -> Binding<Double> { |
| | | Binding( |
| | | get: { currentThemeTuning(for: theme) }, |
| | | set: { updateThemeTuning($0, for: theme) } |
| | | ) |
| | | } |
| | | |
| | | private func themeOptionAccessory(for theme: AppGridTheme) -> ThemeOptionAccessory { |
| | |
| | | guard newState.isUnlocked else { return } |
| | | if let preview = proEntitlement.themePreviewState { |
| | | appGridThemeID = preview.theme.rawValue |
| | | persistThemeTuning(preview.themeTuning, for: preview.theme) |
| | | proEntitlement.stopThemePreview() |
| | | } |
| | | if settingsProPromptFeature != nil { |
| | | dismissSettingsProPrompt() |
| | | } |
| | | } |
| | | |
| | | @ViewBuilder |
| | | private var themeColorTuningSection: some View { |
| | | if let theme = activeColorTuningTheme { |
| | | VStack(alignment: .leading, spacing: 8) { |
| | | HStack(alignment: .firstTextBaseline, spacing: 10) { |
| | | Label( |
| | | tr(theme == .deepBlue ? "theme.tuning.dark" : "theme.tuning.light"), |
| | | systemImage: "slider.horizontal.3" |
| | | ) |
| | | .font(.system(size: 13, weight: .semibold)) |
| | | .foregroundStyle(.primary) |
| | | |
| | | Spacer(minLength: 12) |
| | | |
| | | Text("\(Int(round(currentThemeTuning(for: theme) * 100)))") |
| | | .font(.system(size: 12, weight: .semibold, design: .monospaced)) |
| | | .foregroundStyle(.secondary) |
| | | } |
| | | |
| | | Slider(value: themeTuningBinding(for: theme), in: 0...1) |
| | | .controlSize(.regular) |
| | | .accessibilityLabel(tr(theme == .deepBlue ? "theme.tuning.dark" : "theme.tuning.light")) |
| | | |
| | | Text(tr("theme.tuning.desc")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | } |
| | | .padding(14) |
| | | .frame(maxWidth: .infinity, alignment: .leading) |
| | | .background( |
| | | RoundedRectangle(cornerRadius: 10, style: .continuous) |
| | | .fill(Color(nsColor: .controlBackgroundColor).opacity(0.82)) |
| | | ) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 10, style: .continuous) |
| | | .stroke(Color.secondary.opacity(0.18), lineWidth: 1) |
| | | ) |
| | | .transition(.opacity.combined(with: .move(edge: .top))) |
| | | } |
| | | } |
| | | |
| | |
| | | .lineSpacing(2) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | |
| | | if let price = proEntitlement.priceDisplayText, !proEntitlement.isUnlocked { |
| | | Text(price) |
| | | .font(.system(size: 12, weight: .semibold)) |
| | | .foregroundStyle(Color.accentColor) |
| | | } |
| | | } |
| | | |
| | | Spacer(minLength: 16) |
| | |
| | | case .theme: |
| | | ScrollView(.vertical, showsIndicators: true) { |
| | | VStack(alignment: .leading, spacing: 18) { |
| | | VStack(alignment: .leading, spacing: 6) { |
| | | Text(tr("settings.theme")) |
| | | .font(.headline) |
| | | Text(tr("settings.themeDesc")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | } |
| | | |
| | | themeProStatusRow |
| | | |
| | | LazyVGrid(columns: themeColumns, alignment: .leading, spacing: 12) { |
| | | ForEach(AppGridTheme.allCases) { theme in |
| | | ForEach(AppGridTheme.settingsDisplayOrder) { theme in |
| | | ThemeOptionButton( |
| | | theme: theme, |
| | | title: tr(theme.titleKey), |
| | | isSelected: effectiveAppGridTheme == theme, |
| | | previewTuning: currentThemeTuning(for: theme), |
| | | accessory: themeOptionAccessory(for: theme) |
| | | ) { |
| | | handleThemeSelection(theme) |
| | |
| | | } |
| | | } |
| | | |
| | | Text(tr("settings.themeScopeDesc")) |
| | | .font(.caption) |
| | | .foregroundStyle(.secondary) |
| | | .fixedSize(horizontal: false, vertical: true) |
| | | themeColorTuningSection |
| | | } |
| | | .frame(maxWidth: generalContentWidth, alignment: .leading) |
| | | .padding(.horizontal) |
| | |
| | | let theme: AppGridTheme |
| | | let title: String |
| | | let isSelected: Bool |
| | | let previewTuning: Double |
| | | let accessory: ThemeOptionAccessory |
| | | let action: () -> Void |
| | | |
| | |
| | | RoundedRectangle(cornerRadius: 8, style: .continuous) |
| | | .fill( |
| | | LinearGradient( |
| | | colors: theme.previewColors, |
| | | colors: theme.previewColors(tuning: previewTuning), |
| | | startPoint: .topLeading, |
| | | endPoint: .bottomTrailing |
| | | ) |