feat(ui): simplify keyboard chips and reorganize settings preferences
- Remove polish scenario and ASR locale chips from keyboard top bar - Fix polish intensity labels via SharedL10n (Shared.strings table) - Merge polish intensity and personal dictionary into preferences card - Personal dictionary: no list icon; detail view uses nav bar + back Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
@@ -24,41 +24,51 @@ import OSGKeyboardShared
|
|||||||
@MainActor
|
@MainActor
|
||||||
struct PersonalDictionaryView: View {
|
struct PersonalDictionaryView: View {
|
||||||
@Environment(\.themePalette) private var palette: ThemePalette
|
@Environment(\.themePalette) private var palette: ThemePalette
|
||||||
@Environment(\.dismiss) private var dismiss
|
@ObservedObject private var config = ProviderConfig.shared
|
||||||
|
|
||||||
@State private var dictionary: PersonalDictionary = AppGroupStore().personalDictionary
|
@State private var dictionary: PersonalDictionary = AppGroupStore().personalDictionary
|
||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
|
@State private var showClearAllConfirmation = false
|
||||||
|
|
||||||
private let store = AppGroupStore()
|
private let store = AppGroupStore()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
Group {
|
||||||
VStack(spacing: 0) {
|
if dictionary.entries.isEmpty {
|
||||||
PageHeaderRow(title: "settings.personalDictionary.title") {
|
emptyState
|
||||||
if !dictionary.entries.isEmpty {
|
} else {
|
||||||
PageHeaderConfirmButton(
|
list
|
||||||
systemImage: "trash",
|
}
|
||||||
accessibilityLabel: "settings.personalDictionary.clearAll",
|
}
|
||||||
confirmTitle: "settings.personalDictionary.clearAll.confirmTitle",
|
.background(palette.background.ignoresSafeArea())
|
||||||
confirmMessage: "settings.personalDictionary.clearAll.message",
|
.navigationTitle("settings.personalDictionary.title")
|
||||||
confirmActionTitle: "settings.personalDictionary.clearAll.confirm"
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
) {
|
.toolbar(.visible, for: .navigationBar)
|
||||||
clearAll()
|
.toolbar {
|
||||||
}
|
if !dictionary.entries.isEmpty {
|
||||||
}
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
}
|
Button {
|
||||||
|
showClearAllConfirmation = true
|
||||||
ZStack {
|
} label: {
|
||||||
palette.background.ignoresSafeArea()
|
Image(systemName: "trash")
|
||||||
if dictionary.entries.isEmpty {
|
|
||||||
emptyState
|
|
||||||
} else {
|
|
||||||
list
|
|
||||||
}
|
}
|
||||||
|
.accessibilityLabel(AppL10n.string("settings.personalDictionary.clearAll"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(palette.background)
|
}
|
||||||
.toolbar(.hidden, for: .navigationBar)
|
.toolbarBackground(palette.background, for: .navigationBar)
|
||||||
|
.toolbarBackground(.visible, for: .navigationBar)
|
||||||
|
.confirmationDialog(
|
||||||
|
AppL10n.string("settings.personalDictionary.clearAll.confirmTitle"),
|
||||||
|
isPresented: $showClearAllConfirmation,
|
||||||
|
titleVisibility: .visible
|
||||||
|
) {
|
||||||
|
Button(AppL10n.string("settings.personalDictionary.clearAll.confirm"), role: .destructive) {
|
||||||
|
clearAll()
|
||||||
|
}
|
||||||
|
Button(AppL10n.string("common.cancel"), role: .cancel) {}
|
||||||
|
} message: {
|
||||||
|
Text("settings.personalDictionary.clearAll.message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +113,7 @@ struct PersonalDictionaryView: View {
|
|||||||
|
|
||||||
private func section(for category: PersonalDictionary.Entry.Category, items: [PersonalDictionary.Entry]) -> some View {
|
private func section(for category: PersonalDictionary.Entry.Category, items: [PersonalDictionary.Entry]) -> some View {
|
||||||
VStack(alignment: .leading, spacing: Spacing.sm) {
|
VStack(alignment: .leading, spacing: Spacing.sm) {
|
||||||
Text(LocalizedStringKey(category.labelKey))
|
Text(SharedL10n.string(category.labelKey, language: config.uiLanguage))
|
||||||
.font(TypeStyle.caption2)
|
.font(TypeStyle.caption2)
|
||||||
.foregroundStyle(palette.textSecondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
@@ -131,7 +141,7 @@ struct PersonalDictionaryView: View {
|
|||||||
.foregroundStyle(palette.textPrimary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Text(LocalizedStringKey(entry.source.labelKey))
|
Text(SharedL10n.string(entry.source.labelKey, language: config.uiLanguage))
|
||||||
.font(TypeStyle.caption2)
|
.font(TypeStyle.caption2)
|
||||||
.foregroundStyle(palette.textTertiary)
|
.foregroundStyle(palette.textTertiary)
|
||||||
if entry.usageCount > 1 {
|
if entry.usageCount > 1 {
|
||||||
@@ -231,7 +241,9 @@ struct PersonalDictionaryView: View {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
#Preview {
|
#Preview {
|
||||||
ThemedRoot {
|
ThemedRoot {
|
||||||
PersonalDictionaryView()
|
NavigationStack {
|
||||||
|
PersonalDictionaryView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -74,11 +74,9 @@ struct SettingsView: View {
|
|||||||
providerSection
|
providerSection
|
||||||
apiSection
|
apiSection
|
||||||
}
|
}
|
||||||
polishIntensitySection
|
|
||||||
if config.engineMode == "local" {
|
if config.engineMode == "local" {
|
||||||
localEngineSettingsSection
|
localEngineSettingsSection
|
||||||
}
|
}
|
||||||
personalDictionaryLinkSection
|
|
||||||
if presentation == .tab {
|
if presentation == .tab {
|
||||||
preferencesSection
|
preferencesSection
|
||||||
footerLinks
|
footerLinks
|
||||||
@@ -268,6 +266,19 @@ struct SettingsView: View {
|
|||||||
set: { config.handednessPreference = $0 }
|
set: { config.handednessPreference = $0 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Divider().background(palette.divider)
|
||||||
|
|
||||||
|
polishIntensityPreferenceRows
|
||||||
|
|
||||||
|
Divider().background(palette.divider)
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
PersonalDictionaryView()
|
||||||
|
} label: {
|
||||||
|
personalDictionaryPreferenceRow
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||||
.overlay(
|
.overlay(
|
||||||
@@ -277,74 +288,50 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Polish intensity (v0.3.0)
|
private var polishIntensityPreferenceRows: some View {
|
||||||
|
VStack(alignment: .leading, spacing: Spacing.sm) {
|
||||||
private var polishIntensitySection: some View {
|
Text("settings.polishIntensity.title")
|
||||||
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
.font(TypeStyle.body)
|
||||||
sectionHeader("settings.polishIntensity.title")
|
.foregroundStyle(palette.textPrimary)
|
||||||
VStack(spacing: 0) {
|
|
||||||
Picker("", selection: $config.polishIntensity) {
|
|
||||||
ForEach(PolishIntensity.allCases, id: \.self) { intensity in
|
|
||||||
Text(LocalizedStringKey(intensity.labelKey))
|
|
||||||
.tag(intensity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.segmented)
|
|
||||||
.padding(.horizontal, Spacing.md)
|
.padding(.horizontal, Spacing.md)
|
||||||
.padding(.vertical, Spacing.sm)
|
.padding(.top, Spacing.sm)
|
||||||
|
|
||||||
Text(LocalizedStringKey(config.polishIntensity.descriptionKey))
|
Picker("", selection: $config.polishIntensity) {
|
||||||
.font(TypeStyle.caption2)
|
ForEach(PolishIntensity.allCases, id: \.self) { intensity in
|
||||||
.foregroundStyle(palette.textSecondary)
|
Text(SharedL10n.string(intensity.labelKey, language: config.uiLanguage))
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.tag(intensity)
|
||||||
.padding(.horizontal, Spacing.md)
|
}
|
||||||
.padding(.bottom, Spacing.sm)
|
|
||||||
}
|
}
|
||||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
.pickerStyle(.segmented)
|
||||||
.overlay(
|
.padding(.horizontal, Spacing.md)
|
||||||
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
|
|
||||||
.stroke(palette.divider, lineWidth: 0.5)
|
Text(SharedL10n.string(config.polishIntensity.descriptionKey, language: config.uiLanguage))
|
||||||
)
|
.font(TypeStyle.caption2)
|
||||||
|
.foregroundStyle(palette.textSecondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, Spacing.md)
|
||||||
|
.padding(.bottom, Spacing.sm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Personal dictionary (v0.3.0)
|
private var personalDictionaryPreferenceRow: some View {
|
||||||
|
HStack(spacing: Spacing.sm) {
|
||||||
private var personalDictionaryLinkSection: some View {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
Text("settings.personalDictionary.title")
|
||||||
sectionHeader("settings.personalDictionary.sectionTitle")
|
.font(TypeStyle.body)
|
||||||
VStack(spacing: 0) {
|
.foregroundStyle(palette.textPrimary)
|
||||||
NavigationLink {
|
Text("settings.personalDictionary.summary")
|
||||||
PersonalDictionaryView()
|
.font(TypeStyle.caption2)
|
||||||
} label: {
|
.foregroundStyle(palette.textSecondary)
|
||||||
HStack(spacing: Spacing.sm) {
|
|
||||||
MaterialIcon(name: .bookmark, size: 18)
|
|
||||||
.foregroundStyle(palette.accent)
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text("settings.personalDictionary.title")
|
|
||||||
.font(TypeStyle.body)
|
|
||||||
.foregroundStyle(palette.textPrimary)
|
|
||||||
Text("settings.personalDictionary.summary")
|
|
||||||
.font(TypeStyle.caption2)
|
|
||||||
.foregroundStyle(palette.textSecondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "chevron.right")
|
|
||||||
.font(.system(size: 14, weight: .semibold))
|
|
||||||
.foregroundStyle(palette.textTertiary)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, Spacing.md)
|
|
||||||
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
}
|
||||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
Spacer()
|
||||||
.overlay(
|
Image(systemName: "chevron.right")
|
||||||
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
|
.font(.system(size: 14, weight: .semibold))
|
||||||
.stroke(palette.divider, lineWidth: 0.5)
|
.foregroundStyle(palette.textTertiary)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, Spacing.md)
|
||||||
|
.frame(minHeight: SettingsListMetrics.doubleLineMinHeight)
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Footer links (tab settings only)
|
// MARK: - Footer links (tab settings only)
|
||||||
|
|||||||
@@ -140,17 +140,8 @@ public struct KeyboardRootView: View {
|
|||||||
} else {
|
} else {
|
||||||
CloudEngineChip()
|
CloudEngineChip()
|
||||||
}
|
}
|
||||||
if state.isPolishScenarioChipVisible {
|
// Polish scenario and ASR locale are configured in the main-app
|
||||||
ScenarioChip(state: state)
|
// Settings tab only — keep the keyboard top bar uncluttered.
|
||||||
}
|
|
||||||
LocaleChip(localeId: state.localeId) { newId in
|
|
||||||
state.setLocale(newId)
|
|
||||||
}
|
|
||||||
// v0.3.0: detected app context — the per-app polish mode.
|
|
||||||
// The chip mirrors `AppGroupStore.detectedAppContext` and
|
|
||||||
// writes overrides back so the next LLM call uses the new
|
|
||||||
// tone. Hidden during onboarding (the overlay reads better
|
|
||||||
// without chip clutter).
|
|
||||||
if state.hasCompletedOnboarding {
|
if state.hasCompletedOnboarding {
|
||||||
AppContextChip(state: state)
|
AppContextChip(state: state)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user