Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions App/SnapGlass/Sources/DesignSystem/GlassSurface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,20 @@ struct GlassSurface<S: InsettableShape>: 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
}
}
}
Expand Down
53 changes: 28 additions & 25 deletions App/SnapGlass/Sources/Editor/AnnotationInspectorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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()

Expand All @@ -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,
]
)
)
Expand All @@ -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,
]
)
)
Expand All @@ -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
Expand Down
19 changes: 12 additions & 7 deletions App/SnapGlass/Sources/Overlays/BarcodeResultPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SharedKit

/// 选区条码识别的结果浮层。
///
/// 与动作栏同族:`NSVisualEffectView` + `.hudWindow` + `.withinWindow`。
/// 与动作栏同族:`NSVisualEffectView` + `.hudWindow` + `.behindWindow`。
/// 它是 `AreaTrackingView` 的子视图而不是独立窗口,因此不会被自己的截图
/// 收进去,也不依赖任何 `sharingType` 处理。
///
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
)
}
Expand Down Expand Up @@ -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),
Expand All @@ -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
)
)
Expand Down Expand Up @@ -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
)
}

Expand Down
7 changes: 6 additions & 1 deletion App/SnapGlass/Sources/Overlays/CaptureActionBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions App/SnapGlass/Sources/Windows/HistoryPreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ struct HistoryStorageDashboard: View {
metricCard(
icon: "eyedropper",
title: "Colors",
value: "\(colorCount ?? 0)",
// 读取失败与「真的没有记录」必须区分:未知显示破折号,不冒充 0。
value: colorCount.map { String($0) } ?? "—",
tint: .orange
)
metricCard(
Expand Down
10 changes: 10 additions & 0 deletions App/SnapGlass/Sources/Windows/HistoryView+Actions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 31 additions & 9 deletions App/SnapGlass/Sources/Windows/HistoryView+States.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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"
}

}
6 changes: 5 additions & 1 deletion App/SnapGlass/Sources/Windows/HistoryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,6 +79,8 @@ struct HistoryView: View {
colorFilterBar
if isLoadingColors, colorEntries.isEmpty {
loadingState
} else if colorHistory == nil {
colorUnavailableState
} else if filteredColorEntries.isEmpty {
colorEmptyState
} else {
Expand Down
Loading
Loading