2bc8c1b87d
Adapt typing/voice surfaces for iPad width and height, add the system globe key and last-input editing flow, harden host-only Rime deployment, and remove clipboard voice commands. Bump build to 61.
254 lines
8.5 KiB
Swift
254 lines
8.5 KiB
Swift
// LastInputEditView.swift
|
|
// OSGKeyboard · Keyboard Extension
|
|
|
|
import SwiftUI
|
|
import OSGKeyboardShared
|
|
|
|
struct LastInputEditView: View {
|
|
private enum Layout {
|
|
static let primaryButtonHeight: CGFloat = 50
|
|
static let primaryButtonWidth: CGFloat = primaryButtonHeight * 3
|
|
}
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
@ObservedObject var state: KeyboardState
|
|
@State private var selectedPage: Int? = 0
|
|
|
|
private var palette: ThemePalette {
|
|
colorScheme == .dark ? Palette.dark : Palette.light
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
topBar.frame(height: KeyboardTopBarMetrics.height)
|
|
VStack(spacing: 0) {
|
|
ZStack(alignment: .bottom) {
|
|
pages
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
VStack(spacing: 0) {
|
|
statusLine.frame(height: 18)
|
|
pageIndicator.frame(height: 12)
|
|
}
|
|
.allowsHitTesting(false)
|
|
}
|
|
.frame(height: 174)
|
|
.contentShape(Rectangle())
|
|
.simultaneousGesture(reviewSwipeGesture)
|
|
primaryRow.frame(height: 55)
|
|
}
|
|
.frame(maxWidth: KeyboardChromeLayout.voiceContentMaxWidth)
|
|
}
|
|
.padding(.vertical, 4)
|
|
.padding(.horizontal, KeyboardChromeLayout.horizontalInset)
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: KeyboardChromeLayout.totalHeight)
|
|
.environment(\.themePalette, palette)
|
|
.onChange(of: state.editSession) { _, newValue in
|
|
guard newValue.review != nil else {
|
|
selectedPage = 0
|
|
return
|
|
}
|
|
if reduceMotion {
|
|
selectedPage = 1
|
|
} else {
|
|
withAnimation(.interactiveSpring(response: 0.34, dampingFraction: 0.86)) {
|
|
selectedPage = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var topBar: some View {
|
|
HStack {
|
|
KeyboardBrandLogo(action: state.openSettings)
|
|
Spacer(minLength: 0)
|
|
KeyboardCancelButton(
|
|
action: state.closeEditMode,
|
|
accessibilityLabel: ExtL10n.text("keyboard.edit.close"),
|
|
accessibilityHint: ExtL10n.text("keyboard.edit.closeHint")
|
|
)
|
|
}
|
|
.padding(.horizontal, KeyboardTopBarMetrics.nestedHorizontalInset)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var pages: some View {
|
|
if let source = state.editSession.source {
|
|
EditTextPager(
|
|
originalTitle: ExtL10n.string("keyboard.edit.page.original"),
|
|
originalText: source.reference.displayText,
|
|
editedTitle: ExtL10n.string("keyboard.edit.page.edited"),
|
|
editedText: state.editSession.review?.resultText,
|
|
contentBottomInset: 30,
|
|
selectedPage: $selectedPage
|
|
)
|
|
}
|
|
}
|
|
|
|
private var statusLine: some View {
|
|
Text(editTranscript)
|
|
.font(TypeStyle.caption)
|
|
.foregroundStyle(isFailure ? palette.warning : palette.textPrimary)
|
|
.lineLimit(1)
|
|
.truncationMode(.head)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
private var pageIndicator: some View {
|
|
HStack(spacing: 5) {
|
|
Circle()
|
|
.fill(selectedPage != 1 ? palette.textPrimary : palette.textTertiary.opacity(0.45))
|
|
.frame(width: 5, height: 5)
|
|
Circle()
|
|
.fill(selectedPage == 1 ? palette.textPrimary : palette.textTertiary.opacity(0.45))
|
|
.frame(width: 5, height: 5)
|
|
}
|
|
.opacity(state.editSession.review == nil ? 0 : 1)
|
|
.accessibilityHidden(true)
|
|
}
|
|
|
|
private var primaryRow: some View {
|
|
HStack(spacing: Spacing.sm) {
|
|
helperText(leftHelper)
|
|
Button(action: primaryAction) {
|
|
ZStack {
|
|
Capsule().fill(palette.accent)
|
|
if case .listening = state.editSession {
|
|
Capsule()
|
|
.stroke(Color.white.opacity(0.28), lineWidth: 1.5)
|
|
.scaleEffect(1 + min(max(state.level, 0), 1) * 0.08)
|
|
.animation(Motion.soft, value: state.level)
|
|
}
|
|
primaryIcon
|
|
}
|
|
.frame(
|
|
width: Layout.primaryButtonWidth,
|
|
height: Layout.primaryButtonHeight
|
|
)
|
|
.contentShape(Capsule())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(primaryDisabled)
|
|
.accessibilityLabel(Text(primaryAccessibilityLabel))
|
|
helperText(rightHelper)
|
|
}
|
|
}
|
|
|
|
private func helperText(_ value: String) -> some View {
|
|
Text(value)
|
|
.font(TypeStyle.caption)
|
|
.foregroundStyle(palette.textSecondary.opacity(0.55))
|
|
.multilineTextAlignment(.center)
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.75)
|
|
.frame(maxWidth: .infinity)
|
|
.contentShape(Rectangle())
|
|
.simultaneousGesture(reviewSwipeGesture)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var primaryIcon: some View {
|
|
switch state.editSession {
|
|
case .preparing, .processing, .applying, .appending:
|
|
ProgressView().tint(.white)
|
|
case .review:
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 21, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
case .listening:
|
|
WaveformView(
|
|
level: state.level,
|
|
barCount: 7,
|
|
color: .white,
|
|
active: true
|
|
)
|
|
.frame(width: 35, height: 22)
|
|
.clipped()
|
|
default:
|
|
Image(systemName: "mic.fill")
|
|
.font(.system(size: 21, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
}
|
|
}
|
|
|
|
private var primaryDisabled: Bool {
|
|
switch state.editSession {
|
|
case .preparing, .processing, .applying, .appending:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func primaryAction() {
|
|
switch state.editSession {
|
|
case .listening:
|
|
state.stopEditListening()
|
|
case .review:
|
|
state.confirmEditResult()
|
|
case .failed:
|
|
state.beginEditLastInput()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private var editTranscript: String {
|
|
if case .failed(_, let message) = state.editSession {
|
|
return message
|
|
}
|
|
return state.lastTranscript
|
|
}
|
|
|
|
private var isFailure: Bool {
|
|
if case .failed = state.editSession { return true }
|
|
return false
|
|
}
|
|
|
|
private var leftHelper: String {
|
|
state.editSession.review == nil
|
|
? ExtL10n.string("keyboard.edit.helper.speak")
|
|
: ExtL10n.string("keyboard.edit.helper.compare")
|
|
}
|
|
|
|
private var rightHelper: String {
|
|
if state.editSession.review != nil {
|
|
return state.editCanReplaceOriginal
|
|
? ExtL10n.string("keyboard.edit.helper.apply")
|
|
: ExtL10n.string("keyboard.edit.helper.append")
|
|
}
|
|
return ExtL10n.string("keyboard.edit.helper.finish")
|
|
}
|
|
|
|
private var primaryAccessibilityLabel: String {
|
|
if state.editSession.review != nil {
|
|
return state.editCanReplaceOriginal
|
|
? ExtL10n.string("keyboard.edit.apply")
|
|
: ExtL10n.string("keyboard.edit.append")
|
|
}
|
|
return ExtL10n.string("keyboard.edit.stop")
|
|
}
|
|
|
|
private var reviewSwipeGesture: some Gesture {
|
|
DragGesture(minimumDistance: 12)
|
|
.onEnded { value in
|
|
guard state.editSession.review != nil else { return }
|
|
let translation = value.predictedEndTranslation
|
|
guard abs(translation.width) > abs(translation.height) * 1.2,
|
|
abs(translation.width) >= 28 else {
|
|
return
|
|
}
|
|
let targetPage = translation.width < 0 ? 1 : 0
|
|
guard selectedPage != targetPage else { return }
|
|
if reduceMotion {
|
|
selectedPage = targetPage
|
|
} else {
|
|
withAnimation(.interactiveSpring(response: 0.34, dampingFraction: 0.86)) {
|
|
selectedPage = targetPage
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|