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:
@@ -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.
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" = "偏好设置";
|
||||
|
||||
Reference in New Issue
Block a user