feat(keyboard): add Notes skill and system-style typing
Add save-to-Notes export and direct map handoff while aligning multi-touch typing, period shortcuts, return actions, and navigation visuals with system behavior.
This commit is contained in:
@@ -773,10 +773,30 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
|
||||
private func returnKeyRole(for returnKeyType: UIReturnKeyType) -> State.ReturnKeyRole {
|
||||
switch returnKeyType {
|
||||
case .send, .go, .search, .join, .route, .google, .yahoo, .continue, .emergencyCall:
|
||||
return .send
|
||||
case .default, .next, .done:
|
||||
case .default:
|
||||
return .newline
|
||||
case .go:
|
||||
return .go
|
||||
case .google:
|
||||
return .google
|
||||
case .join:
|
||||
return .join
|
||||
case .next:
|
||||
return .next
|
||||
case .route:
|
||||
return .route
|
||||
case .search:
|
||||
return .search
|
||||
case .send:
|
||||
return .send
|
||||
case .yahoo:
|
||||
return .yahoo
|
||||
case .done:
|
||||
return .done
|
||||
case .emergencyCall:
|
||||
return .emergencyCall
|
||||
case .continue:
|
||||
return .continue
|
||||
@unknown default:
|
||||
return .newline
|
||||
}
|
||||
|
||||
@@ -70,13 +70,18 @@ final class AIKeyboardCoordinator {
|
||||
func submitClipboardSkill(_ skill: AIClipboardSkill) {
|
||||
guard canAcceptIdleSubmit else { return }
|
||||
enterIfNeeded()
|
||||
state.pendingClipboardSkillID = skill.kind == .export ? skill.id : nil
|
||||
let material = ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
if skill.kind == .export {
|
||||
state.pendingClipboardSkillID = skill.id
|
||||
state.pendingClipboardSkillSource = material
|
||||
} else {
|
||||
clearPendingExportSkill()
|
||||
}
|
||||
let instruction = AIClipboardSkillCatalog.instruction(
|
||||
for: skill,
|
||||
locale: AIHintLocaleResolver.packLocale(),
|
||||
translationTargetLocaleId: state.translationTargetLocaleId
|
||||
)
|
||||
let material = ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
AIAgentShortcutRun.trace("keyboard.submit skill=\(skill.id) kind=\(skill.kind)")
|
||||
if let material {
|
||||
AIAgentShortcutRun.traceBody("keyboard.clipboard", material)
|
||||
@@ -120,7 +125,7 @@ final class AIKeyboardCoordinator {
|
||||
) {
|
||||
guard case .ready(let prompt) = resolution else {
|
||||
// The clipboard window closed between rendering and this tap.
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
state.aiSession.fail(
|
||||
ExtL10n.string("keyboard.ai.error.clipboardUnavailable"),
|
||||
utteranceID: nil
|
||||
@@ -128,7 +133,7 @@ final class AIKeyboardCoordinator {
|
||||
return
|
||||
}
|
||||
guard let conversationID = state.aiSession.conversationID else {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
return
|
||||
}
|
||||
let disposition = flow.submitAIQuestion(
|
||||
@@ -137,14 +142,14 @@ final class AIKeyboardCoordinator {
|
||||
thinkingEnabled: thinkingEnabled
|
||||
)
|
||||
if case .rejected(let rejection) = disposition {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
state.aiSession.fail(message(for: rejection), utteranceID: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
guard state.aiSession.isBusy else { return }
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
flow.cancelAIRecording()
|
||||
state.aiSession.cancelCurrentWork()
|
||||
}
|
||||
@@ -153,7 +158,7 @@ final class AIKeyboardCoordinator {
|
||||
if state.aiSession.canInsert, let answer = state.aiSession.answer {
|
||||
guard insertAnswer(answer) else { return }
|
||||
state.aiSession.markAnswerInserted(
|
||||
offersSend: state.returnKeyRole == .send
|
||||
offersSend: state.returnKeyRole.usesActionFill
|
||||
)
|
||||
} else if state.aiSession.canSend {
|
||||
state.aiSession.markAnswerSent()
|
||||
@@ -228,16 +233,21 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
func fail(_ message: String, utteranceID: UUID?) {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
state.aiSession.fail(message, utteranceID: utteranceID)
|
||||
}
|
||||
|
||||
private func endConversationIfNeeded() {
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
guard let conversationID = state.aiSession.conversationID else { return }
|
||||
flow.endAIConversation(conversationID)
|
||||
}
|
||||
|
||||
private func clearPendingExportSkill() {
|
||||
state.pendingClipboardSkillID = nil
|
||||
state.pendingClipboardSkillSource = nil
|
||||
}
|
||||
|
||||
private var isPendingExportSkill: Bool {
|
||||
guard let id = state.pendingClipboardSkillID else { return false }
|
||||
return resolvedSkill(id: id)?.kind == .export
|
||||
@@ -251,9 +261,10 @@ final class AIKeyboardCoordinator {
|
||||
}
|
||||
|
||||
/// Parse an export skill. Empty → in-keyboard tip, stay in the host app.
|
||||
/// Lines → hand off to the host to run the companion Shortcut.
|
||||
/// Lines → hand off to the host (Shortcut, Maps, or Didi).
|
||||
private func finishExportSkill(answer: String) {
|
||||
let source = ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
let source = state.pendingClipboardSkillSource
|
||||
?? ClipboardHistoryStore.shared.newestAIHintEligibleEntry()?.text
|
||||
let skillID = state.pendingClipboardSkillID
|
||||
let items: [String]
|
||||
let emptyTipKey: String
|
||||
@@ -267,6 +278,13 @@ final class AIKeyboardCoordinator {
|
||||
case AIClipboardSkillCatalog.navigateID:
|
||||
items = AIAddressExtraction.lines(from: answer, sourceClipboard: source)
|
||||
emptyTipKey = "keyboard.ai.skill.noAddress"
|
||||
case AIClipboardSkillCatalog.saveToNotesID:
|
||||
items = AINoteExport.items(
|
||||
from: answer,
|
||||
sourceClipboard: source,
|
||||
locale: AIHintLocaleResolver.packLocale()
|
||||
)
|
||||
emptyTipKey = "keyboard.ai.skill.noNote"
|
||||
default:
|
||||
items = AIGenericSkillExport.items(from: answer)
|
||||
emptyTipKey = "keyboard.ai.skill.noExportItems"
|
||||
@@ -280,7 +298,7 @@ final class AIKeyboardCoordinator {
|
||||
AIAgentShortcutRun.traceBody("keyboard.parsedTitles", items.joined(separator: "\n"))
|
||||
}
|
||||
#endif
|
||||
state.pendingClipboardSkillID = nil
|
||||
clearPendingExportSkill()
|
||||
if state.aiSession.isBusy {
|
||||
state.aiSession.cancelCurrentWork()
|
||||
}
|
||||
@@ -292,14 +310,25 @@ final class AIKeyboardCoordinator {
|
||||
state.skillTipText = ExtL10n.string(emptyTipKey)
|
||||
return
|
||||
}
|
||||
guard let skillID,
|
||||
resolvedSkill(id: skillID)?.shortcutName != nil else {
|
||||
AIAgentShortcutRun.trace("keyboard.parse missingShortcut skill=\(skillID ?? "nil")")
|
||||
guard let skillID, let skill = resolvedSkill(id: skillID) else {
|
||||
AIAgentShortcutRun.trace("keyboard.parse missingSkill")
|
||||
state.skillTipText = ExtL10n.string("keyboard.ai.skill.shortcutMissing")
|
||||
return
|
||||
}
|
||||
if skill.requiresShortcut, skill.shortcutName == nil {
|
||||
AIAgentShortcutRun.trace("keyboard.parse missingShortcut skill=\(skillID)")
|
||||
state.skillTipText = ExtL10n.string("keyboard.ai.skill.shortcutMissing")
|
||||
return
|
||||
}
|
||||
AIAgentShortcutRun.trace("keyboard.handoffToHost skill=\(skillID) items=\(items.count)")
|
||||
state.skillTipText = ExtL10n.string("keyboard.ai.skill.runningShortcut")
|
||||
let tipKey: String
|
||||
switch skillID {
|
||||
case AIClipboardSkillCatalog.navigateID:
|
||||
tipKey = "keyboard.ai.skill.openingMaps"
|
||||
default:
|
||||
tipKey = "keyboard.ai.skill.runningShortcut"
|
||||
}
|
||||
state.skillTipText = ExtL10n.string(tipKey)
|
||||
state.runClipboardExportSkill(skillID, items)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//
|
||||
// Grid-level UIKit touch tracking for the typing surface:
|
||||
// Down highlight → Move reselect → Up commit (letters / space / return),
|
||||
// delete repeats on down, Shift holds while the gesture owns it.
|
||||
// overlapping fingers commit in press order, delete repeats on down,
|
||||
// Shift holds while that finger owns it.
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
@@ -12,7 +13,7 @@ import OSGKeyboardShared
|
||||
struct TypingKeyTouchPad: UIViewRepresentable {
|
||||
var layout: TypingKeyLayout
|
||||
var hapticIntensity: KeyboardHapticIntensity
|
||||
var onHighlightChange: (String?) -> Void
|
||||
var onHighlightChange: (Set<String>) -> Void
|
||||
var onCommit: (TypingKeyHitTarget) -> Void
|
||||
var onDeleteFire: () -> Void
|
||||
var onShiftBegan: () -> Void
|
||||
@@ -36,8 +37,7 @@ struct TypingKeyTouchPad: UIViewRepresentable {
|
||||
@MainActor
|
||||
final class Coordinator {
|
||||
var parent: TypingKeyTouchPad
|
||||
private var activeKeyID: String?
|
||||
private var gestureOwnsShift = false
|
||||
private let tracker = TypingTouchTracker()
|
||||
private var deleteRepeatTask: Task<Void, Never>?
|
||||
private var deleteRepeatStartedAt: Date?
|
||||
private var isDeleteRepeating = false
|
||||
@@ -46,72 +46,24 @@ struct TypingKeyTouchPad: UIViewRepresentable {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func handleBegan(at point: CGPoint) {
|
||||
resetDeleteRepeat()
|
||||
gestureOwnsShift = false
|
||||
guard let key = hit(at: point) else {
|
||||
setHighlight(nil)
|
||||
return
|
||||
}
|
||||
activate(key)
|
||||
func handleBegan(id: ObjectIdentifier, at point: CGPoint) {
|
||||
apply(tracker.began(id: id, key: hit(at: point)))
|
||||
}
|
||||
|
||||
func handleMoved(at point: CGPoint) {
|
||||
let key = hit(at: point)
|
||||
if key?.id == activeKeyID { return }
|
||||
|
||||
if activeBehavior == .deleteRepeat {
|
||||
stopDeleteRepeat()
|
||||
}
|
||||
|
||||
if let key {
|
||||
activate(key)
|
||||
} else {
|
||||
// Outside plane: clear highlight; Shift stays held until ended.
|
||||
setHighlight(nil)
|
||||
activeKeyID = nil
|
||||
}
|
||||
func handleMoved(id: ObjectIdentifier, at point: CGPoint) {
|
||||
apply(tracker.moved(id: id, key: hit(at: point)))
|
||||
}
|
||||
|
||||
func handleEnded(at point: CGPoint) {
|
||||
// Outside the key plane → cancel (no commit), per accuracy plan.
|
||||
let key = hit(at: point)
|
||||
stopDeleteRepeat()
|
||||
|
||||
defer {
|
||||
setHighlight(nil)
|
||||
activeKeyID = nil
|
||||
finishShiftIfNeeded()
|
||||
}
|
||||
|
||||
guard let key else { return }
|
||||
|
||||
switch key.behavior {
|
||||
case .commitOnRelease:
|
||||
parent.onCommit(key)
|
||||
case .deleteRepeat:
|
||||
// Already fired on down / while held.
|
||||
break
|
||||
case .shiftHold:
|
||||
// endShiftHold decides tap vs hold-with-type.
|
||||
break
|
||||
}
|
||||
func handleEnded(id: ObjectIdentifier, at point: CGPoint) {
|
||||
apply(tracker.ended(id: id, key: hit(at: point)))
|
||||
}
|
||||
|
||||
func handleCancelled() {
|
||||
stopDeleteRepeat()
|
||||
setHighlight(nil)
|
||||
activeKeyID = nil
|
||||
finishShiftIfNeeded()
|
||||
func handleCancelled(id: ObjectIdentifier) {
|
||||
apply(tracker.cancelled(id: id))
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
private var activeBehavior: TypingKeyTouchBehavior? {
|
||||
guard let activeKeyID else { return nil }
|
||||
return parent.layout.key(id: activeKeyID)?.behavior
|
||||
}
|
||||
|
||||
private func hit(at point: CGPoint) -> TypingKeyHitTarget? {
|
||||
let layout = parent.layout
|
||||
return KeyHitTesting.hitTarget(
|
||||
@@ -124,27 +76,29 @@ struct TypingKeyTouchPad: UIViewRepresentable {
|
||||
)
|
||||
}
|
||||
|
||||
private func activate(_ key: TypingKeyHitTarget) {
|
||||
activeKeyID = key.id
|
||||
setHighlight(key.id)
|
||||
playFeedback(for: key)
|
||||
|
||||
switch key.behavior {
|
||||
case .commitOnRelease:
|
||||
break
|
||||
case .deleteRepeat:
|
||||
parent.onDeleteFire()
|
||||
startDeleteRepeat()
|
||||
case .shiftHold:
|
||||
if !gestureOwnsShift {
|
||||
gestureOwnsShift = true
|
||||
parent.onShiftBegan()
|
||||
}
|
||||
private func apply(_ effects: TypingTouchEffects) {
|
||||
if effects.stopDeleteRepeat {
|
||||
stopDeleteRepeat()
|
||||
}
|
||||
}
|
||||
|
||||
private func setHighlight(_ id: String?) {
|
||||
parent.onHighlightChange(id)
|
||||
for key in effects.commits {
|
||||
parent.onCommit(key)
|
||||
}
|
||||
if let key = effects.playFeedback {
|
||||
playFeedback(for: key)
|
||||
}
|
||||
if effects.deleteFire {
|
||||
parent.onDeleteFire()
|
||||
}
|
||||
if effects.startDeleteRepeat {
|
||||
startDeleteRepeat()
|
||||
}
|
||||
if effects.beginShift {
|
||||
parent.onShiftBegan()
|
||||
}
|
||||
if effects.endShift {
|
||||
parent.onShiftEnded()
|
||||
}
|
||||
parent.onHighlightChange(tracker.highlightedKeyIDs)
|
||||
}
|
||||
|
||||
private func playFeedback(for key: TypingKeyHitTarget) {
|
||||
@@ -207,16 +161,6 @@ struct TypingKeyTouchPad: UIViewRepresentable {
|
||||
deleteRepeatTask?.cancel()
|
||||
deleteRepeatTask = nil
|
||||
}
|
||||
|
||||
private func resetDeleteRepeat() {
|
||||
stopDeleteRepeat()
|
||||
}
|
||||
|
||||
private func finishShiftIfNeeded() {
|
||||
guard gestureOwnsShift else { return }
|
||||
gestureOwnsShift = false
|
||||
parent.onShiftEnded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +183,7 @@ final class TypingKeyTouchPadUIView: UIView {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = Self.padTint
|
||||
isMultipleTouchEnabled = false
|
||||
isMultipleTouchEnabled = true
|
||||
isExclusiveTouch = true
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
@@ -250,21 +194,30 @@ final class TypingKeyTouchPadUIView: UIView {
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let touch = touches.first else { return }
|
||||
coordinator?.handleBegan(at: touch.location(in: self))
|
||||
for touch in Self.sorted(touches) {
|
||||
coordinator?.handleBegan(id: ObjectIdentifier(touch), at: touch.location(in: self))
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let touch = touches.first else { return }
|
||||
coordinator?.handleMoved(at: touch.location(in: self))
|
||||
for touch in Self.sorted(touches) {
|
||||
coordinator?.handleMoved(id: ObjectIdentifier(touch), at: touch.location(in: self))
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
guard let touch = touches.first else { return }
|
||||
coordinator?.handleEnded(at: touch.location(in: self))
|
||||
for touch in Self.sorted(touches) {
|
||||
coordinator?.handleEnded(id: ObjectIdentifier(touch), at: touch.location(in: self))
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
coordinator?.handleCancelled()
|
||||
for touch in Self.sorted(touches) {
|
||||
coordinator?.handleCancelled(id: ObjectIdentifier(touch))
|
||||
}
|
||||
}
|
||||
|
||||
private static func sorted(_ touches: Set<UITouch>) -> [UITouch] {
|
||||
touches.sorted { $0.timestamp < $1.timestamp }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ struct TypingRootView: View {
|
||||
/// After the first expand, keep the panel tree mounted and only toggle
|
||||
/// opacity so subsequent ▼/▲ taps stay cheap.
|
||||
@State private var candidatePanelMounted = false
|
||||
/// Key currently under the finger (grid-level touch pad).
|
||||
@State private var highlightedKeyID: String?
|
||||
/// Keys currently under a finger (grid-level touch pad, multi-touch).
|
||||
@State private var highlightedKeyIDs: Set<String> = []
|
||||
|
||||
static func totalHeight(isIPad: Bool = false, width: CGFloat = 0) -> CGFloat {
|
||||
TypingLayoutMetrics.contentHeight(isIPad: isIPad, width: width)
|
||||
@@ -398,7 +398,7 @@ struct TypingRootView: View {
|
||||
TypingKeyTouchPad(
|
||||
layout: layout,
|
||||
hapticIntensity: state.keyboardHapticIntensity,
|
||||
onHighlightChange: { highlightedKeyID = $0 },
|
||||
onHighlightChange: { highlightedKeyIDs = $0 },
|
||||
onCommit: { commitTypingKey($0) },
|
||||
onDeleteFire: { apply(typing.handleKey("⌫")) },
|
||||
onShiftBegan: { typing.beginShiftHold() },
|
||||
@@ -448,10 +448,10 @@ struct TypingRootView: View {
|
||||
let pageLabel = typing.page == .letters ? "123" : "ABC"
|
||||
let spaceLabel = typing.language == .chinese ? "空格" : "space"
|
||||
let returnLabel: String = {
|
||||
switch state.returnKeyRole {
|
||||
case .newline: return "return"
|
||||
case .send: return ExtL10n.string(state.returnKeyRole.titleKey)
|
||||
if state.returnKeyRole.usesActionFill {
|
||||
return ExtL10n.string(state.returnKeyRole.titleKey)
|
||||
}
|
||||
return "return"
|
||||
}()
|
||||
|
||||
// iPad spends its extra width on comma / period like the system
|
||||
@@ -491,7 +491,7 @@ struct TypingRootView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func visualTypingKey(_ key: TypingKeyHitTarget) -> some View {
|
||||
let pressed = highlightedKeyID == key.id
|
||||
let pressed = highlightedKeyIDs.contains(key.id)
|
||||
let isBottom = key.id.hasPrefix("bottom.")
|
||||
let isShift = key.label == "⇧"
|
||||
let shiftLit = isShift && typing.isShiftEnabled
|
||||
@@ -624,13 +624,14 @@ struct TypingRootView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var returnKeyLabel: some View {
|
||||
switch state.returnKeyRole {
|
||||
case .newline:
|
||||
Image(systemName: "arrow.turn.down.left")
|
||||
.font(.system(size: 21, weight: .medium))
|
||||
case .send:
|
||||
if state.returnKeyRole.usesActionFill {
|
||||
ExtL10n.text(state.returnKeyRole.titleKey)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.6)
|
||||
} else {
|
||||
Image(systemName: "arrow.turn.down.left")
|
||||
.font(.system(size: 21, weight: .medium))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,42 +685,29 @@ struct TypingRootView: View {
|
||||
}
|
||||
|
||||
private var returnKeyFill: Color {
|
||||
switch state.returnKeyRole {
|
||||
case .newline: return keyFill
|
||||
case .send: return sendKeyFill
|
||||
}
|
||||
state.returnKeyRole.usesActionFill ? sendKeyFill : keyFill
|
||||
}
|
||||
|
||||
private var returnKeyPressedFill: Color {
|
||||
switch state.returnKeyRole {
|
||||
case .newline: return keyPressedFill
|
||||
case .send: return sendKeyPressedFill
|
||||
}
|
||||
state.returnKeyRole.usesActionFill ? sendKeyPressedFill : keyPressedFill
|
||||
}
|
||||
|
||||
private var returnKeyBorder: Color {
|
||||
switch state.returnKeyRole {
|
||||
case .newline:
|
||||
return palette.divider
|
||||
case .send:
|
||||
if state.returnKeyRole.usesActionFill {
|
||||
return Color.black.opacity(colorScheme == .dark ? 0.10 : 0.08)
|
||||
}
|
||||
return palette.divider
|
||||
}
|
||||
|
||||
private var returnKeyAccessibilityLabel: String {
|
||||
switch state.returnKeyRole {
|
||||
case .newline: return "return"
|
||||
case .send: return ExtL10n.string(state.returnKeyRole.titleKey)
|
||||
if state.returnKeyRole.usesActionFill {
|
||||
return ExtL10n.string(state.returnKeyRole.titleKey)
|
||||
}
|
||||
return "return"
|
||||
}
|
||||
|
||||
private var returnKeyTextColor: Color {
|
||||
switch state.returnKeyRole {
|
||||
case .newline:
|
||||
return keyTextColor
|
||||
case .send:
|
||||
return .white
|
||||
}
|
||||
state.returnKeyRole.usesActionFill ? .white : keyTextColor
|
||||
}
|
||||
|
||||
/// The send key stays recognizable in both appearances without becoming neon.
|
||||
|
||||
@@ -518,6 +518,9 @@ struct AIKeyboardView: View {
|
||||
if state.pendingClipboardSkillID == AIClipboardSkillCatalog.navigateID {
|
||||
return ExtL10n.string("keyboard.ai.skill.extractingAddress")
|
||||
}
|
||||
if state.pendingClipboardSkillID == AIClipboardSkillCatalog.saveToNotesID {
|
||||
return ExtL10n.string("keyboard.ai.skill.namingNote")
|
||||
}
|
||||
if let draft = state.aiSession.draftAnswerText, !draft.isEmpty {
|
||||
return ExtL10n.string("keyboard.ai.generating")
|
||||
}
|
||||
|
||||
@@ -484,7 +484,7 @@ public struct KeyboardRootView: View {
|
||||
title: title,
|
||||
label: title,
|
||||
disabled: disabled,
|
||||
isSend: state.returnKeyRole == .send,
|
||||
isSend: state.returnKeyRole.usesActionFill,
|
||||
hapticIntensity: state.keyboardHapticIntensity
|
||||
) {
|
||||
state.insertNewline()
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
"common.space" = "Space";
|
||||
"common.newline" = "Return";
|
||||
"common.send" = "Send";
|
||||
"keyboard.return.go" = "Go";
|
||||
"keyboard.return.search" = "Search";
|
||||
"keyboard.return.join" = "Join";
|
||||
"keyboard.return.next" = "Next";
|
||||
"keyboard.return.route" = "Route";
|
||||
"keyboard.return.google" = "Google";
|
||||
"keyboard.return.yahoo" = "Yahoo";
|
||||
"keyboard.return.emergencyCall" = "Emergency Call";
|
||||
|
||||
/* Privacy footnote (onboarding welcome page) */
|
||||
"privacy.audio.title" = "On-device transcription";
|
||||
@@ -299,6 +307,7 @@
|
||||
"keyboard.ai.skill.translate" = "Translate";
|
||||
"keyboard.ai.skill.extractTodos" = "Tasks";
|
||||
"keyboard.ai.skill.extractEvents" = "Events";
|
||||
"keyboard.ai.skill.saveToNotes" = "Notes";
|
||||
"keyboard.ai.skill.navigate" = "Navigate";
|
||||
"keyboard.ai.skill.previewPolish" = "Polish";
|
||||
"keyboard.ai.skill.previewIdeas" = "Ideas";
|
||||
@@ -306,10 +315,13 @@
|
||||
"keyboard.ai.skill.noTodos" = "No tasks in the clipboard";
|
||||
"keyboard.ai.skill.noEvents" = "No date or time in the clipboard";
|
||||
"keyboard.ai.skill.noAddress" = "No address in the clipboard";
|
||||
"keyboard.ai.skill.noNote" = "Nothing to save to Notes";
|
||||
"keyboard.ai.skill.noExportItems" = "Nothing to send to the Shortcut";
|
||||
"keyboard.ai.skill.extracting" = "Finding tasks…";
|
||||
"keyboard.ai.skill.extractingEvents" = "Finding events…";
|
||||
"keyboard.ai.skill.extractingAddress" = "Finding an address…";
|
||||
"keyboard.ai.skill.namingNote" = "Naming the note…";
|
||||
"keyboard.ai.skill.runningShortcut" = "Running Shortcut…";
|
||||
"keyboard.ai.skill.openingMaps" = "Opening Maps…";
|
||||
"keyboard.ai.skill.shortcutMissing" = "Companion Shortcut not found. Reinstall it in Skills";
|
||||
"keyboard.ai.skill.handoffFailed" = "Couldn’t open the app to run the Shortcut";
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
"common.space" = "空格";
|
||||
"common.newline" = "回车";
|
||||
"common.send" = "发送";
|
||||
"keyboard.return.go" = "前往";
|
||||
"keyboard.return.search" = "搜索";
|
||||
"keyboard.return.join" = "加入";
|
||||
"keyboard.return.next" = "下一项";
|
||||
"keyboard.return.route" = "路线";
|
||||
"keyboard.return.google" = "Google";
|
||||
"keyboard.return.yahoo" = "Yahoo";
|
||||
"keyboard.return.emergencyCall" = "紧急电话";
|
||||
|
||||
/* Privacy footnote (onboarding welcome page) */
|
||||
"privacy.audio.title" = "支持本地转写";
|
||||
@@ -299,6 +307,7 @@
|
||||
"keyboard.ai.skill.translate" = "翻译";
|
||||
"keyboard.ai.skill.extractTodos" = "待办";
|
||||
"keyboard.ai.skill.extractEvents" = "日程";
|
||||
"keyboard.ai.skill.saveToNotes" = "备忘录";
|
||||
"keyboard.ai.skill.navigate" = "导航";
|
||||
"keyboard.ai.skill.previewPolish" = "润色";
|
||||
"keyboard.ai.skill.previewIdeas" = "点子";
|
||||
@@ -306,10 +315,13 @@
|
||||
"keyboard.ai.skill.noTodos" = "剪贴板内容没有待办事项";
|
||||
"keyboard.ai.skill.noEvents" = "剪贴板内容没有日期或时间";
|
||||
"keyboard.ai.skill.noAddress" = "剪贴板内容没有可导航的地址";
|
||||
"keyboard.ai.skill.noNote" = "剪贴板没有可保存的内容";
|
||||
"keyboard.ai.skill.noExportItems" = "没有可发给捷径的内容";
|
||||
"keyboard.ai.skill.extracting" = "正在提取待办…";
|
||||
"keyboard.ai.skill.extractingEvents" = "正在提取日程…";
|
||||
"keyboard.ai.skill.extractingAddress" = "正在识别地址…";
|
||||
"keyboard.ai.skill.namingNote" = "正在生成标题…";
|
||||
"keyboard.ai.skill.runningShortcut" = "正在运行捷径…";
|
||||
"keyboard.ai.skill.openingMaps" = "正在打开地图…";
|
||||
"keyboard.ai.skill.shortcutMissing" = "找不到配套捷径,请在技能页重新安装";
|
||||
"keyboard.ai.skill.handoffFailed" = "无法打开 App 运行捷径";
|
||||
|
||||
Reference in New Issue
Block a user