feat(keyboard): add clipboard history and undo pastes
Ship optional keyboard clipboard history with settings, suggestion strip, and paste-permission guidance; let undo roll back clipboard inserts; bump build to 64.
This commit is contained in:
@@ -176,6 +176,12 @@ struct MainAppRoot: View {
|
||||
// The keyboard sends the user here precisely because typing
|
||||
// resources are missing — deploy without waiting for warmup.
|
||||
RimeDeploymentController.shared.deployNow(reason: "url.deployrime")
|
||||
case "settings":
|
||||
// Path may be `settings/clipboard` (host = settings, path = /clipboard).
|
||||
if url.path.contains("clipboard") {
|
||||
SettingsDeepLink.setPending(.clipboard)
|
||||
}
|
||||
NotificationCenter.default.post(name: .osgOpenSettingsDeepLink, object: nil)
|
||||
#if DEBUG
|
||||
case "seed-demo":
|
||||
DemoDataSeeder.seedRichPlaceholderData()
|
||||
|
||||
@@ -26,6 +26,9 @@ struct MainTabView: View {
|
||||
}
|
||||
.background(palette.background)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .osgOpenSettingsDeepLink)) { _ in
|
||||
tab = .settings
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Phone layout
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
// MARK: - Navigation row (title + optional summary subtitle)
|
||||
// MARK: - Navigation row (title + optional trailing summary before chevron)
|
||||
|
||||
struct SettingsNavigationRow: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
@@ -32,24 +32,24 @@ struct SettingsNavigationRow: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: Spacing.sm) {
|
||||
VStack(alignment: .leading, spacing: Spacing.xxs) {
|
||||
if let resolvedTitle {
|
||||
Text(resolvedTitle)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
} else if let localizedTitle {
|
||||
Text(localizedTitle)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
}
|
||||
if let subtitle, !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
if let resolvedTitle {
|
||||
Text(resolvedTitle)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
} else if let localizedTitle {
|
||||
Text(localizedTitle)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
}
|
||||
Spacer(minLength: Spacing.xs)
|
||||
// Trailing summary — same slot as SettingsVersionRow's value.
|
||||
if let subtitle, !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
@@ -306,6 +306,98 @@ struct AIAgentSettingsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Clipboard
|
||||
|
||||
struct ClipboardSettingsView: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
@ObservedObject var config: ProviderConfig
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
CardPageContent {
|
||||
CardSection("settings.clipboard.section") {
|
||||
VStack(spacing: 0) {
|
||||
Toggle(isOn: $config.clipboardHistoryEnabled) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("settings.clipboard.history.title")
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
Text("settings.clipboard.history.footer")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
}
|
||||
.tint(palette.accent)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
Toggle(isOn: clipboardCandidateBinding) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("settings.clipboard.candidate.title")
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
Text("settings.clipboard.candidate.footer")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
}
|
||||
}
|
||||
.tint(palette.accent)
|
||||
.disabled(!config.clipboardHistoryEnabled)
|
||||
.opacity(config.clipboardHistoryEnabled ? 1 : 0.45)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.surfaceCard()
|
||||
}
|
||||
|
||||
// iOS asks per read unless the user flips the durable
|
||||
// "Paste from Other Apps" permission to Allow.
|
||||
CardSection("settings.clipboard.paste.section") {
|
||||
VStack(spacing: 0) {
|
||||
Text("settings.clipboard.paste.body")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
Button {
|
||||
AppPermissions.openSystemSettings()
|
||||
} label: {
|
||||
SettingsNavigationRow(
|
||||
titleText: AppL10n.string(
|
||||
"settings.clipboard.paste.open",
|
||||
language: config.uiLanguage
|
||||
)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.surfaceCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(palette.background.ignoresSafeArea())
|
||||
.navigationTitle(AppL10n.string("settings.clipboard.title", language: config.uiLanguage))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.hidesTabBarWhenPushed()
|
||||
.onChange(of: config.clipboardHistoryEnabled) { _, enabled in
|
||||
if !enabled {
|
||||
config.clipboardCandidateBarEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var clipboardCandidateBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { config.clipboardHistoryEnabled && config.clipboardCandidateBarEnabled },
|
||||
set: { config.clipboardCandidateBarEnabled = $0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - About
|
||||
|
||||
struct AboutSettingsView: View {
|
||||
|
||||
@@ -22,6 +22,7 @@ private enum SettingsRoute: Hashable {
|
||||
case textPolish
|
||||
case general
|
||||
case aiAgent
|
||||
case clipboard
|
||||
case about
|
||||
}
|
||||
|
||||
@@ -96,9 +97,21 @@ struct SettingsView: View {
|
||||
settingsDestination(for: route)
|
||||
}
|
||||
.task { await loadDynamicLocales() }
|
||||
.onAppear { consumeClipboardDeepLinkIfNeeded() }
|
||||
.onReceive(NotificationCenter.default.publisher(for: .osgOpenSettingsDeepLink)) { _ in
|
||||
consumeClipboardDeepLinkIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func consumeClipboardDeepLinkIfNeeded() {
|
||||
guard SettingsDeepLink.consumePending() == .clipboard else { return }
|
||||
if !path.isEmpty {
|
||||
path = NavigationPath()
|
||||
}
|
||||
path.append(SettingsRoute.clipboard)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func settingsDestination(for route: SettingsRoute) -> some View {
|
||||
switch route {
|
||||
@@ -110,6 +123,8 @@ struct SettingsView: View {
|
||||
GeneralSettingsView(config: config)
|
||||
case .aiAgent:
|
||||
AIAgentSettingsView(config: config)
|
||||
case .clipboard:
|
||||
ClipboardSettingsView(config: config)
|
||||
case .about:
|
||||
AboutSettingsView(config: config)
|
||||
}
|
||||
@@ -135,6 +150,14 @@ struct SettingsView: View {
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
settingsRouteButton(
|
||||
.clipboard,
|
||||
title: "settings.clipboard.title",
|
||||
subtitle: clipboardSettingsSubtitle
|
||||
)
|
||||
|
||||
Divider().background(palette.divider)
|
||||
|
||||
LocalePickerRow(
|
||||
locales: effectiveLocales,
|
||||
selection: Binding(
|
||||
@@ -191,6 +214,13 @@ struct SettingsView: View {
|
||||
.surfaceCard()
|
||||
}
|
||||
|
||||
private var clipboardSettingsSubtitle: String {
|
||||
if config.clipboardHistoryEnabled {
|
||||
return AppL10n.string("settings.clipboard.subtitle.on", language: config.uiLanguage)
|
||||
}
|
||||
return AppL10n.string("settings.clipboard.subtitle.off", language: config.uiLanguage)
|
||||
}
|
||||
|
||||
private func settingsRouteButton(
|
||||
_ route: SettingsRoute,
|
||||
title: LocalizedStringKey,
|
||||
|
||||
@@ -200,6 +200,17 @@
|
||||
"settings.aiAgent.title" = "AI Agent";
|
||||
"settings.aiAgent.responseLength.section" = "Answers";
|
||||
"settings.aiAgent.responseLength.title" = "Response length";
|
||||
"settings.clipboard.title" = "Clipboard";
|
||||
"settings.clipboard.section" = "Features";
|
||||
"settings.clipboard.subtitle.on" = "On";
|
||||
"settings.clipboard.subtitle.off" = "Off";
|
||||
"settings.clipboard.history.title" = "History";
|
||||
"settings.clipboard.history.footer" = "Keeps the latest 15 plain-text copies on this device.";
|
||||
"settings.clipboard.candidate.title" = "Suggestion strip";
|
||||
"settings.clipboard.candidate.footer" = "Shows the newest copy above the keys for one-tap insert.";
|
||||
"settings.clipboard.paste.section" = "System access";
|
||||
"settings.clipboard.paste.body" = "Set Paste from Other Apps to Allow to stop paste prompts. If the option is missing, copy text and tap the keyboard suggestion once first.";
|
||||
"settings.clipboard.paste.open" = "Open iOS Settings";
|
||||
"settings.typingInput.title" = "Text Input";
|
||||
"settings.typingInput.default.title" = "Default to Text Input";
|
||||
"settings.typingInput.default.description" = "Open the text input keyboard instead of voice input by default";
|
||||
|
||||
@@ -200,6 +200,17 @@
|
||||
"settings.aiAgent.title" = "AI Agent";
|
||||
"settings.aiAgent.responseLength.section" = "回答";
|
||||
"settings.aiAgent.responseLength.title" = "回复篇幅";
|
||||
"settings.clipboard.title" = "剪贴板";
|
||||
"settings.clipboard.section" = "功能";
|
||||
"settings.clipboard.subtitle.on" = "已开启";
|
||||
"settings.clipboard.subtitle.off" = "已关闭";
|
||||
"settings.clipboard.history.title" = "历史记录";
|
||||
"settings.clipboard.history.footer" = "本机保存最近 15 条纯文本。";
|
||||
"settings.clipboard.candidate.title" = "建议条";
|
||||
"settings.clipboard.candidate.footer" = "最新复制显示在键盘上方,点一下即可插入。";
|
||||
"settings.clipboard.paste.section" = "系统授权";
|
||||
"settings.clipboard.paste.body" = "将「从其他 App 粘贴」设为「允许」,即可不再弹窗。若没有此项,先复制文字并点一次键盘建议条。";
|
||||
"settings.clipboard.paste.open" = "打开系统设置";
|
||||
"settings.typingInput.title" = "文本输入";
|
||||
"settings.typingInput.default.title" = "默认进行文字输入";
|
||||
"settings.typingInput.default.description" = "打开键盘时,默认使用文字输入键盘而非语音输入";
|
||||
|
||||
Reference in New Issue
Block a user