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:
@@ -7,12 +7,18 @@ import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
struct KeyboardSurfaceRoot: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@ObservedObject var state: KeyboardState
|
||||
@ObservedObject var typing: TypingSessionController
|
||||
|
||||
var onInsert: (String) -> Void
|
||||
var onDeleteBackward: () -> Void
|
||||
|
||||
private var palette: ThemePalette {
|
||||
colorScheme == .dark ? Palette.dark : Palette.light
|
||||
}
|
||||
|
||||
/// Height is deliberately independent of the surface: the voice surface
|
||||
/// adopts the typing surface's content-driven height and parks the surplus
|
||||
/// above its action cluster, so switching surfaces never resizes the
|
||||
@@ -26,29 +32,42 @@ struct KeyboardSurfaceRoot: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch state.surface {
|
||||
case .voice:
|
||||
KeyboardRootView(
|
||||
state: state,
|
||||
typing: typing,
|
||||
onInsert: onInsert
|
||||
)
|
||||
case .typing:
|
||||
TypingRootView(
|
||||
state: state,
|
||||
typing: typing,
|
||||
onInsert: onInsert,
|
||||
onDeleteBackward: onDeleteBackward
|
||||
)
|
||||
case .ai:
|
||||
AIKeyboardView(
|
||||
state: state,
|
||||
typing: typing,
|
||||
onInsert: onInsert
|
||||
)
|
||||
ZStack {
|
||||
Group {
|
||||
switch state.surface {
|
||||
case .voice:
|
||||
KeyboardRootView(
|
||||
state: state,
|
||||
typing: typing,
|
||||
onInsert: wrappedInsert
|
||||
)
|
||||
case .typing:
|
||||
TypingRootView(
|
||||
state: state,
|
||||
typing: typing,
|
||||
onInsert: wrappedInsert,
|
||||
onDeleteBackward: wrappedDeleteBackward
|
||||
)
|
||||
case .ai:
|
||||
AIKeyboardView(
|
||||
state: state,
|
||||
typing: typing,
|
||||
onInsert: wrappedInsert
|
||||
)
|
||||
}
|
||||
}
|
||||
.opacity(state.clipboardOverlay == .none ? 1 : 0)
|
||||
.allowsHitTesting(state.clipboardOverlay == .none)
|
||||
|
||||
// Match every surface's outer chrome so the panel title / X sit in
|
||||
// the same slot as the logo + clipboard chip (not 4 pt higher).
|
||||
clipboardOverlayLayer
|
||||
.padding(.top, TypingSurfaceMetrics.outerPaddingTop)
|
||||
.padding(.bottom, TypingSurfaceMetrics.outerPaddingBottom)
|
||||
}
|
||||
// Overlays are siblings of the surfaces, so the palette has to be
|
||||
// injected here or they fall back to the environment's dark default.
|
||||
.environment(\.themePalette, palette)
|
||||
.animation(.easeInOut(duration: 0.15), value: state.surface)
|
||||
.onChange(of: state.surface) { _, newSurface in
|
||||
if newSurface != .typing {
|
||||
@@ -56,4 +75,41 @@ struct KeyboardSurfaceRoot: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func wrappedInsert(_ text: String) {
|
||||
if !text.isEmpty {
|
||||
state.noteUserDidInputText()
|
||||
}
|
||||
onInsert(text)
|
||||
}
|
||||
|
||||
private func wrappedDeleteBackward() {
|
||||
state.noteUserDidInputText()
|
||||
onDeleteBackward()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var clipboardOverlayLayer: some View {
|
||||
switch state.clipboardOverlay {
|
||||
case .none:
|
||||
EmptyView()
|
||||
case .enableGuide:
|
||||
ClipboardEnableGuideView(
|
||||
onClose: state.dismissClipboardOverlay,
|
||||
onOpenSettings: {
|
||||
state.dismissClipboardOverlay()
|
||||
state.openClipboardSettings()
|
||||
}
|
||||
)
|
||||
case .historyPanel:
|
||||
ClipboardHistoryPanelView(
|
||||
history: ClipboardHistoryStore.shared,
|
||||
onClose: state.dismissClipboardOverlay,
|
||||
onClear: state.clearClipboardHistory,
|
||||
onInsert: { state.insertClipboardText($0) },
|
||||
onDelete: { state.deleteClipboardHistoryEntry($0) },
|
||||
pastePermissionHint: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,16 @@ struct TypingRootView: View {
|
||||
@ViewBuilder
|
||||
private var topRegion: some View {
|
||||
if hasCandidateContent {
|
||||
// Composing Chinese/English candidates hide the clipboard strip.
|
||||
candidateBar
|
||||
} else if let suggestion = state.clipboardSuggestionText, !suggestion.isEmpty {
|
||||
// Same slot as logo + capsule tabs — hide chrome until dismissed.
|
||||
ClipboardSuggestionBar(
|
||||
text: suggestion,
|
||||
onInsert: { state.insertClipboardText(suggestion) },
|
||||
onDismiss: state.dismissClipboardSuggestion
|
||||
)
|
||||
.padding(.horizontal, KeyboardTopBarMetrics.nestedHorizontalInset)
|
||||
} else {
|
||||
idleTopBar
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user