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.
This commit is contained in:
Rocky
2026-06-20 14:10:18 +08:00
parent db5151ad9a
commit 642c97c540
8 changed files with 213 additions and 112 deletions
+4 -2
View File
@@ -6,7 +6,7 @@
<false/> <false/>
<key>NSPrivacyTrackingDomains</key> <key>NSPrivacyTrackingDomains</key>
<array/> <array/>
<key>NSPrivacyCollectedUsageTypes</key> <key>NSPrivacyCollectedDataTypes</key>
<array/> <array/>
<key>NSPrivacyAccessedAPITypes</key> <key>NSPrivacyAccessedAPITypes</key>
<array> <array>
@@ -14,7 +14,9 @@
<key>NSPrivacyAccessedAPIType</key> <key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string> <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key> <key>NSPrivacyAccessedAPITypeReasons</key>
<string>CA92.1</string> <array>
<string>CA92.1</string>
</array>
</dict> </dict>
</array> </array>
</dict> </dict>
@@ -32,6 +32,7 @@ enum AppTab: Int, CaseIterable {
struct MinimalTabBar: View { struct MinimalTabBar: View {
@Environment(\.themePalette) private var palette: ThemePalette @Environment(\.themePalette) private var palette: ThemePalette
@Environment(\.colorScheme) private var colorScheme
@Binding var selection: AppTab @Binding var selection: AppTab
var body: some View { var body: some View {
@@ -44,7 +45,7 @@ struct MinimalTabBar: View {
name: tab.icon, name: tab.icon,
size: 24 size: 24
) )
.foregroundStyle(selection == tab ? palette.accent : palette.textTertiary) .foregroundStyle(tabIconColor(for: tab))
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: 48) .frame(height: 48)
.contentShape(Rectangle()) .contentShape(Rectangle())
@@ -61,4 +62,12 @@ struct MinimalTabBar: View {
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
.padding(.bottom, Spacing.xs) .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
}
} }
@@ -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)
}
}
@@ -0,0 +1,37 @@
// PageHeaderRow.swift
// OSGKeyboard · Main App
//
// + navigation toolbar
// iOS leading/trailing
import SwiftUI
import OSGKeyboardShared
struct PageHeaderRow<Trailing: View>: 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() }
}
}
+22 -37
View File
@@ -7,7 +7,6 @@ import OSGKeyboardShared
struct HistoryView: View { struct HistoryView: View {
@Environment(\.themePalette) private var palette: ThemePalette @Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject private var store = SpeechHistoryStore.shared @ObservedObject private var store = SpeechHistoryStore.shared
@State private var showClearConfirm = false
private static let dayFormatter: DateFormatter = { private static let dayFormatter: DateFormatter = {
let f = DateFormatter() let f = DateFormatter()
@@ -25,13 +24,27 @@ struct HistoryView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ZStack { VStack(spacing: 0) {
palette.background.ignoresSafeArea() 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 { ZStack {
emptyState palette.background.ignoresSafeArea()
} else {
VStack(spacing: 0) { if store.entries.isEmpty {
emptyState
} else {
ScrollView { ScrollView {
LazyVStack(alignment: .leading, spacing: Spacing.xl) { LazyVStack(alignment: .leading, spacing: Spacing.xl) {
ForEach(store.groupedByDay, id: \.day) { group in ForEach(store.groupedByDay, id: \.day) { group in
@@ -42,25 +55,11 @@ struct HistoryView: View {
.padding(.vertical, Spacing.md) .padding(.vertical, Spacing.md)
.padding(.bottom, 100) .padding(.bottom, 100)
} }
clearFooter
} }
} }
} }
.navigationTitle(LocalizedStringKey("history.title")) .background(palette.background)
.navigationBarTitleDisplayMode(.inline) .toolbar(.hidden, for: .navigationBar)
}
.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")
} }
} }
@@ -116,18 +115,4 @@ struct HistoryView: View {
.padding(Spacing.md) .padding(Spacing.md)
.frame(maxWidth: .infinity, alignment: .leading) .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)
}
} }
+41 -57
View File
@@ -18,7 +18,6 @@ struct SettingsView: View {
@ObservedObject var config = ProviderConfig.shared @ObservedObject var config = ProviderConfig.shared
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var showResetConfirm = false
@State private var safariURL: URL? @State private var safariURL: URL?
let presentation: SettingsPresentation let presentation: SettingsPresentation
@@ -32,57 +31,57 @@ struct SettingsView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ZStack { VStack(spacing: 0) {
palette.background.ignoresSafeArea() PageHeaderRow(title: "settings.title") {
ScrollView { HStack(spacing: Spacing.xs) {
VStack(spacing: Spacing.md) { PageHeaderConfirmButton(
engineSection systemImage: "arrow.counterclockwise",
if config.engineMode == "cloud" { accessibilityLabel: "settings.reset.confirm",
providerSection confirmTitle: "settings.reset.title",
apiSection confirmMessage: "settings.reset.message",
confirmActionTitle: "common.reset"
) {
config.reset()
} }
languageSection if presentation == .sheet {
if config.engineMode == "cloud" { Button("common.done") { dismiss() }
promptSection .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")) .background(palette.background)
.navigationBarTitleDisplayMode(.inline) .toolbar(.hidden, for: .navigationBar)
.task { await loadDynamicLocales() } .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 .sheet(item: $safariURL) { url in
SafariSheet(url: url) 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 // MARK: - Engine
@@ -313,21 +312,6 @@ struct SettingsView: View {
.buttonStyle(.plain) .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 // MARK: - Header
private func sectionHeader(_ title: LocalizedStringKey) -> some View { private func sectionHeader(_ title: LocalizedStringKey) -> some View {
+4 -8
View File
@@ -4,7 +4,7 @@
<dict> <dict>
<key>NSPrivacyTracking</key> <key>NSPrivacyTracking</key>
<false/> <false/>
<key>NSPrivacyCollectedUsageTypes</key> <key>NSPrivacyCollectedDataTypes</key>
<array/> <array/>
<key>NSPrivacyAccessedAPITypes</key> <key>NSPrivacyAccessedAPITypes</key>
<array> <array>
@@ -12,13 +12,9 @@
<key>NSPrivacyAccessedAPIType</key> <key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string> <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key> <key>NSPrivacyAccessedAPITypeReasons</key>
<string>CA92.1</string> <array>
</dict> <string>CA92.1</string>
<dict> </array>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<string>DDA9.1</string>
</dict> </dict>
</array> </array>
</dict> </dict>
+2 -5
View File
@@ -57,16 +57,13 @@ public struct KeyboardRootView: View {
} }
.padding(.top, 4) .padding(.top, 4)
.padding(.bottom, 6) .padding(.bottom, 6)
.background(keyboardBackground) // chrome
.background(Color.clear)
.frame(height: Self.totalHeight) .frame(height: Self.totalHeight)
// Feed the resolved palette to all nested chips/buttons. // Feed the resolved palette to all nested chips/buttons.
.environment(\.themePalette, palette) .environment(\.themePalette, palette)
} }
private var keyboardBackground: Color {
colorScheme == .dark ? palette.background : Color.clear
}
// MARK: - Top bar // MARK: - Top bar
private var topBar: some View { private var topBar: some View {