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/>
|
<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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user