From 642c97c5409b462bfcddcab0bcb7a7177afc8269 Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:10:18 +0800 Subject: [PATCH] fix: privacy manifests, history/settings headers, and dock contrast Resolve TestFlight ITMS-91056 by correcting privacy manifest keys and reason arrays. Add left-aligned page headers with circular confirm buttons, keep the keyboard surface transparent, and improve unselected dock icon contrast in light and dark mode. --- OSGKeyboard/PrivacyInfo.xcprivacy | 8 +- .../Views/Components/MinimalTabBar.swift | 11 ++- .../Components/PageHeaderConfirmButton.swift | 91 +++++++++++++++++ .../Views/Components/PageHeaderRow.swift | 37 +++++++ OSGKeyboard/Views/HistoryView.swift | 59 +++++------ OSGKeyboard/Views/SettingsView.swift | 98 ++++++++----------- OSGKeyboardExt/PrivacyInfo.xcprivacy | 14 +-- OSGKeyboardExt/Views/KeyboardRootView.swift | 7 +- 8 files changed, 213 insertions(+), 112 deletions(-) create mode 100644 OSGKeyboard/Views/Components/PageHeaderConfirmButton.swift create mode 100644 OSGKeyboard/Views/Components/PageHeaderRow.swift diff --git a/OSGKeyboard/PrivacyInfo.xcprivacy b/OSGKeyboard/PrivacyInfo.xcprivacy index 1ea7348..5704bed 100644 --- a/OSGKeyboard/PrivacyInfo.xcprivacy +++ b/OSGKeyboard/PrivacyInfo.xcprivacy @@ -6,7 +6,7 @@ NSPrivacyTrackingDomains - NSPrivacyCollectedUsageTypes + NSPrivacyCollectedDataTypes NSPrivacyAccessedAPITypes @@ -14,8 +14,10 @@ NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - CA92.1 + + CA92.1 + - \ No newline at end of file + diff --git a/OSGKeyboard/Views/Components/MinimalTabBar.swift b/OSGKeyboard/Views/Components/MinimalTabBar.swift index 777b04a..2be669c 100644 --- a/OSGKeyboard/Views/Components/MinimalTabBar.swift +++ b/OSGKeyboard/Views/Components/MinimalTabBar.swift @@ -32,6 +32,7 @@ enum AppTab: Int, CaseIterable { struct MinimalTabBar: View { @Environment(\.themePalette) private var palette: ThemePalette + @Environment(\.colorScheme) private var colorScheme @Binding var selection: AppTab var body: some View { @@ -44,7 +45,7 @@ struct MinimalTabBar: View { name: tab.icon, size: 24 ) - .foregroundStyle(selection == tab ? palette.accent : palette.textTertiary) + .foregroundStyle(tabIconColor(for: tab)) .frame(maxWidth: .infinity) .frame(height: 48) .contentShape(Rectangle()) @@ -61,4 +62,12 @@ struct MinimalTabBar: View { .frame(maxWidth: .infinity, alignment: .center) .padding(.bottom, Spacing.xs) } + + private func tabIconColor(for tab: AppTab) -> Color { + if selection == tab { return palette.accent } + // 未选中:浅色模式更深、深色模式更亮,提升 dock 可读性。 + return colorScheme == .dark + ? Color(white: 0.76) + : palette.textSecondary + } } diff --git a/OSGKeyboard/Views/Components/PageHeaderConfirmButton.swift b/OSGKeyboard/Views/Components/PageHeaderConfirmButton.swift new file mode 100644 index 0000000..18e06b7 --- /dev/null +++ b/OSGKeyboard/Views/Components/PageHeaderConfirmButton.swift @@ -0,0 +1,91 @@ +// PageHeaderConfirmButton.swift +// OSGKeyboard · Main App +// +// 圆形黑色图标按钮;确认框以 popover 从按钮向下展开(非底部 action sheet)。 + +import SwiftUI +import OSGKeyboardShared + +struct PageHeaderConfirmButton: View { + @Environment(\.themePalette) private var palette: ThemePalette + @Environment(\.colorScheme) private var colorScheme + + let systemImage: String + let accessibilityLabel: LocalizedStringKey + let confirmTitle: LocalizedStringKey + let confirmMessage: LocalizedStringKey + let confirmActionTitle: LocalizedStringKey + let onConfirm: () -> Void + + @State private var showConfirm = false + + private let buttonSize: CGFloat = 36 + + var body: some View { + Button { + showConfirm = true + } label: { + Image(systemName: systemImage) + .font(.system(size: 16, weight: .medium)) + .foregroundStyle(iconColor) + .frame(width: buttonSize, height: buttonSize) + .background(circleFill, in: Circle()) + .overlay(Circle().stroke(circleStroke, lineWidth: 0.5)) + } + .buttonStyle(.plain) + .accessibilityLabel(Text(accessibilityLabel)) + .popover(isPresented: $showConfirm, arrowEdge: .top) { + confirmPopover + .presentationCompactAdaptation(.popover) + } + } + + /// 浅色模式:更亮的圆底 + 黑色图标;深色模式:抬升表面色 + 浅色图标。 + private var circleFill: Color { + switch colorScheme { + case .dark: + return palette.surfaceElevated + default: + return Color.white + } + } + + private var circleStroke: Color { + colorScheme == .dark ? palette.dividerStrong : palette.divider + } + + private var iconColor: Color { + colorScheme == .dark ? palette.textPrimary : .black + } + + private var confirmPopover: some View { + VStack(alignment: .leading, spacing: Spacing.md) { + Text(confirmTitle) + .font(TypeStyle.headline) + .foregroundStyle(palette.textPrimary) + + Text(confirmMessage) + .font(TypeStyle.footnote) + .foregroundStyle(palette.textSecondary) + .fixedSize(horizontal: false, vertical: true) + + HStack(spacing: Spacing.sm) { + Spacer(minLength: 0) + Button(LocalizedStringKey("common.cancel")) { + showConfirm = false + } + .font(TypeStyle.bodyEmph) + .foregroundStyle(palette.textSecondary) + + Button(confirmActionTitle) { + showConfirm = false + onConfirm() + } + .font(TypeStyle.bodyEmph) + .foregroundStyle(palette.danger) + } + } + .padding(Spacing.md) + .frame(minWidth: 260) + } +} diff --git a/OSGKeyboard/Views/Components/PageHeaderRow.swift b/OSGKeyboard/Views/Components/PageHeaderRow.swift new file mode 100644 index 0000000..717fe7a --- /dev/null +++ b/OSGKeyboard/Views/Components/PageHeaderRow.swift @@ -0,0 +1,37 @@ +// PageHeaderRow.swift +// OSGKeyboard · Main App +// +// 左对齐页面标题 + 同行右侧操作区。不用 navigation toolbar 放标题, +// 避免 iOS 把 leading/trailing 项挤进「…」溢出菜单。 + +import SwiftUI +import OSGKeyboardShared + +struct PageHeaderRow: View { + @Environment(\.themePalette) private var palette: ThemePalette + + let title: LocalizedStringKey + @ViewBuilder var trailing: () -> Trailing + + var body: some View { + HStack(alignment: .center, spacing: Spacing.sm) { + Text(title) + .font(TypeStyle.title2) + .foregroundStyle(palette.textPrimary) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + + trailing() + } + .padding(.horizontal, Spacing.md) + .padding(.top, Spacing.md) + .padding(.bottom, Spacing.sm) + } +} + +extension PageHeaderRow where Trailing == EmptyView { + init(title: LocalizedStringKey) { + self.title = title + self.trailing = { EmptyView() } + } +} diff --git a/OSGKeyboard/Views/HistoryView.swift b/OSGKeyboard/Views/HistoryView.swift index c6660b5..6351a2a 100644 --- a/OSGKeyboard/Views/HistoryView.swift +++ b/OSGKeyboard/Views/HistoryView.swift @@ -7,7 +7,6 @@ import OSGKeyboardShared struct HistoryView: View { @Environment(\.themePalette) private var palette: ThemePalette @ObservedObject private var store = SpeechHistoryStore.shared - @State private var showClearConfirm = false private static let dayFormatter: DateFormatter = { let f = DateFormatter() @@ -25,13 +24,27 @@ struct HistoryView: View { var body: some View { NavigationStack { - ZStack { - palette.background.ignoresSafeArea() + VStack(spacing: 0) { + PageHeaderRow(title: "history.title") { + if !store.entries.isEmpty { + PageHeaderConfirmButton( + systemImage: "trash", + accessibilityLabel: "history.clear.button", + confirmTitle: "history.clear.title", + confirmMessage: "history.clear.message", + confirmActionTitle: "history.clear.confirm" + ) { + store.clearAll() + } + } + } - if store.entries.isEmpty { - emptyState - } else { - VStack(spacing: 0) { + ZStack { + palette.background.ignoresSafeArea() + + if store.entries.isEmpty { + emptyState + } else { ScrollView { LazyVStack(alignment: .leading, spacing: Spacing.xl) { ForEach(store.groupedByDay, id: \.day) { group in @@ -42,25 +55,11 @@ struct HistoryView: View { .padding(.vertical, Spacing.md) .padding(.bottom, 100) } - - clearFooter } } } - .navigationTitle(LocalizedStringKey("history.title")) - .navigationBarTitleDisplayMode(.inline) - } - .confirmationDialog( - LocalizedStringKey("history.clear.title"), - isPresented: $showClearConfirm, - titleVisibility: .visible - ) { - Button(LocalizedStringKey("history.clear.confirm"), role: .destructive) { - store.clearAll() - } - Button(LocalizedStringKey("common.cancel"), role: .cancel) {} - } message: { - Text("history.clear.message") + .background(palette.background) + .toolbar(.hidden, for: .navigationBar) } } @@ -116,18 +115,4 @@ struct HistoryView: View { .padding(Spacing.md) .frame(maxWidth: .infinity, alignment: .leading) } - - private var clearFooter: some View { - Button(role: .destructive) { - showClearConfirm = true - } label: { - Text("history.clear.button") - .font(TypeStyle.caption) - .foregroundStyle(palette.danger) - .frame(maxWidth: .infinity, minHeight: 44) - } - .buttonStyle(.plain) - .padding(.horizontal, Spacing.md) - .padding(.bottom, Spacing.sm) - } } diff --git a/OSGKeyboard/Views/SettingsView.swift b/OSGKeyboard/Views/SettingsView.swift index 021822b..787276b 100644 --- a/OSGKeyboard/Views/SettingsView.swift +++ b/OSGKeyboard/Views/SettingsView.swift @@ -18,7 +18,6 @@ struct SettingsView: View { @ObservedObject var config = ProviderConfig.shared @Environment(\.dismiss) private var dismiss - @State private var showResetConfirm = false @State private var safariURL: URL? let presentation: SettingsPresentation @@ -32,57 +31,57 @@ struct SettingsView: View { var body: some View { NavigationStack { - ZStack { - palette.background.ignoresSafeArea() - ScrollView { - VStack(spacing: Spacing.md) { - engineSection - if config.engineMode == "cloud" { - providerSection - apiSection + VStack(spacing: 0) { + PageHeaderRow(title: "settings.title") { + HStack(spacing: Spacing.xs) { + PageHeaderConfirmButton( + systemImage: "arrow.counterclockwise", + accessibilityLabel: "settings.reset.confirm", + confirmTitle: "settings.reset.title", + confirmMessage: "settings.reset.message", + confirmActionTitle: "common.reset" + ) { + config.reset() } - languageSection - if config.engineMode == "cloud" { - promptSection + if presentation == .sheet { + Button("common.done") { dismiss() } + .font(TypeStyle.headline) + .foregroundStyle(palette.accent) + .frame(minHeight: 44) } - if presentation == .tab { - footerLinks - } - resetButton } - .padding(.horizontal, Spacing.md) - .padding(.vertical, Spacing.md) - .padding(.bottom, presentation == .tab ? 100 : Spacing.lg) + } + + ZStack { + palette.background.ignoresSafeArea() + ScrollView { + VStack(spacing: Spacing.md) { + engineSection + if config.engineMode == "cloud" { + providerSection + apiSection + } + languageSection + if config.engineMode == "cloud" { + promptSection + } + if presentation == .tab { + footerLinks + } + } + .padding(.horizontal, Spacing.md) + .padding(.vertical, Spacing.md) + .padding(.bottom, presentation == .tab ? 100 : Spacing.lg) + } } } - .navigationTitle(LocalizedStringKey("settings.title")) - .navigationBarTitleDisplayMode(.inline) + .background(palette.background) + .toolbar(.hidden, for: .navigationBar) .task { await loadDynamicLocales() } - .toolbar { - if presentation == .sheet { - ToolbarItem(placement: .confirmationAction) { - Button("common.done") { dismiss() } - .font(TypeStyle.headline) - .foregroundStyle(palette.accent) - } - } - } .sheet(item: $safariURL) { url in SafariSheet(url: url) } } - .confirmationDialog( - LocalizedStringKey("settings.reset.title"), - isPresented: $showResetConfirm, - titleVisibility: .visible - ) { - Button(LocalizedStringKey("common.reset"), role: .destructive) { - config.reset() - } - Button(LocalizedStringKey("common.cancel"), role: .cancel) {} - } message: { - Text("settings.reset.message") - } } // MARK: - Engine @@ -313,21 +312,6 @@ struct SettingsView: View { .buttonStyle(.plain) } - // MARK: - Reset - - private var resetButton: some View { - Button(role: .destructive) { - showResetConfirm = true - } label: { - Text("settings.reset.confirm") - .font(TypeStyle.caption) - .foregroundStyle(palette.danger) - .frame(maxWidth: .infinity, minHeight: 36) - } - .buttonStyle(.plain) - .padding(.top, presentation == .tab ? Spacing.xs : Spacing.sm) - } - // MARK: - Header private func sectionHeader(_ title: LocalizedStringKey) -> some View { diff --git a/OSGKeyboardExt/PrivacyInfo.xcprivacy b/OSGKeyboardExt/PrivacyInfo.xcprivacy index 19a8d70..93cac16 100644 --- a/OSGKeyboardExt/PrivacyInfo.xcprivacy +++ b/OSGKeyboardExt/PrivacyInfo.xcprivacy @@ -4,7 +4,7 @@ NSPrivacyTracking - NSPrivacyCollectedUsageTypes + NSPrivacyCollectedDataTypes NSPrivacyAccessedAPITypes @@ -12,14 +12,10 @@ NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - CA92.1 - - - NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryActiveKeyboards - NSPrivacyAccessedAPITypeReasons - DDA9.1 + + CA92.1 + - \ No newline at end of file + diff --git a/OSGKeyboardExt/Views/KeyboardRootView.swift b/OSGKeyboardExt/Views/KeyboardRootView.swift index 961d858..6aebaa8 100644 --- a/OSGKeyboardExt/Views/KeyboardRootView.swift +++ b/OSGKeyboardExt/Views/KeyboardRootView.swift @@ -57,16 +57,13 @@ public struct KeyboardRootView: View { } .padding(.top, 4) .padding(.bottom, 6) - .background(keyboardBackground) + // 透明背景:让系统键盘 chrome 透出,不自行铺色(深浅模式一致)。 + .background(Color.clear) .frame(height: Self.totalHeight) // Feed the resolved palette to all nested chips/buttons. .environment(\.themePalette, palette) } - private var keyboardBackground: Color { - colorScheme == .dark ? palette.background : Color.clear - } - // MARK: - Top bar private var topBar: some View {