From 39e759d015af5d82f387b57a8d137bdeb844d430 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 19 Jun 2026 22:40:08 +0800
Subject: [PATCH] Fix historical Apple default note migration
---
src/Apptag/PreferencesView.swift | 393 ++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 295 insertions(+), 98 deletions(-)
diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index bc0bd82..2dea588 100644
--- a/src/Apptag/PreferencesView.swift
+++ b/src/Apptag/PreferencesView.swift
@@ -3,16 +3,55 @@
// MARK: - Preferences View
+private enum SettingsTab: String, CaseIterable, Identifiable {
+ case language
+ case general
+ case hotkeys
+ case tags
+ case data
+ case about
+
+ var id: String { rawValue }
+
+ var titleKey: String {
+ switch self {
+ case .language: return "settings.language"
+ case .general: return "settings.general"
+ case .hotkeys: return "quickSearch.hotkeys"
+ case .tags: return "settings.tags"
+ case .data: return "settings.data"
+ case .about: return "settings.about"
+ }
+ }
+
+ var systemImage: String {
+ switch self {
+ case .language: return "globe"
+ case .general: return "gearshape"
+ case .hotkeys: return "keyboard"
+ case .tags: return "tag.fill"
+ case .data: return "externaldrive.fill"
+ case .about: return "info.circle"
+ }
+ }
+}
+
+private enum InitialLayoutMode: String {
+ case uncategorized
+ case smart
+}
+
struct PreferencesView: View {
private let settingsWindowWidth: CGFloat = 1000
+ private let settingsWindowHeight: CGFloat = 640
private let settingsContentWidth: CGFloat = 940
private let generalContentWidth: CGFloat = 720
private let generalLabelWidth: CGFloat = 190
private let generalControlWidth: CGFloat = 500
private let compactPickerWidth: CGFloat = 320
- private let dataPanelWidth: CGFloat = 760
- private let dataLabelWidth: CGFloat = 220
- private let dataActionWidth: CGFloat = 112
+ private let dataPanelWidth: CGFloat = 820
+ private let dataLabelWidth: CGFloat = 190
+ private let dataActionWidth: CGFloat = 128
@AppStorage("tagFontSize") private var tagFontSize: Double = AppDefaults.tagFontSize
@AppStorage("iconSize") private var iconSize: Double = AppDefaults.iconSize
@@ -23,6 +62,7 @@
@AppStorage("showDockIcon") private var showDockIcon = AppDefaults.showDockIcon
@AppStorage("launchAtLogin") private var launchAtLogin = AppDefaults.launchAtLogin
@AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
+ @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips
@AppStorage("mainHotkeyRegistrationState") private var mainHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue
@AppStorage("quickSearchHotkeyRegistrationState") private var quickSearchHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue
@State private var selectedLanguage = L10n.selectedLanguageCode
@@ -31,10 +71,14 @@
@State private var tagColors: [String: Int] = [:]
@State private var categoryScheme = TagDatabase.CategorySchemeState()
@State private var isApplyingSystemScheme = false
+ @State private var isResettingToUncategorized = false
@State private var showApplySystemSchemeConfirmation = false
+ @State private var showResetToUncategorizedConfirmation = false
@State private var isDataFilePanelPresented = false
@State private var hotkeyStatusToast: String? = nil
@State private var hotkeyStatusToastToken: UUID? = nil
+ @State private var selectedTab: SettingsTab = .general
+ @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
private func scanApps() {
DispatchQueue.global(qos: .userInitiated).async {
@@ -264,6 +308,18 @@
}
}
+ private func applySelectedInitialLayout() {
+ switch selectedInitialLayoutMode {
+ case .uncategorized:
+ guard !isResettingToUncategorized else { return }
+ withAnimation(.easeOut(duration: 0.16)) {
+ showResetToUncategorizedConfirmation = true
+ }
+ case .smart:
+ applySystemInitialScheme()
+ }
+ }
+
private func performApplySystemInitialScheme() {
guard !isApplyingSystemScheme else { return }
showApplySystemSchemeConfirmation = false
@@ -291,6 +347,29 @@
message: tr("settings.systemSchemeApplyFailed")
)
}
+ }
+ }
+ }
+
+ private func performResetToUncategorized() {
+ guard !isResettingToUncategorized else { return }
+ showResetToUncategorizedConfirmation = false
+ TagDatabase.flushPendingCategorySchemeBackupBatch()
+ isResettingToUncategorized = true
+
+ DispatchQueue.global(qos: .userInitiated).async {
+ let result = AppLibraryController.resetToUncategorized()
+
+ DispatchQueue.main.async {
+ isResettingToUncategorized = false
+ allApps = result.snapshot.apps
+ tagColors = result.snapshot.tagColors
+ refreshDataState()
+ notifyDataChanged()
+ showDataAlert(
+ title: tr("settings.resetToUncategorizedApplied"),
+ message: tr("settings.resetToUncategorizedAppliedMessage")
+ )
}
}
}
@@ -353,6 +432,58 @@
.contentShape(Rectangle())
}
.buttonStyle(.plain)
+ }
+
+ private func initialLayoutOption(
+ _ title: String,
+ mode: InitialLayoutMode
+ ) -> some View {
+ let isSelected = selectedInitialLayoutMode == mode
+ return Button {
+ selectedInitialLayoutMode = mode
+ } label: {
+ HStack(spacing: 6) {
+ Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
+ .font(.system(size: 13, weight: .medium))
+ .frame(width: 16, height: 16)
+ Text(title)
+ .font(.system(size: 13, weight: .medium))
+ .lineLimit(1)
+ .minimumScaleFactor(0.85)
+ }
+ .foregroundStyle(isSelected ? Color.accentColor : Color.primary)
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ }
+
+ private func dataSectionTitle(_ title: String) -> some View {
+ Text(title)
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ .minimumScaleFactor(0.82)
+ .multilineTextAlignment(.trailing)
+ .frame(width: dataLabelWidth, alignment: .trailing)
+ }
+
+ private func dataSection<Content: View>(
+ minHeight: CGFloat,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+ content()
+ .padding(.horizontal, 20)
+ .padding(.vertical, 18)
+ .frame(width: dataPanelWidth, alignment: .leading)
+ .frame(minHeight: minHeight, alignment: .leading)
+ .background(
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .fill(Color(nsColor: .controlBackgroundColor))
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .stroke(Color.secondary.opacity(0.18), lineWidth: 1)
+ )
}
private func syncSelectedLanguage() {
@@ -493,9 +624,58 @@
}
}
+ private var settingsTabBar: some View {
+ HStack(spacing: 8) {
+ ForEach(SettingsTab.allCases) { tab in
+ settingsTabButton(tab)
+ }
+ }
+ .frame(height: 96, alignment: .center)
+ .frame(maxWidth: .infinity)
+ .background(Color(nsColor: .windowBackgroundColor))
+ }
+
+ private func settingsTabButton(_ tab: SettingsTab) -> some View {
+ let isSelected = selectedTab == tab
+ return Button {
+ selectedTab = tab
+ } label: {
+ VStack(spacing: 5) {
+ Image(systemName: tab.systemImage)
+ .font(.system(size: 25, weight: .regular))
+ .symbolRenderingMode(.hierarchical)
+ .frame(width: 32, height: 32)
+ Text(tr(tab.titleKey))
+ .font(.system(size: 13, weight: .semibold))
+ .lineLimit(2)
+ .multilineTextAlignment(.center)
+ .minimumScaleFactor(0.8)
+ }
+ .foregroundStyle(isSelected ? Color.accentColor : Color.secondary)
+ .frame(width: 108, height: 74)
+ .background(
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .fill(isSelected ? Color.accentColor.opacity(0.10) : Color.clear)
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .stroke(isSelected ? Color.accentColor.opacity(0.20) : Color.clear, lineWidth: 1)
+ )
+ .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
+ }
+ .buttonStyle(.plain)
+ .accessibilityLabel(tr(tab.titleKey))
+ .accessibilityIdentifier("settings-tab-\(tab.rawValue)")
+ }
+
var body: some View {
ZStack {
- TabView {
+ VStack(spacing: 0) {
+ settingsTabBar
+ Divider()
+ Group {
+ switch selectedTab {
+ case .language:
// Tab 1: Language
VStack(alignment: .leading, spacing: 10) {
Text(tr("settings.language"))
@@ -558,9 +738,9 @@
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
.frame(maxWidth: settingsContentWidth, alignment: .leading)
- .tabItem { Label(tr("settings.language"), systemImage: "globe") }
.padding()
+ case .general:
// Tab 2: General
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 0) {
@@ -581,9 +761,12 @@
AppDelegate.refreshChromeSettings()
}
Toggle(tr("settings.hideAppNames"), isOn: $hideAppNames)
+ Toggle(tr("settings.hideUsageTips"), isOn: $hideUsageTips)
+ .help(tr("settings.hideUsageTipsDesc"))
}
.frame(maxWidth: .infinity, alignment: .center)
- .padding(.bottom, 16)
+ .padding(.top, 12)
+ .padding(.bottom, 18)
Divider()
.padding(.bottom, 16)
@@ -646,10 +829,12 @@
}
}
.frame(maxWidth: generalContentWidth, alignment: .center)
- .padding()
+ .padding(.horizontal)
+ .padding(.top, 28)
+ .padding(.bottom)
}
- .tabItem { Label(tr("settings.general"), systemImage: "gearshape") }
+ case .hotkeys:
// Tab 3: Hotkeys
ScrollView(.vertical, showsIndicators: true) {
VStack(alignment: .leading, spacing: 16) {
@@ -659,8 +844,8 @@
.frame(maxWidth: generalContentWidth, alignment: .center)
.padding()
}
- .tabItem { Label(tr("quickSearch.hotkeys"), systemImage: "keyboard") }
+ case .tags:
// Tab 4: Tags
VStack(spacing: 0) {
TagEditorView(
@@ -669,57 +854,99 @@
onRefresh: { scanApps() }
)
}
- .tabItem { Label(tr("settings.tags"), systemImage: "tag.fill") }
.onAppear { scanApps() }
+ case .data:
// Tab 5: Data
VStack(spacing: 0) {
- Spacer(minLength: 32)
+ Spacer(minLength: 36)
- VStack(alignment: .center, spacing: 0) {
- VStack(alignment: .leading, spacing: 12) {
+ VStack(alignment: .center, spacing: 24) {
+ dataSection(minHeight: 124) {
HStack(alignment: .center, spacing: 12) {
- Text(tr("settings.systemScheme"))
- .font(.system(size: 13, weight: .semibold))
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .minimumScaleFactor(0.82)
- .multilineTextAlignment(.trailing)
- .frame(width: dataLabelWidth, alignment: .trailing)
+ dataSectionTitle(tr("settings.initialLayout"))
- Text(SmartStartService.systemInitialSchemeName)
- .font(.system(size: 13, weight: .medium))
- .foregroundStyle(.primary)
- .lineLimit(1)
- .truncationMode(.middle)
+ HStack(spacing: 22) {
+ initialLayoutOption(
+ tr("settings.initialLayoutUncategorized"),
+ mode: .uncategorized
+ )
+ initialLayoutOption(
+ tr("settings.initialLayoutSmart"),
+ mode: .smart
+ )
+ }
.frame(maxWidth: .infinity, alignment: .leading)
- .layoutPriority(0)
+ .layoutPriority(1)
dataActionButton(
tr("settings.applyScheme"),
prominent: true,
- enabled: !isApplyingSystemScheme,
- action: applySystemInitialScheme
+ enabled: !isApplyingSystemScheme && !isResettingToUncategorized,
+ action: applySelectedInitialLayout
)
.frame(width: dataActionWidth, alignment: .trailing)
.layoutPriority(2)
}
+ }
- HStack(alignment: .center, spacing: 12) {
- Text(tr("settings.previousScheme"))
- .font(.system(size: 13, weight: .semibold))
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .minimumScaleFactor(0.82)
- .multilineTextAlignment(.trailing)
- .frame(width: dataLabelWidth, alignment: .trailing)
- Text(previousCategorySchemeName)
- .font(.system(size: 13, weight: .medium))
- .foregroundStyle(canRestorePreviousScheme ? .primary : .tertiary)
- .lineLimit(1)
- .truncationMode(.middle)
- .frame(maxWidth: .infinity, alignment: .leading)
- .layoutPriority(0)
+ dataSection(minHeight: 190) {
+ HStack(alignment: .top, spacing: 12) {
+ dataSectionTitle(tr("settings.customLayout"))
+ .padding(.top, 4)
+
+ VStack(alignment: .leading, spacing: 10) {
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
+ Text(tr("settings.previousScheme"))
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .minimumScaleFactor(0.82)
+ Text(previousCategorySchemeName)
+ .font(.system(size: 13, weight: .medium))
+ .foregroundStyle(canRestorePreviousScheme ? .primary : .tertiary)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ HStack(alignment: .firstTextBaseline, spacing: 8) {
+ Text(tr("settings.currentScheme"))
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .minimumScaleFactor(0.82)
+ Text(currentCategorySchemeName)
+ .font(.system(size: 13, weight: .medium))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
+ Divider().opacity(0.35)
+ .padding(.top, 2)
+
+ VStack(alignment: .center, spacing: 8) {
+ HStack(spacing: 12) {
+ Button(tr("settings.export")) { exportTags() }
+ .buttonStyle(.bordered)
+ .disabled(isDataFilePanelPresented)
+ Button(tr("settings.import")) { importTags() }
+ .buttonStyle(.bordered)
+ .disabled(isDataFilePanelPresented)
+ }
+ Text(tr("settings.backupDesc"))
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
+ .frame(maxWidth: dataPanelWidth - dataLabelWidth - dataActionWidth - 92, alignment: .center)
+ }
+ .frame(maxWidth: .infinity, alignment: .center)
+ }
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .layoutPriority(1)
dataActionButton(
tr("settings.restorePreviousScheme"),
@@ -730,60 +957,11 @@
.frame(width: dataActionWidth, alignment: .trailing)
.layoutPriority(2)
}
-
- HStack(alignment: .firstTextBaseline, spacing: 12) {
- Text(tr("settings.currentScheme"))
- .font(.system(size: 13, weight: .semibold))
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .minimumScaleFactor(0.82)
- .multilineTextAlignment(.trailing)
- .frame(width: dataLabelWidth, alignment: .trailing)
- Text(currentCategorySchemeName)
- .font(.system(size: 13, weight: .medium))
- .foregroundStyle(.primary)
- .lineLimit(1)
- .truncationMode(.middle)
- .frame(maxWidth: .infinity, alignment: .leading)
- Color.clear
- .frame(width: dataActionWidth, height: 1)
- }
-
- Divider().opacity(0.35)
-
- VStack(alignment: .center, spacing: 8) {
- HStack(spacing: 12) {
- Button(tr("settings.export")) { exportTags() }
- .buttonStyle(.bordered)
- .disabled(isDataFilePanelPresented)
- Button(tr("settings.import")) { importTags() }
- .buttonStyle(.bordered)
- .disabled(isDataFilePanelPresented)
- }
- Text(tr("settings.backupDesc"))
- .font(.caption)
- .foregroundStyle(.secondary)
- .multilineTextAlignment(.center)
- .fixedSize(horizontal: false, vertical: true)
- .frame(maxWidth: dataPanelWidth - 72, alignment: .center)
- }
- .frame(maxWidth: .infinity, alignment: .center)
}
- .padding(.horizontal, 20)
- .padding(.vertical, 16)
- .frame(width: dataPanelWidth, alignment: .leading)
- .background(
- RoundedRectangle(cornerRadius: 8, style: .continuous)
- .fill(Color(nsColor: .controlBackgroundColor))
- )
- .overlay(
- RoundedRectangle(cornerRadius: 8, style: .continuous)
- .stroke(Color.secondary.opacity(0.16), lineWidth: 1)
- )
}
.frame(width: dataPanelWidth, alignment: .center)
- Spacer(minLength: 22)
+ Spacer(minLength: 30)
HStack(alignment: .center, spacing: 14) {
Text(tr("settings.bubbleDisplayScope"))
@@ -810,10 +988,10 @@
Spacer(minLength: 18)
}
- .tabItem { Label(tr("settings.data"), systemImage: "externaldrive.fill") }
.padding()
.onAppear { refreshDataState() }
+ case .about:
// Tab 6: About
VStack(spacing: 0) {
Spacer(minLength: 72)
@@ -876,8 +1054,10 @@
Spacer(minLength: 30)
Spacer(minLength: 18)
}
- .tabItem { Label(tr("settings.about"), systemImage: "info.circle") }
.padding()
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onChange(of: selectedLanguage) { _, code in
L10n.switchSelection(to: code)
@@ -909,7 +1089,7 @@
}
if showApplySystemSchemeConfirmation {
- ApplySystemSchemeConfirmationView(
+ SettingsConfirmationView(
title: tr("settings.applySystemSchemeWarningTitle"),
message: tr("settings.applySystemSchemeWarningMessage"),
confirmTitle: tr("settings.confirmApplyScheme"),
@@ -918,6 +1098,23 @@
onCancel: {
withAnimation(.easeOut(duration: 0.14)) {
showApplySystemSchemeConfirmation = false
+ }
+ }
+ )
+ .zIndex(3)
+ .transition(.opacity)
+ }
+
+ if showResetToUncategorizedConfirmation {
+ SettingsConfirmationView(
+ title: tr("settings.resetToUncategorizedWarningTitle"),
+ message: tr("settings.resetToUncategorizedWarningMessage"),
+ confirmTitle: tr("settings.confirmResetToUncategorized"),
+ cancelTitle: tr("settings.cancel"),
+ onConfirm: performResetToUncategorized,
+ onCancel: {
+ withAnimation(.easeOut(duration: 0.14)) {
+ showResetToUncategorizedConfirmation = false
}
}
)
@@ -947,7 +1144,7 @@
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
}
- .frame(width: settingsWindowWidth, height: 480)
+ .frame(width: settingsWindowWidth, height: settingsWindowHeight)
.onAppear {
syncSelectedLanguage()
showPendingHotkeyWarningIfNeeded()
@@ -1032,7 +1229,7 @@
}
}
-private struct ApplySystemSchemeConfirmationView: View {
+private struct SettingsConfirmationView: View {
let title: String
let message: String
let confirmTitle: String
--
Gitblit v1.9.3