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:
@@ -6,7 +6,7 @@
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedUsageTypes</key>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
@@ -14,7 +14,9 @@
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
palette.background.ignoresSafeArea()
|
||||
|
||||
if store.entries.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,6 +31,27 @@ struct SettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
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()
|
||||
}
|
||||
if presentation == .sheet {
|
||||
Button("common.done") { dismiss() }
|
||||
.font(TypeStyle.headline)
|
||||
.foregroundStyle(palette.accent)
|
||||
.frame(minHeight: 44)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
palette.background.ignoresSafeArea()
|
||||
ScrollView {
|
||||
@@ -48,41 +68,20 @@ struct SettingsView: View {
|
||||
if presentation == .tab {
|
||||
footerLinks
|
||||
}
|
||||
resetButton
|
||||
}
|
||||
.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 {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedUsageTypes</key>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
@@ -12,13 +12,9 @@
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryActiveKeyboards</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<string>DDA9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user