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 {