From e26e5cb2aa2e7e28c5787463adbe3eaf00702b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E6=98=8A=E7=87=83?= <11169285@MacBook-Air-2.local> Date: Wed, 28 Oct 2026 10:16:57 +0800 Subject: [PATCH] =?UTF-8?q?release:=20v0.9.6=20=E2=80=94=20=E6=B6=B2?= =?UTF-8?q?=E6=80=81=E7=8E=BB=E7=92=83=E4=B8=8E=20HUD=20=E7=9A=84=207=20?= =?UTF-8?q?=E9=A1=B9=20P0=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/DesignSystem/GlassSurface.swift | 16 ++++-- .../Editor/AnnotationInspectorView.swift | 53 ++++++++++--------- .../BarcodeResultPanel+Controls.swift | 10 ++-- .../Sources/Overlays/BarcodeResultPanel.swift | 19 ++++--- .../Overlays/CaptureActionBarView.swift | 7 ++- .../Windows/HistoryPreferencesView.swift | 19 ++++++- .../Windows/HistoryStorageDashboard.swift | 3 +- .../Sources/Windows/HistoryView+Actions.swift | 10 ++++ .../Sources/Windows/HistoryView+States.swift | 40 ++++++++++---- .../Sources/Windows/HistoryView.swift | 6 ++- CHANGELOG.md | 14 ++++- .../Sources/ColorHistoryStore.swift | 47 +++++++++++++++- README.md | 2 +- project.yml | 2 +- version.txt | 2 +- 15 files changed, 190 insertions(+), 60 deletions(-) diff --git a/App/SnapGlass/Sources/DesignSystem/GlassSurface.swift b/App/SnapGlass/Sources/DesignSystem/GlassSurface.swift index 0a256bc..7f5df0c 100644 --- a/App/SnapGlass/Sources/DesignSystem/GlassSurface.swift +++ b/App/SnapGlass/Sources/DesignSystem/GlassSurface.swift @@ -40,12 +40,20 @@ struct GlassSurface: ViewModifier { private var surface: some View { if reduceTransparency { shape.fill(Color(nsColor: .windowBackgroundColor)) - } else if #available(macOS 26.0, *) { - shape - .fill(.clear) - .glassEffect(.regular, in: shape) } else { + // `glassEffect` 只存在于 macOS 26 SDK(Swift 6.2 起)。旧 SDK 上连符号 + // 都没有,只写 `#available` 会在编译期失败,因此编译期与运行时两层守卫都要有。 + #if compiler(>=6.2) + if #available(macOS 26.0, *) { + shape + .fill(.clear) + .glassEffect(.regular, in: shape) + } else { + shape.fill(elevation.material) + } + #else shape.fill(elevation.material) + #endif } } } diff --git a/App/SnapGlass/Sources/Editor/AnnotationInspectorView.swift b/App/SnapGlass/Sources/Editor/AnnotationInspectorView.swift index cc17470..ce60531 100644 --- a/App/SnapGlass/Sources/Editor/AnnotationInspectorView.swift +++ b/App/SnapGlass/Sources/Editor/AnnotationInspectorView.swift @@ -60,38 +60,41 @@ struct AnnotationInspectorView: View { } var body: some View { - VStack(alignment: .leading, spacing: DS.Space.s12) { - HStack { - Label { - Text(headerTitle) - } icon: { - Image(systemName: headerIcon) - } - .font(.headline) + ScrollView(.vertical) { + VStack(alignment: .leading, spacing: DS.Space.s12) { + HStack { + Label { + Text(headerTitle) + } icon: { + Image(systemName: headerIcon) + } + .font(.headline) - Spacer() + Spacer() - if isEditingSelection { - Button(role: .destructive, action: viewModel.removeSelectedNode) { - Image(systemName: "trash") + if isEditingSelection { + Button(role: .destructive, action: viewModel.removeSelectedNode) { + Image(systemName: "trash") + } + .buttonStyle(.plain) + .frame(width: DS.Hit.small, height: DS.Hit.small) + .contentShape(Rectangle()) + .help("Delete selected annotation") + .accessibilityLabel("Delete selected annotation") } - .buttonStyle(.plain) - .frame(width: DS.Hit.small, height: DS.Hit.small) - .contentShape(Rectangle()) - .help("Delete selected annotation") - .accessibilityLabel("Delete selected annotation") } - } - if hasContext { - controls - } else { - emptyState - } + if hasContext { + controls + } else { + emptyState + } - Spacer() + Spacer(minLength: 0) + } + .padding(DS.Space.s12) + .frame(maxWidth: .infinity, alignment: .leading) } - .padding(DS.Space.s12) .frame(minWidth: 240, idealWidth: 240, maxWidth: 280) .glassSurface( Rectangle(), diff --git a/App/SnapGlass/Sources/Overlays/BarcodeResultPanel+Controls.swift b/App/SnapGlass/Sources/Overlays/BarcodeResultPanel+Controls.swift index 5cb4674..ab0527a 100644 --- a/App/SnapGlass/Sources/Overlays/BarcodeResultPanel+Controls.swift +++ b/App/SnapGlass/Sources/Overlays/BarcodeResultPanel+Controls.swift @@ -40,7 +40,7 @@ extension BarcodeResultPanel { control.bezelStyle = .inline control.title = "" control.imagePosition = .imageOnly - control.contentTintColor = NSColor.white.withAlphaComponent(0.7) + control.contentTintColor = .secondaryLabelColor control.widthAnchor.constraint(equalToConstant: 20).isActive = true control.heightAnchor.constraint(equalToConstant: 20).isActive = true } @@ -81,7 +81,7 @@ extension BarcodeResultPanel { func attributedPayload(_ payload: BarcodePayload) -> NSAttributedString { let secondary: [NSAttributedString.Key: Any] = [ .font: NSFont.monospacedSystemFont(ofSize: 11.5, weight: .regular), - .foregroundColor: NSColor.white.withAlphaComponent(0.62), + .foregroundColor: NSColor.secondaryLabelColor, ] let text = NSMutableAttributedString() @@ -99,7 +99,7 @@ extension BarcodeResultPanel { string: host, attributes: [ .font: NSFont.systemFont(ofSize: 12.5, weight: .semibold), - .foregroundColor: NSColor.white.withAlphaComponent(0.94), + .foregroundColor: NSColor.labelColor, ] ) ) @@ -115,7 +115,7 @@ extension BarcodeResultPanel { string: payload.text, attributes: [ .font: NSFont.monospacedSystemFont(ofSize: 12, weight: .regular), - .foregroundColor: NSColor.white.withAlphaComponent(0.94), + .foregroundColor: NSColor.labelColor, ] ) ) @@ -128,7 +128,7 @@ extension BarcodeResultPanel { func badge(_ text: String) -> NSTextField { let field = NSTextField(labelWithString: text) field.font = .systemFont(ofSize: 10, weight: .semibold) - field.textColor = .white + field.textColor = .labelColor field.alignment = .center field.wantsLayer = true field.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.16).cgColor diff --git a/App/SnapGlass/Sources/Overlays/BarcodeResultPanel.swift b/App/SnapGlass/Sources/Overlays/BarcodeResultPanel.swift index 58e1569..fc61a09 100644 --- a/App/SnapGlass/Sources/Overlays/BarcodeResultPanel.swift +++ b/App/SnapGlass/Sources/Overlays/BarcodeResultPanel.swift @@ -6,7 +6,7 @@ import SharedKit /// 选区条码识别的结果浮层。 /// -/// 与动作栏同族:`NSVisualEffectView` + `.hudWindow` + `.withinWindow`。 +/// 与动作栏同族:`NSVisualEffectView` + `.hudWindow` + `.behindWindow`。 /// 它是 `AreaTrackingView` 的子视图而不是独立窗口,因此不会被自己的截图 /// 收进去,也不依赖任何 `sharingType` 处理。 /// @@ -49,7 +49,12 @@ final class BarcodeResultPanel: NSVisualEffectView { super.init(frame: frameRect) material = .hudWindow - blendingMode = .withinWindow + // 宿主 `AreaSelectionPanel` 是 clear + 非 opaque:窗内没有可采样的内容, + // `.withinWindow` 只会退化成一块灰底。改用 `.behindWindow` 采桌面, + // 与 `PinnedToolbarView` 保持一致。 + blendingMode = .behindWindow + // HUD 恒定深色:上面的语义文字色据此解析,浅色系统与「降低透明度」下都保持浅字。 + appearance = NSAppearance(named: .darkAqua) state = .active wantsLayer = true layer?.cornerRadius = 10 @@ -212,7 +217,7 @@ final class BarcodeResultPanel: NSVisualEffectView { label( AppLocalization.string("Scanning…"), font: .systemFont(ofSize: 12), - color: NSColor.white.withAlphaComponent(0.62) + color: .secondaryLabelColor ), ]) row.orientation = .horizontal @@ -280,7 +285,7 @@ final class BarcodeResultPanel: NSVisualEffectView { label( AppLocalization.string("%d more", overflow), font: .systemFont(ofSize: 11), - color: NSColor.white.withAlphaComponent(0.42) + color: .tertiaryLabelColor ) ) } @@ -324,7 +329,7 @@ final class BarcodeResultPanel: NSVisualEffectView { label( AppLocalization.string("No barcode in selection"), font: .systemFont(ofSize: 12), - color: NSColor.white.withAlphaComponent(0.62), + color: .secondaryLabelColor, lines: 2 ), rescanButton(role: .secondary), @@ -349,7 +354,7 @@ final class BarcodeResultPanel: NSVisualEffectView { label( title, font: .systemFont(ofSize: 12, weight: .semibold), - color: NSColor.white.withAlphaComponent(0.94), + color: .labelColor, maxWidth: Self.contentWidth - reserved - 96 ) ) @@ -388,7 +393,7 @@ final class BarcodeResultPanel: NSVisualEffectView { label( AppLocalization.string(hasLink ? "Barcode Hint with Link" : "Barcode Hint"), font: .systemFont(ofSize: 10), - color: NSColor.white.withAlphaComponent(0.42) + color: .tertiaryLabelColor ) } diff --git a/App/SnapGlass/Sources/Overlays/CaptureActionBarView.swift b/App/SnapGlass/Sources/Overlays/CaptureActionBarView.swift index 9c246af..077dfd9 100644 --- a/App/SnapGlass/Sources/Overlays/CaptureActionBarView.swift +++ b/App/SnapGlass/Sources/Overlays/CaptureActionBarView.swift @@ -21,7 +21,12 @@ final class CaptureActionBarView: NSVisualEffectView { super.init(frame: frameRect) material = .hudWindow - blendingMode = .withinWindow + // 宿主 `AreaSelectionPanel` 是 clear + 非 opaque:窗内没有可采样的内容, + // `.withinWindow` 只会退化成一块灰底。改用 `.behindWindow` 采桌面, + // 与 `PinnedToolbarView` 保持一致。 + blendingMode = .behindWindow + // HUD 恒定深色:按钮标题等语义色据此解析,浅色系统下不会变成黑字。 + appearance = NSAppearance(named: .darkAqua) state = .active wantsLayer = true layer?.cornerRadius = DS.Corner.c10 diff --git a/App/SnapGlass/Sources/Windows/HistoryPreferencesView.swift b/App/SnapGlass/Sources/Windows/HistoryPreferencesView.swift index 2f4ed3e..3dcd924 100644 --- a/App/SnapGlass/Sources/Windows/HistoryPreferencesView.swift +++ b/App/SnapGlass/Sources/Windows/HistoryPreferencesView.swift @@ -31,6 +31,9 @@ struct HistoryPreferencesView: View { @State private var pendingCleanupCount = 0 @State private var showsCleanupConfirmation = false @State private var policyRequestGeneration = 0 + /// Retention changes need `HistoryActor.shared`; a failed store initialization + /// must be surfaced instead of silently dropping the change. + @State private var showsHistoryUnavailable = false private let logger = Logger(category: "preferences") @@ -122,6 +125,12 @@ struct HistoryPreferencesView: View { PreferencesCardCaption(text: "Favourite screenshots are not removed by count or age limits.") } + // 单独挂在保留卡上:同一视图上的两个 `.alert` 只会生效一个。 + .alert("History unavailable", isPresented: $showsHistoryUnavailable) { + Button("OK", role: .cancel) {} + } message: { + Text("Check the application support folder permissions") + } } private var storageCard: some View { @@ -205,7 +214,10 @@ struct HistoryPreferencesView: View { } private func clearColorHistory() async { - guard let colorHistory = ColorHistoryStore.shared else { return } + guard let colorHistory = ColorHistoryStore.shared else { + showsHistoryUnavailable = true + return + } do { try await colorHistory.clear() } catch { @@ -223,7 +235,10 @@ struct HistoryPreferencesView: View { let pendingRetentionDays = keepIndefinitely ? 0 : retentionDays let pendingStorageSize = storageSize - guard let history = HistoryActor.shared else { return } + guard let history = HistoryActor.shared else { + showsHistoryUnavailable = true + return + } Task { let deletions = await history.entriesPendingDeletion( maxItems: pendingMaxItems, diff --git a/App/SnapGlass/Sources/Windows/HistoryStorageDashboard.swift b/App/SnapGlass/Sources/Windows/HistoryStorageDashboard.swift index 25dfc47..f530658 100644 --- a/App/SnapGlass/Sources/Windows/HistoryStorageDashboard.swift +++ b/App/SnapGlass/Sources/Windows/HistoryStorageDashboard.swift @@ -99,7 +99,8 @@ struct HistoryStorageDashboard: View { metricCard( icon: "eyedropper", title: "Colors", - value: "\(colorCount ?? 0)", + // 读取失败与「真的没有记录」必须区分:未知显示破折号,不冒充 0。 + value: colorCount.map { String($0) } ?? "—", tint: .orange ) metricCard( diff --git a/App/SnapGlass/Sources/Windows/HistoryView+Actions.swift b/App/SnapGlass/Sources/Windows/HistoryView+Actions.swift index fa06e8f..849e37a 100644 --- a/App/SnapGlass/Sources/Windows/HistoryView+Actions.swift +++ b/App/SnapGlass/Sources/Windows/HistoryView+Actions.swift @@ -104,6 +104,16 @@ extension HistoryView { await loadEntries() } + /// Retries creating the shared color history store after an initialization + /// failure (for example, a transient permissions or key-access problem). + func retryColorHistoryInitialization() async { + if colorHistory == nil { + ColorHistoryStore.resetSharedForRetry() + colorHistory = ColorHistoryStore.shared + } + await loadColorEntries() + } + func loadScreenshotEntries() async { let loadID = UUID() screenshotLoadID = loadID diff --git a/App/SnapGlass/Sources/Windows/HistoryView+States.swift b/App/SnapGlass/Sources/Windows/HistoryView+States.swift index a7115eb..9d632a6 100644 --- a/App/SnapGlass/Sources/Windows/HistoryView+States.swift +++ b/App/SnapGlass/Sources/Windows/HistoryView+States.swift @@ -55,6 +55,35 @@ extension HistoryView { NSWorkspace.shared.open(folder) } + /// 取色历史存储初始化失败:与「还没有取过色」区分开,并给出重试入口。 + var colorUnavailableState: some View { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 40)) + .foregroundColor(.secondary) + + Text("History unavailable") + .font(.title3) + .foregroundColor(.secondary) + + Text("Check the application support folder permissions") + .font(.caption) + .foregroundStyle(.tertiary) + .multilineTextAlignment(.center) + .frame(maxWidth: 340) + + HStack(spacing: 12) { + Button("Retry") { + Task { await retryColorHistoryInitialization() } + } + Button("Open Storage Folder") { + openHistoryFolder() + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + // MARK: - Empty State var emptyStateTitle: LocalizedStringKey { @@ -97,11 +126,7 @@ extension HistoryView { .font(.title3) .foregroundColor(.secondary) - if colorHistory == nil { - Text("Check the application support folder permissions") - .font(.caption) - .foregroundStyle(.tertiary) - } else if !colorFilter.isEmpty { + if !colorFilter.isEmpty { Text("Try a different search term") .font(.caption) .foregroundStyle(.tertiary) @@ -111,10 +136,7 @@ extension HistoryView { } var colorEmptyStateTitle: LocalizedStringKey { - if colorHistory == nil { - return "History unavailable" - } - return colorFilter.isEmpty ? "No colors captured yet" : "No matching colors" + colorFilter.isEmpty ? "No colors captured yet" : "No matching colors" } } diff --git a/App/SnapGlass/Sources/Windows/HistoryView.swift b/App/SnapGlass/Sources/Windows/HistoryView.swift index 92a5ce1..1112d63 100644 --- a/App/SnapGlass/Sources/Windows/HistoryView.swift +++ b/App/SnapGlass/Sources/Windows/HistoryView.swift @@ -39,7 +39,9 @@ struct HistoryView: View { /// A failed history read, kept separate from transient action errors. @State var loadError: String? - let colorHistory = ColorHistoryStore.shared + /// Kept in state so a failed shared store initialization can be retried from + /// the unavailable screen instead of requiring an app restart. + @State var colorHistory: ColorHistoryStore? = ColorHistoryStore.shared /// Kept in state so a failed `HistoryActor.shared` initialization can be /// retried from the unavailable screen instead of requiring an app restart. @State var history: HistoryActor? = HistoryActor.shared @@ -77,6 +79,8 @@ struct HistoryView: View { colorFilterBar if isLoadingColors, colorEntries.isEmpty { loadingState + } else if colorHistory == nil { + colorUnavailableState } else if filteredColorEntries.isEmpty { colorEmptyState } else { diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f5280..24f4ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.9.6] - 2026-10-28 + +### Fixed +- `GlassSurface` 补编译期守卫:旧 SDK(Swift < 6.2)没有 `glassEffect` 符号,26+ 分支改为同时受 `#if compiler(>=6.2)` 与 `#available` 保护,跨 Xcode 版本不再编译失败。 +- 条码结果浮层文字改用系统语义色(`labelColor` / `secondaryLabelColor` / `tertiaryLabelColor`)并固定深色 HUD 外观,浅色系统与「降低透明度」下仍可读。 +- 标注检查器整列可滚动:600pt 高窗口下底部控件不再被裁掉,且能滚动到位。 +- 选区动作栏与条码结果浮层改用 `.behindWindow` 混合:宿主遮罩窗是 clear + 非 opaque,`.withinWindow` 采不到桌面内容,HUD 曾显示为灰块。 +- 历史窗口「颜色」页新增「历史不可用」态与重试入口,与「还没有取色记录」区分;共享存储初始化失败不再被缓存成不可恢复状态。 +- 偏好设置修改保留策略或清空取色历史时,存储初始化失败会弹出可见提示,不再静默丢弃操作。 +- 存储概览的取色数量在读取失败时显示「—」,不再把未知显示成 0。 + ## [0.9.5] - 2026-10-27 ### Added @@ -367,7 +378,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - 移除 GUI App 的 Automation 窗口、`snapglass://` URL Scheme、App Intents 产品依赖和 CLI 构建目标 - 移除临时构建产物目录 `output/`,统一收敛到 `release/` -[Unreleased]: https://github.com/blackkcold/snapocr/compare/v0.9.5...HEAD +[Unreleased]: https://github.com/blackkcold/snapocr/compare/v0.9.6...HEAD +[0.9.6]: https://github.com/blackkcold/snapocr/compare/v0.9.5...v0.9.6 [0.9.5]: https://github.com/blackkcold/snapocr/compare/v0.9.1...v0.9.5 [0.9.1]: https://github.com/blackkcold/snapocr/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/blackkcold/snapocr/compare/v0.8.9...v0.9.0 diff --git a/Packages/HistoryCore/Sources/ColorHistoryStore.swift b/Packages/HistoryCore/Sources/ColorHistoryStore.swift index 9e534ed..851e299 100644 --- a/Packages/HistoryCore/Sources/ColorHistoryStore.swift +++ b/Packages/HistoryCore/Sources/ColorHistoryStore.swift @@ -15,7 +15,19 @@ import SharedKit public actor ColorHistoryStore { /// 共享实例,供 UI 层使用;初始化失败时返回 `nil`,调用方优雅降级。 - public static let shared: ColorHistoryStore? = try? ColorHistoryStore() + /// + /// 失败不缓存:目录或密钥恢复后,下一次访问(或「重试」入口)可重新初始化。 + public static var shared: ColorHistoryStore? { + ColorHistorySharedProvider.shared.instance() + } + + /// 丢弃当前共享实例,下一次访问重新初始化。 + /// + /// 与 `HistoryActor.resetSharedForRetry()` 同源:供历史窗口的「重试」使用, + /// 避免把一次初始化失败缓存成不可恢复状态。 + public static func resetSharedForRetry() { + ColorHistorySharedProvider.shared.reset() + } private let fileURL: URL private let cryptoService: CryptoService @@ -115,3 +127,36 @@ public actor ColorHistoryStore { return min(max(configured, 10), 5_000) } } + +/// 线程安全的共享 actor 缓存。 +/// +/// 与 `HistorySharedProvider` 同构:用 `NSLock` 而非 actor,让同步调用点 +/// (菜单栏、View 初始化)也能取得同一实例;初始化失败不缓存,下一次访问可重试。 +private final class ColorHistorySharedProvider: @unchecked Sendable { + static let shared = ColorHistorySharedProvider { try ColorHistoryStore() } + + private let lock = NSLock() + private let makeStore: @Sendable () throws -> ColorHistoryStore + private var cached: ColorHistoryStore? + + init(makeStore: @escaping @Sendable () throws -> ColorHistoryStore) { + self.makeStore = makeStore + } + + func instance() -> ColorHistoryStore? { + lock.lock() + defer { lock.unlock() } + if let cached { + return cached + } + guard let created = try? makeStore() else { return nil } + cached = created + return created + } + + func reset() { + lock.lock() + defer { lock.unlock() } + cached = nil + } +} diff --git a/README.md b/README.md index a5be6c0..1da53ab 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@

- Download + Download

diff --git a/project.yml b/project.yml index ad7fbc0..8d3e55c 100644 --- a/project.yml +++ b/project.yml @@ -15,7 +15,7 @@ settings: SWIFT_VERSION: "6.0" MACOSX_DEPLOYMENT_TARGET: "13.0" ALWAYS_SEARCH_USER_PATHS: NO - MARKETING_VERSION: "0.9.5" + MARKETING_VERSION: "0.9.6" CURRENT_PROJECT_VERSION: "1" configs: Debug: diff --git a/version.txt b/version.txt index b0bb878..85b7c69 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.9.5 +0.9.6