chore(typing): snapshot local pinyin WIP before syncing cloud branch

Preserve the in-progress local typing/pinyin implementation so feat/pinyin
can safely reset to origin/feat/pinyin (cloud English + Chinese typing).
This commit is contained in:
Rocky
2026-08-03 13:14:01 +08:00
parent c037be3654
commit d1e5fed964
58 changed files with 370539 additions and 121 deletions
+2 -1
View File
@@ -31,11 +31,12 @@
<li><strong>Transcribed text</strong> — in Cloud polish mode, the final text (not audio) may be sent to the LLM provider you configure (e.g. OpenAI) for punctuation and formatting.</li>
<li><strong>API credentials</strong> — stored in the iOS Keychain and shared between the main app and keyboard extension. When iCloud settings sync is enabled, keys replicate through iCloud Keychain (not iCloud KVS JSON).</li>
<li><strong>App preferences</strong> — engine mode, language, and keyboard settings stored in App Group UserDefaults. Optional iCloud sync mirrors preferences, usage statistics, and voice history through your private iCloud account.</li>
<li><strong>On-device typing learning</strong> — the Chinese keyboard stores selected words and candidate frequencies in the App Group on your device. OSGKeyboard does not upload this user dictionary.</li>
</ul>
<h2>What we do not collect</h2>
<ul>
<li>We do <strong>not</strong> log or upload ordinary keystrokes you type with the keyboard.</li>
<li>We do <strong>not</strong> log or upload ordinary keystrokes. Only the on-device Chinese candidate-learning data described above is retained locally.</li>
<li>We do <strong>not</strong> operate analytics or advertising SDKs.</li>
<li>We do <strong>not</strong> sell personal data.</li>
</ul>
@@ -1,18 +1,20 @@
// OpenSourceLicenseCatalog.swift
// OSGKeyboard · Main App
//
// Single source of truth for third-party open-source components shipped
// with OSGKeyboard. Consumed by Settings About Third-Party Licenses.
// Single source of truth for third-party components distributed by each
// OSGKeyboard platform. Consumed by Settings About Third-Party Licenses.
//
// Keep this list aligned with bundled resources and runtime downloads.
//
// iOS targets remain zero-SPM. macOS local ASR links mlx-audio-swift (vendored
// under ThirdParty/) for Qwen3 MLX streaming; model weights download via catalog.
import Foundation
enum OpenSourceLicenseCatalog {
enum Platform: Hashable {
case iOS
case macOS
}
struct Entry: Identifiable, Hashable {
let id: String
let name: String
@@ -22,17 +24,82 @@ enum OpenSourceLicenseCatalog {
let url: URL?
/// Verbatim license body for the long-scroll disclosure page.
let licenseText: String
let platforms: Set<Platform>
}
/// Bundled libraries and runtime components referenced by the app.
static let entries: [Entry] = [
private static let allEntries: [Entry] = [
.init(
id: "material-icons",
name: "Google Material Icons",
licenseName: "Apache-2.0",
purpose: "MaterialIcons-Regular.ttf bundled with the iOS app for Settings and navigation iconography.",
url: URL(string: "https://github.com/google/material-design-icons"),
licenseText: apache2Text
licenseText: resourceText(
named: "LICENSE-PINYIN-SIMP-APACHE",
fallback: apache2Text
),
platforms: [.iOS]
),
.init(
id: "librime-static",
name: "librime 1.17.0 + static dependencies",
licenseName: "BSD-3-Clause and others",
purpose: "Chinese input runtime packaged by librime-xcframework 1.17.0-pack.1. Includes complete notices for Boost, OpenCC, LevelDB, yaml-cpp, glog, marisa-trie, RapidJSON, Darts and other statically linked components.",
url: URL(string: "https://github.com/ghostflyby/librime-xcframework"),
licenseText: resourceText(
named: "LIBRIME-COMBINED-NOTICES",
fallback: bsd3Text
),
platforms: [.iOS]
),
.init(
id: "rime-pinyin-simp",
name: "rime-pinyin-simp",
licenseName: "Apache-2.0",
purpose: "Permissive Simplified-Chinese baseline dictionary used to generate OSGKeyboard's Rime lexicon.",
url: URL(string: "https://github.com/rime/rime-pinyin-simp"),
licenseText: resourceText(
named: "LICENSE-PINYIN-SIMP-APACHE",
fallback: apache2Text
),
platforms: [.iOS]
),
.init(
id: "jieba",
name: "Jieba",
licenseName: "MIT",
purpose: "Modern Simplified-Chinese words and frequency weights used by the generated typing dictionary.",
url: URL(string: "https://github.com/fxsjy/jieba"),
licenseText: resourceText(
named: "LICENSE-JIEBA-MIT",
fallback: mitText
),
platforms: [.iOS]
),
.init(
id: "phrase-pinyin-data",
name: "phrase-pinyin-data",
licenseName: "MIT",
purpose: "Phrase pronunciation data used to resolve polyphonic Chinese words.",
url: URL(string: "https://github.com/mozillazg/phrase-pinyin-data"),
licenseText: resourceText(
named: "LICENSE-PHRASE-PINYIN-DATA-MIT",
fallback: mitText
),
platforms: [.iOS]
),
.init(
id: "pinyin-data",
name: "pinyin-data",
licenseName: "MIT",
purpose: "Per-character Mandarin pronunciation fallback for generated dictionary entries.",
url: URL(string: "https://github.com/mozillazg/pinyin-data"),
licenseText: resourceText(
named: "LICENSE-PINYIN-DATA-MIT",
fallback: mitText
),
platforms: [.iOS]
),
.init(
id: "mlx-audio-swift",
@@ -40,10 +107,28 @@ enum OpenSourceLicenseCatalog {
licenseName: "MIT",
purpose: "macOS local Qwen3 MLX streaming ASR (MLXAudioSTT), linked only in the Mac app target.",
url: URL(string: "https://github.com/Blaizzy/mlx-audio-swift"),
licenseText: mitText
licenseText: mlxAudioMITText,
platforms: [.macOS]
),
]
static func entries(for platform: Platform) -> [Entry] {
allEntries.filter { $0.platforms.contains(platform) }
}
private static func resourceText(named name: String, fallback: String) -> String {
let bundles = Bundle.allFrameworks + Bundle.allBundles + [Bundle.main]
for bundle in bundles {
guard let url = bundle.url(forResource: name, withExtension: "txt"),
let text = try? String(contentsOf: url, encoding: .utf8),
!text.isEmpty else {
continue
}
return text
}
return fallback
}
// MARK: - License bodies
static let apache2Text = """
@@ -85,4 +170,36 @@ enum OpenSourceLicenseCatalog {
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
static let mlxAudioMITText = """
MIT License
Copyright (c) 2025 Prince Canuma
\(mitText.components(separatedBy: "\n").dropFirst(2).joined(separator: "\n"))
"""
static let bsd3Text = """
BSD 3-Clause License
Copyright (c) 2014, RIME Developers
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DAMAGES ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE.
"""
}
+6
View File
@@ -68,6 +68,12 @@ struct MainAppRoot: View {
Task {
await AppCloudSync.shared.pullAllIfEnabled()
}
Task {
let typingConfig = TypingInputConfiguration.shared.snapshot
try? await RimeResourceInstaller.shared.installIfNeeded(
configuration: typingConfig
)
}
}
.onReceive(NotificationCenter.default.publisher(for: .settingsDidSyncFromCloud)) { _ in
config.reloadFromPersistedStorage()
@@ -11,6 +11,8 @@ import OSGKeyboardShared
struct OpenSourceLicensesView: View {
@Environment(\.themePalette) private var palette: ThemePalette
private let entries = OpenSourceLicenseCatalog.entries(for: .iOS)
var body: some View {
ScrollView {
CardPageContent(spacing: SettingsListMetrics.sectionLabelSpacing) {
@@ -21,7 +23,7 @@ struct OpenSourceLicensesView: View {
.padding(.horizontal, Spacing.xs)
VStack(spacing: 0) {
ForEach(Array(OpenSourceLicenseCatalog.entries.enumerated()), id: \.element.id) { index, entry in
ForEach(Array(entries.enumerated()), id: \.element.id) { index, entry in
NavigationLink {
OpenSourceLicenseDetailView(entry: entry)
} label: {
@@ -29,7 +31,7 @@ struct OpenSourceLicensesView: View {
}
.buttonStyle(.plain)
if index < OpenSourceLicenseCatalog.entries.count - 1 {
if index < entries.count - 1 {
Divider().background(palette.divider)
}
}
@@ -151,6 +151,29 @@ struct PolishIntensityPickerRow: View {
}
}
// MARK: - Default input surface toggle
struct DefaultTypingInputToggleRow: View {
@Environment(\.themePalette) private var palette: ThemePalette
@Binding var isOn: Bool
var body: some View {
Toggle(isOn: $isOn) {
VStack(alignment: .leading, spacing: 3) {
Text("settings.typingInput.default.title")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
Text("settings.typingInput.default.description")
.font(TypeStyle.caption)
.foregroundStyle(palette.textSecondary)
.fixedSize(horizontal: false, vertical: true)
}
}
.tint(palette.accent)
.settingsListRow()
}
}
// MARK: - Cursor drag navigation toggle
struct CursorDragNavigationToggleRow: View {
@@ -221,6 +221,7 @@ struct VoiceSessionSettingsRows: View {
struct GeneralSettingsView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject var config: ProviderConfig
@ObservedObject private var typingConfiguration = TypingInputConfiguration.shared
var body: some View {
ScrollView {
@@ -241,6 +242,21 @@ struct GeneralSettingsView: View {
CardSection("settings.general.keyboard.title") {
VStack(spacing: 0) {
DefaultTypingInputToggleRow(
isOn: $typingConfiguration.defaultToTyping
)
Divider().background(palette.divider)
NavigationLink {
TypingInputSettingsView()
} label: {
SettingsNavigationRow(title: "settings.typingInput.title")
}
.buttonStyle(.plain)
Divider().background(palette.divider)
HandednessPickerRow(
selection: Binding(
get: { config.handednessPreference },
+9 -8
View File
@@ -50,7 +50,9 @@ struct SettingsView: View {
}
dailySection
transcriptionAndPolishSection
moreEntriesSection
if presentation == .tab {
moreEntriesSection
}
}
.modifier(SettingsScrollBottomPadding(presentation: presentation))
}
@@ -114,6 +116,10 @@ struct SettingsView: View {
private var dailySection: some View {
CardSection("settings.daily.title") {
VStack(spacing: 0) {
settingsRouteButton(.general, title: "settings.general.title")
Divider().background(palette.divider)
LocalePickerRow(
locales: effectiveLocales,
selection: Binding(
@@ -160,16 +166,11 @@ struct SettingsView: View {
}
}
// MARK: - General / About
// MARK: - About
private var moreEntriesSection: some View {
VStack(spacing: 0) {
settingsRouteButton(.general, title: "settings.general.title")
if presentation == .tab {
Divider().background(palette.divider)
settingsRouteButton(.about, title: "settings.about.title")
}
settingsRouteButton(.about, title: "settings.about.title")
}
.surfaceCard()
}
@@ -0,0 +1,96 @@
// TypingInputSettingsView.swift
// OSGKeyboard · Main App
//
// Schema and opt-in fuzzy-pinyin settings. Changing fuzzy pairs triggers
// host-side redeployment; the keyboard extension never compiles schemas.
import SwiftUI
import OSGKeyboardShared
struct TypingInputSettingsView: View {
@Environment(\.themePalette) private var palette: ThemePalette
@ObservedObject private var configuration = TypingInputConfiguration.shared
@State private var isDeploying = false
@State private var deploymentError: String?
var body: some View {
List {
Section("输入方案") {
Picker("拼音方案", selection: $configuration.schema) {
ForEach(TypingInputSchema.allCases) { schema in
Text(schema.displayName).tag(schema)
}
}
.pickerStyle(.inline)
}
Section {
ForEach(PinyinFuzzyPair.allCases) { pair in
Toggle(
pair.displayName,
isOn: Binding(
get: { configuration.fuzzyPairs.contains(pair) },
set: { enabled in
configuration.setFuzzyPair(pair, enabled: enabled)
deployUpdatedSchemas()
}
)
)
}
} header: {
Text("模糊音")
} footer: {
Text("默认全部关闭。只开启你需要的组合,避免候选噪音。")
}
Section("输入法资源") {
HStack {
Text("状态")
Spacer()
if isDeploying {
ProgressView()
.controlSize(.small)
} else {
Text(statusText)
.foregroundStyle(
deploymentError == nil ? palette.textSecondary : palette.danger
)
}
}
Button("重新部署输入法资源") {
deployUpdatedSchemas()
}
.disabled(isDeploying)
}
}
.scrollContentBackground(.hidden)
.background(palette.background)
.navigationTitle("settings.typingInput.title")
.navigationBarTitleDisplayMode(.inline)
}
private var statusText: String {
if let deploymentError { return deploymentError }
return RimeResourceInstaller.isReady ? "已就绪" : "待初始化"
}
private func deployUpdatedSchemas() {
guard !isDeploying else { return }
let snapshot = configuration.snapshot
isDeploying = true
deploymentError = nil
Task {
do {
try await RimeResourceInstaller.shared.installIfNeeded(
configuration: snapshot,
force: true
)
} catch {
deploymentError = error.localizedDescription
}
isDeploying = false
}
}
}
+3
View File
@@ -184,6 +184,9 @@
"settings.general.appearanceLanguage.title" = "Appearance & Language";
"settings.general.keyboard.title" = "Keyboard & Gestures";
"settings.general.sync.title" = "Sync";
"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";
"settings.speechRecognition.title" = "Speech Recognition";
"settings.textPolish.title" = "Text Polish";
"settings.preferences.title" = "Preferences";
@@ -184,6 +184,9 @@
"settings.general.appearanceLanguage.title" = "外观与语言";
"settings.general.keyboard.title" = "键盘与操作";
"settings.general.sync.title" = "同步";
"settings.typingInput.title" = "文本输入";
"settings.typingInput.default.title" = "默认进行文字输入";
"settings.typingInput.default.description" = "打开键盘时,默认使用文字输入键盘而非语音输入";
"settings.speechRecognition.title" = "语音识别配置";
"settings.textPolish.title" = "文本润色配置";
"settings.preferences.title" = "偏好设置";