From 5e5122f1722336aef2c3b67a3e5ee5ff5ecab3eb Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:45:29 +0800 Subject: [PATCH] chore: adopt source-available license and strengthen privacy disclosures Replace MIT with a restrictive source-available license and align README/docs copy. Add cloud polish acknowledgment, privacy manifest updates, voice history disclosure, export compliance metadata, and support links for App Store readiness. --- LICENSE | 233 ++++++++++++++++-- OSGKeyboard/Info.plist | 2 + OSGKeyboard/PrivacyInfo.xcprivacy | 15 +- OSGKeyboard/Services/LegalLinks.swift | 5 + .../Components/PrivacyDisclosureViews.swift | 93 +++++++ OSGKeyboard/Views/EnginePickerSection.swift | 34 ++- OSGKeyboard/Views/HistoryView.swift | 7 + OSGKeyboard/Views/OnboardingView.swift | 7 + OSGKeyboard/Views/SettingsView.swift | 5 + OSGKeyboard/en.lproj/InfoPlist.strings | 4 + OSGKeyboard/en.lproj/Localizable.strings | 8 +- OSGKeyboard/zh-Hans.lproj/InfoPlist.strings | 4 + OSGKeyboard/zh-Hans.lproj/Localizable.strings | 8 +- OSGKeyboardExt/Services/HostAppLauncher.swift | 29 +-- OSGKeyboardShared/Models/ProviderConfig.swift | 7 + README.md | 8 +- README.zh.md | 8 +- docs/index.html | 10 +- docs/privacy.html | 2 + docs/privacy/index.html | 2 + project.yml | 2 + 21 files changed, 429 insertions(+), 64 deletions(-) create mode 100644 OSGKeyboard/Views/Components/PrivacyDisclosureViews.swift create mode 100644 OSGKeyboard/en.lproj/InfoPlist.strings create mode 100644 OSGKeyboard/zh-Hans.lproj/InfoPlist.strings diff --git a/LICENSE b/LICENSE index 6b97ef6..4e8eb26 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,220 @@ -MIT License +OSGKeyboard Source Available License +Version 1.0, June 2026 -Copyright (c) 2026 OSGKeyboard Contributors +Copyright (c) 2026 OSGKeyboard Contributors. All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +This is NOT an open-source license. The software is made available for +transparency and personal, non-commercial use only. Commercial rights are +reserved exclusively by the copyright holder unless a separate written +agreement is granted. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Contact for commercial licensing: rocky.hk@gmail.com -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + +1. Definitions + + "Software" means the OSGKeyboard source code, documentation, and + associated files in this repository. + + "You" means any individual or legal entity exercising permissions under + this License. + + "Commercial Use" means any use of the Software that is intended for or + directed toward commercial advantage or monetary compensation, including + but not limited to: selling, licensing, or distributing the Software or + derivatives; offering a product or service that incorporates or is + derived from the Software; using the Software in production for a + business; or using the Software to develop a competing product. + + +2. Permitted Uses (Non-Commercial, Personal) + + You may, without a separate commercial license: + + (a) Clone, download, and view the Software for personal learning and + reference. + + (b) Build and run the Software locally on devices you own or control, + for personal, non-commercial evaluation and debugging. + + (c) Modify the Software for your own personal, non-commercial use on + devices you own or control, provided you do not distribute, + publish, or make available any modified version to any third party. + + +3. Prohibited Uses + + Without prior written permission from the copyright holder, You may NOT: + + (a) Use the Software for any Commercial Use. + + (b) Redistribute, publish, sublicense, sell, or otherwise make the + Software or any portion thereof available to any third party, + whether in original or modified form. + + (c) Create, maintain, or distribute forked or derivative works based on + the Software, including repositories, packages, or products intended + for public or third-party use. + + (d) Remove or alter copyright notices or this License. + + (e) Use the Software, or any derivative thereof, as the basis for a + competing keyboard, voice-input, or substantially similar product. + + +4. Commercial Licensing + + Exclusive commercial rights are retained by the copyright holder. + Commercial Use requires a separate written license. To inquire, contact: + + rocky.hk@gmail.com + + +5. Contributions + + Contributions (e.g., pull requests, patches, issues with code) are + welcome. By submitting a contribution, You represent that You have the + right to submit it and You grant the copyright holder a perpetual, + worldwide, non-exclusive, royalty-free license to use, reproduce, + modify, and sublicense Your contribution as part of the Software under + this License. You retain ownership of Your contribution; no copyright + transfer is required. + + +6. App Store Distribution + + Distribution of compiled application binaries through the Apple App Store + (or other channels) by the copyright holder or its authorized licensees + does not grant You any rights to the source code beyond those stated in + this License. + + +7. Third-Party Components + + The Software may include or depend on third-party components governed by + their own licenses. This License applies only to OSGKeyboard's own + original work and does not override third-party license terms. + + +8. Disclaimer of Warranty + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. + + +9. Limitation of Liability + + IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY + CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +10. Termination + + Your rights under this License terminate automatically if You breach + any term. Sections 3–9 survive termination. + + +11. Governing Language + + This English text is the authoritative version. A Chinese translation + follows for reference only. + + +================================================================================ +OSGKeyboard 源码可见许可协议 +版本 1.0,2026 年 6 月 + +版权所有 (c) 2026 OSGKeyboard Contributors。保留所有权利。 + +本协议并非开源许可。公开源码仅为透明与个人非商用目的。除另行书面授权外, +商业权利由版权持有人独占保留。 + +商业授权联系:rocky.hk@gmail.com + + +1. 定义 + + 「软件」指本仓库中的 OSGKeyboard 源代码、文档及相关文件。 + + 「您」指在本协议下行使权利的任何个人或法人。 + + 「商业使用」指以商业利益或金钱报酬为目的的使用,包括但不限于:销售、 + 许可或分发软件或其衍生作品;提供包含或源于本软件的产品或服务;在 + 商业生产环境中使用;或用于开发竞争产品。 + + +2. 允许的使用(非商用、个人) + + 无需另行商业授权,您可: + + (a) 克隆、下载、阅读软件,用于个人学习与研究; + + (b) 在自有或控制的设备上构建并运行软件,用于个人非商用评估与调试; + + (c) 为个人非商用目的修改软件并在自有设备上使用,但不得向任何第三方 + 分发、发布或提供任何修改版本。 + + +3. 禁止的使用 + + 未经版权持有人事先书面许可,您不得: + + (a) 将软件用于任何商业使用; + + (b) 以任何形式向第三方再分发、发布、转许可、销售或提供软件或其部分, + 无论是否修改; + + (c) 创建、维护或分发基于本软件的分叉或衍生作品,包括面向公众或第三 + 方的仓库、包或产品; + + (d) 移除或篡改版权声明或本协议; + + (e) 将软件或其衍生作品作为竞争性的键盘、语音输入或实质相似产品的 + 基础。 + + +4. 商业授权 + + 商业权利由版权持有人独占保留。商业使用须取得单独书面授权。联系: + + rocky.hk@gmail.com + + +5. 贡献 + + 欢迎提交贡献(如 Pull Request、补丁等)。提交即表示您有权提交该内容, + 并授予版权持有人一项永久的、全球性的、非独占的、免版税许可,以便在 + 本协议下将您的贡献作为软件的一部分使用、复制、修改和再许可。您保留 + 对贡献的所有权;不要求转让版权。 + + +6. App Store 分发 + + 版权持有人或其授权方通过 Apple App Store(或其他渠道)分发编译后的 + 应用,不授予您超出本协议所述范围的源码权利。 + + +7. 第三方组件 + + 软件可能包含或依赖受各自许可约束的第三方组件。本协议仅适用于 + OSGKeyboard 原创作品,不覆盖第三方许可条款。 + + +8. 免责声明 + + 软件按「原样」提供,不提供任何明示或暗示的保证,包括但不限于适销性、 + 特定用途适用性和不侵权保证。 + + +9. 责任限制 + + 在任何情况下,版权持有人或贡献者均不对因软件或使用软件而产生的任何 + 索赔、损害或其他责任承担责任。 + + +10. 终止 + + 若您违反本协议任何条款,您的权利自动终止。第 3–9 条在终止后继续有效。 diff --git a/OSGKeyboard/Info.plist b/OSGKeyboard/Info.plist index 26d3ea3..39fe503 100644 --- a/OSGKeyboard/Info.plist +++ b/OSGKeyboard/Info.plist @@ -36,6 +36,8 @@ CFBundleVersion $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + LSApplicationCategoryType public.app-category.utilities NSAppTransportSecurity diff --git a/OSGKeyboard/PrivacyInfo.xcprivacy b/OSGKeyboard/PrivacyInfo.xcprivacy index 5704bed..765b862 100644 --- a/OSGKeyboard/PrivacyInfo.xcprivacy +++ b/OSGKeyboard/PrivacyInfo.xcprivacy @@ -7,7 +7,20 @@ NSPrivacyTrackingDomains NSPrivacyCollectedDataTypes - + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherUserContent + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + NSPrivacyAccessedAPITypes diff --git a/OSGKeyboard/Services/LegalLinks.swift b/OSGKeyboard/Services/LegalLinks.swift index 5479268..0c9136e 100644 --- a/OSGKeyboard/Services/LegalLinks.swift +++ b/OSGKeyboard/Services/LegalLinks.swift @@ -8,4 +8,9 @@ enum LegalLinks { static var privacyPolicyURL: URL? { URL(string: "https://hkgood.github.io/OSGKeyboard/privacy/") } + + /// Support / feedback (GitHub Issues). + static var supportURL: URL? { + URL(string: "https://github.com/hkgood/OSGKeyboard/issues") + } } diff --git a/OSGKeyboard/Views/Components/PrivacyDisclosureViews.swift b/OSGKeyboard/Views/Components/PrivacyDisclosureViews.swift new file mode 100644 index 0000000..eccf0c9 --- /dev/null +++ b/OSGKeyboard/Views/Components/PrivacyDisclosureViews.swift @@ -0,0 +1,93 @@ +// PrivacyDisclosureViews.swift +// OSGKeyboard · Main App +// +// Reusable privacy / data-flow disclosure blocks for Settings and Onboarding. + +import SwiftUI +import OSGKeyboardShared + +/// Card-style block for Full Access and similar permission explanations. +struct PrivacyInfoCard: View { + @Environment(\.themePalette) private var palette: ThemePalette + + let title: LocalizedStringKey + let bodyText: LocalizedStringKey + + var body: some View { + VStack(alignment: .leading, spacing: Spacing.xs) { + Text(title) + .font(TypeStyle.body) + .foregroundStyle(palette.textPrimary) + Text(bodyText) + .font(TypeStyle.caption2) + .foregroundStyle(palette.textSecondary) + .fixedSize(horizontal: false, vertical: true) + } + .padding(Spacing.md) + .frame(maxWidth: .infinity, alignment: .leading) + .background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous)) + .overlay( + RoundedRectangle(cornerRadius: Radius.large, style: .continuous) + .stroke(palette.divider, lineWidth: 0.5) + ) + } +} + +/// Footnote when Cloud polish is active — text goes to the user's configured API. +struct CloudPolishDisclosureBanner: View { + @Environment(\.themePalette) private var palette: ThemePalette + + var body: some View { + HStack(alignment: .top, spacing: Spacing.sm) { + Image(systemName: "info.circle") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(palette.accent) + Text("settings.privacy.cloud.body") + .font(TypeStyle.caption2) + .foregroundStyle(palette.textSecondary) + .fixedSize(horizontal: false, vertical: true) + } + .padding(Spacing.md) + .frame(maxWidth: .infinity, alignment: .leading) + .background(palette.accentMuted.opacity(0.35), in: RoundedRectangle(cornerRadius: Radius.medium, style: .continuous)) + } +} + +/// First-time confirmation before enabling Cloud polish (user-configured third-party API). +struct CloudSharingAcknowledgmentModifier: ViewModifier { + @ObservedObject var config: ProviderConfig + @Binding var isPresented: Bool + let onConfirm: () -> Void + let onCancel: () -> Void + + func body(content: Content) -> some View { + content + .alert("settings.privacy.cloud.alert.title", isPresented: $isPresented) { + Button("common.continue") { + config.hasAcknowledgedCloudSharing = true + onConfirm() + } + Button("common.cancel", role: .cancel) { + onCancel() + } + } message: { + Text("settings.privacy.cloud.alert.message") + } + } +} + +extension View { + func cloudSharingAcknowledgment( + config: ProviderConfig, + isPresented: Binding, + onConfirm: @escaping () -> Void, + onCancel: @escaping () -> Void + ) -> some View { + modifier(CloudSharingAcknowledgmentModifier( + config: config, + isPresented: isPresented, + onConfirm: onConfirm, + onCancel: onCancel + )) + } +} diff --git a/OSGKeyboard/Views/EnginePickerSection.swift b/OSGKeyboard/Views/EnginePickerSection.swift index ebd43a3..0af8be5 100644 --- a/OSGKeyboard/Views/EnginePickerSection.swift +++ b/OSGKeyboard/Views/EnginePickerSection.swift @@ -22,6 +22,8 @@ struct EnginePickerSection: View { @Environment(\.themePalette) private var palette: ThemePalette @ObservedObject var config: ProviderConfig + @State private var showCloudAcknowledgment = false + @State private var pendingEngineSelection: String? var body: some View { VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) { @@ -46,7 +48,17 @@ struct EnginePickerSection: View { RoundedRectangle(cornerRadius: Radius.large, style: .continuous) .stroke(palette.divider, lineWidth: 0.5) ) + + if config.engineMode == "cloud" { + CloudPolishDisclosureBanner() + } } + .cloudSharingAcknowledgment( + config: config, + isPresented: $showCloudAcknowledgment, + onConfirm: { applyPendingEngineSelection() }, + onCancel: { pendingEngineSelection = nil } + ) } private var localSubtitle: String { @@ -64,10 +76,13 @@ struct EnginePickerSection: View { ) -> some View { let isSelected = config.engineMode == id return Button { - withAnimation(.easeInOut(duration: 0.2)) { - config.engineMode = id - if id == "local" { config.modeId = "transcribe" } + guard config.engineMode != id else { return } + if id == "cloud", !config.hasAcknowledgedCloudSharing { + pendingEngineSelection = id + showCloudAcknowledgment = true + return } + selectEngine(id) } label: { HStack(spacing: Spacing.sm) { engineMark( @@ -97,6 +112,19 @@ struct EnginePickerSection: View { .buttonStyle(.plain) } + private func selectEngine(_ id: String) { + withAnimation(.easeInOut(duration: 0.2)) { + config.engineMode = id + if id == "local" { config.modeId = "transcribe" } + } + } + + private func applyPendingEngineSelection() { + guard let id = pendingEngineSelection else { return } + pendingEngineSelection = nil + selectEngine(id) + } + @ViewBuilder private func engineMark(assetName: String?, systemIcon: String?, isSelected: Bool) -> some View { ZStack { diff --git a/OSGKeyboard/Views/HistoryView.swift b/OSGKeyboard/Views/HistoryView.swift index 6351a2a..729b200 100644 --- a/OSGKeyboard/Views/HistoryView.swift +++ b/OSGKeyboard/Views/HistoryView.swift @@ -39,6 +39,13 @@ struct HistoryView: View { } } + Text("history.subtitle") + .font(TypeStyle.caption2) + .foregroundStyle(palette.textSecondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, Spacing.md) + .padding(.bottom, Spacing.sm) + ZStack { palette.background.ignoresSafeArea() diff --git a/OSGKeyboard/Views/OnboardingView.swift b/OSGKeyboard/Views/OnboardingView.swift index 2cc69c7..8f3e446 100644 --- a/OSGKeyboard/Views/OnboardingView.swift +++ b/OSGKeyboard/Views/OnboardingView.swift @@ -617,6 +617,13 @@ private struct EnableKeyboardPage: View { .padding(.horizontal, Spacing.xl) .padding(.top, OnboardingLayoutMetrics.heroTextGap) + PrivacyInfoCard( + title: "settings.privacy.fullAccess.title", + bodyText: "settings.privacy.fullAccess.body" + ) + .padding(.horizontal, Spacing.lg) + .padding(.top, Spacing.lg) + Button { AppPermissions.openSystemSettings() } label: { diff --git a/OSGKeyboard/Views/SettingsView.swift b/OSGKeyboard/Views/SettingsView.swift index e74be30..c347a1a 100644 --- a/OSGKeyboard/Views/SettingsView.swift +++ b/OSGKeyboard/Views/SettingsView.swift @@ -42,6 +42,7 @@ struct SettingsView: View { confirmActionTitle: "common.reset" ) { config.reset() + SpeechHistoryStore.shared.clearAll() } if presentation == .sheet { Button("common.done") { dismiss() } @@ -301,6 +302,10 @@ struct SettingsView: View { footerLinkRow(title: "settings.privacy.policy", url: url) Divider().background(palette.divider) } + if let url = LegalLinks.supportURL { + footerLinkRow(title: "settings.link.support", url: url) + Divider().background(palette.divider) + } footerLinkRow( title: "settings.link.github", url: URL(string: "https://github.com/hkgood/OSGKeyboard")! diff --git a/OSGKeyboard/en.lproj/InfoPlist.strings b/OSGKeyboard/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..4ff7086 --- /dev/null +++ b/OSGKeyboard/en.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +/* Permission prompts — English */ + +"NSMicrophoneUsageDescription" = "OSGKeyboard uses the microphone for voice dictation and keeps a background audio session active while a voice session is running."; +"NSSpeechRecognitionUsageDescription" = "OSGKeyboard uses on-device speech recognition to transcribe your voice. Audio is processed on your device and is not uploaded for transcription."; diff --git a/OSGKeyboard/en.lproj/Localizable.strings b/OSGKeyboard/en.lproj/Localizable.strings index bbd634c..1784fe1 100644 --- a/OSGKeyboard/en.lproj/Localizable.strings +++ b/OSGKeyboard/en.lproj/Localizable.strings @@ -84,7 +84,7 @@ /* Settings */ "settings.title" = "Settings"; "settings.reset.title" = "Reset all settings?"; -"settings.reset.message" = "API key, model, and base URL will be cleared."; +"settings.reset.message" = "API key, model, base URL, voice history, and cloud consent will be cleared."; "settings.reset.confirm" = "Reset all settings"; "settings.engine.title" = "Engine"; "settings.engine.subtitle" = "Pick the recognition engine. Local engine does transcription only, no API key needed."; @@ -92,7 +92,7 @@ "settings.engine.local.ios26" = "Always on-device, no network."; "settings.engine.local.legacy" = "On-device ASR. Transcription only, no polish."; "settings.engine.cloud.title" = "Cloud polish"; -"settings.engine.cloud.subtitle" = "ASR + LLM polish. API key required."; +"settings.engine.cloud.subtitle" = "On-device ASR + polish via your API. Text is sent to the endpoint you configure."; "settings.provider.title" = "Provider"; "settings.provider.subtitle" = "Pick the LLM that polishes your dictation."; "provider.openai" = "OpenAI"; @@ -123,6 +123,10 @@ "settings.privacy.policy" = "Privacy Policy"; "settings.privacy.fullAccess.title" = "About Full Access"; "settings.privacy.fullAccess.body" = "Full Access is required for the microphone and to read your API key. OSGKeyboard does not record or upload what you type with the keyboard."; +"settings.privacy.cloud.body" = "In Cloud polish mode, transcribed text is sent to the API endpoint you configure (e.g. OpenAI or your own server). OSGKeyboard does not operate servers and does not store transcripts in the cloud."; +"settings.privacy.cloud.alert.title" = "Third-party API"; +"settings.privacy.cloud.alert.message" = "Cloud polish sends transcribed text to the third-party API you configure. OSGKeyboard never stores data on our servers. Continue?"; +"settings.link.support" = "Help & Feedback"; "settings.link.github" = "GitHub"; /* App group error */ diff --git a/OSGKeyboard/zh-Hans.lproj/InfoPlist.strings b/OSGKeyboard/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000..8da774d --- /dev/null +++ b/OSGKeyboard/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +/* 权限说明 — 简体中文 */ + +"NSMicrophoneUsageDescription" = "OSGKeyboard 使用麦克风进行语音听写,并在语音会话运行期间保持后台音频会话。"; +"NSSpeechRecognitionUsageDescription" = "OSGKeyboard 使用设备端语音识别将你的语音转为文字。音频仅在设备上处理,不会上传用于转写。"; diff --git a/OSGKeyboard/zh-Hans.lproj/Localizable.strings b/OSGKeyboard/zh-Hans.lproj/Localizable.strings index d49937f..6976453 100644 --- a/OSGKeyboard/zh-Hans.lproj/Localizable.strings +++ b/OSGKeyboard/zh-Hans.lproj/Localizable.strings @@ -84,7 +84,7 @@ /* Settings */ "settings.title" = "设置"; "settings.reset.title" = "重置所有设置?"; -"settings.reset.message" = "API key、model 和 base URL 都会被清空。"; +"settings.reset.message" = "将清空 API Key、模型、接口地址、语音历史及云端确认状态。"; "settings.reset.confirm" = "重置所有设置"; "settings.engine.title" = "引擎"; "settings.engine.subtitle" = "本地不用 Key,只转文字;云端会润色,要配 Key。"; @@ -92,7 +92,7 @@ "settings.engine.local.ios26" = "全程在手机本地,不用联网"; "settings.engine.local.legacy" = "端侧 ASR,仅转录,无润色。"; "settings.engine.cloud.title" = "云端润色"; -"settings.engine.cloud.subtitle" = "先转文字,再用 AI 润色,要 API Key"; +"settings.engine.cloud.subtitle" = "本地转写 + 你配置的 API 润色,文字发往该第三方服务"; "settings.provider.title" = "提供商"; "settings.provider.subtitle" = "选择 LLM 提供商。"; "provider.openai" = "OpenAI"; @@ -123,6 +123,10 @@ "settings.privacy.policy" = "隐私政策"; "settings.privacy.fullAccess.title" = "关于完全访问"; "settings.privacy.fullAccess.body" = "用来调用麦克风和读取 API Key;不会读取或上传你的输入内容。"; +"settings.privacy.cloud.body" = "云端润色模式下,转写文字会发送到你配置的 API(如 OpenAI 或自建服务)。OSGKeyboard 不运营服务器,也不会把转写内容存到云端。"; +"settings.privacy.cloud.alert.title" = "第三方 API"; +"settings.privacy.cloud.alert.message" = "云端润色会把转写文字发到你配置的第三方 API。OSGKeyboard 不会在自有服务器上存储数据。是否继续?"; +"settings.link.support" = "帮助与反馈"; "settings.link.github" = "GitHub"; /* App group error */ diff --git a/OSGKeyboardExt/Services/HostAppLauncher.swift b/OSGKeyboardExt/Services/HostAppLauncher.swift index 67274a4..6c418e8 100644 --- a/OSGKeyboardExt/Services/HostAppLauncher.swift +++ b/OSGKeyboardExt/Services/HostAppLauncher.swift @@ -1,10 +1,9 @@ // HostAppLauncher.swift // OSGKeyboard · Keyboard Extension // -// Opens the host app via URL using every extension-safe strategy: +// Opens the host app via URL using extension-safe strategies: // 1. `extensionContext.open` (official) // 2. Responder-chain `UIApplication.open` (TypeWhisper pattern) -// 3. `sharedApplication` KVC fallback (common in full-access keyboards) import UIKit @@ -22,23 +21,12 @@ enum HostAppLauncher { completion(true) return } - completion(openViaFallback(url, from: controller)) + completion(openViaResponderChain(url, from: controller)) } } return } - completion(openViaFallback(url, from: controller)) - } - - @MainActor - private static func openViaFallback( - _ url: URL, - from controller: KeyboardViewController - ) -> Bool { - if openViaResponderChain(url, from: controller) { - return true - } - return openViaSharedApplication(url) + completion(openViaResponderChain(url, from: controller)) } @MainActor @@ -56,15 +44,4 @@ enum HostAppLauncher { } return false } - - @MainActor - private static func openViaSharedApplication(_ url: URL) -> Bool { - guard - let application = UIApplication.value(forKeyPath: "sharedApplication") as? UIApplication - else { - return false - } - application.open(url, options: [:]) { _ in } - return true - } } diff --git a/OSGKeyboardShared/Models/ProviderConfig.swift b/OSGKeyboardShared/Models/ProviderConfig.swift index b763290..60886d0 100644 --- a/OSGKeyboardShared/Models/ProviderConfig.swift +++ b/OSGKeyboardShared/Models/ProviderConfig.swift @@ -29,6 +29,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable { static let engineMode = "config.engineMode" static let hasCompletedOnboarding = "config.hasCompletedOnboarding" static let onboardingPage = "config.onboardingPage" + static let hasAcknowledgedCloudSharing = "config.hasAcknowledgedCloudSharing" } @Published public var providerId: String { @@ -80,6 +81,10 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable { @Published public var onboardingPage: Int { didSet { defaults.set(onboardingPage, forKey: Key.onboardingPage) } } + /// User confirmed that Cloud polish sends transcripts to their configured third-party API. + @Published public var hasAcknowledgedCloudSharing: Bool { + didSet { defaults.set(hasAcknowledgedCloudSharing, forKey: Key.hasAcknowledgedCloudSharing) } + } public var isConfigured: Bool { // Local engine (on-device ASR only) doesn't need an API key, @@ -125,6 +130,7 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable { self.hasCompletedOnboarding = resolvedDefaults.bool(forKey: Key.hasCompletedOnboarding) let savedPage = resolvedDefaults.integer(forKey: Key.onboardingPage) self.onboardingPage = savedPage > 0 ? savedPage : 0 + self.hasAcknowledgedCloudSharing = resolvedDefaults.bool(forKey: Key.hasAcknowledgedCloudSharing) } /// Read the API key from the Keychain, falling back to a one-time @@ -170,5 +176,6 @@ public final class ProviderConfig: ObservableObject, @unchecked Sendable { apiKey = "" model = preset.defaultModel systemPrompt = AppGroupStore.defaultSystemPrompt(for: "openai") + hasAcknowledgedCloudSharing = false } } diff --git a/README.md b/README.md index 2309efe..2e0d39c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # OSGKeyboard > Hold a key, speak, release — AI-polished text appears at your cursor in any app. -> An open-source, custom-keyboard-based voice input tool for iOS 26+, inspired by [Typeless](https://typeless.com) and [OpenLess](https://github.com/Open-Less/openless). +> A source-available, custom-keyboard-based voice input tool for iOS 26+, inspired by [Typeless](https://typeless.com) and [OpenLess](https://github.com/Open-Less/openless). ![Platform](https://img.shields.io/badge/platform-iOS%2026%2B-0078D4?logo=apple) ![Swift](https://img.shields.io/badge/Swift-6.0-FA7343?logo=swift) -![License](https://img.shields.io/badge/license-MIT-green) +![License](https://img.shields.io/badge/license-Source%20Available-blue) ![CI](https://github.com/hkgood/OSGKeyboard/actions/workflows/ci.yml/badge.svg) [中文 README](./README.zh.md) @@ -14,7 +14,7 @@ ## What is it? -OSGKeyboard is a free, open alternative to commercial voice-input tools. It runs as a **Custom Keyboard Extension** on iOS, so you can use it in **any app** — Messages, Notes, Mail, ChatGPT, Claude, Cursor, you name it. +OSGKeyboard is a free, source-available alternative to commercial voice-input tools. It runs as a **Custom Keyboard Extension** on iOS, so you can use it in **any app** — Messages, Notes, Mail, ChatGPT, Claude, Cursor, you name it. 1. Press and hold the mic key 2. Speak naturally @@ -138,7 +138,7 @@ That's it. No other code changes required. ## License -[MIT](./LICENSE) — use it, fork it, ship it. No warranty. +[OSGKeyboard Source Available License](./LICENSE) — personal learning and non-commercial local use only. No commercial use, redistribution, or public forks without permission. Commercial licensing: [rocky.hk@gmail.com](mailto:rocky.hk@gmail.com). --- diff --git a/README.zh.md b/README.zh.md index 021ddcc..7eb1924 100644 --- a/README.zh.md +++ b/README.zh.md @@ -1,11 +1,11 @@ # OSGKeyboard > 按住说话,松开即得 AI 润色文字,插入任意 App 的光标处。 -> 一款开源的 iOS 自定义键盘语音输入工具,灵感来自 [Typeless](https://typeless.com) 和 [OpenLess](https://github.com/Open-Less/openless)。 +> 一款源码可见的 iOS 自定义键盘语音输入工具,灵感来自 [Typeless](https://typeless.com) 和 [OpenLess](https://github.com/Open-Less/openless)。 ![Platform](https://img.shields.io/badge/platform-iOS%2026%2B-0078D4?logo=apple) ![Swift](https://img.shields.io/badge/Swift-6.0-FA7343?logo=swift) -![License](https://img.shields.io/badge/license-MIT-green) +![License](https://img.shields.io/badge/license-Source%20Available-blue) [English README](./README.md) @@ -13,7 +13,7 @@ ## 这是什么? -OSGKeyboard 是商业语音输入工具的免费开源替代。它以 **iOS 自定义键盘扩展** 的形式运行,所以你可以在 **任何 App** 里使用 —— 微信、备忘录、邮件、ChatGPT、Claude、Cursor,无所不能。 +OSGKeyboard 是商业语音输入工具的免费、源码可见替代方案。它以 **iOS 自定义键盘扩展** 的形式运行,所以你可以在 **任何 App** 里使用 —— 微信、备忘录、邮件、ChatGPT、Claude、Cursor,无所不能。 1. 长按麦克风键 2. 自由说话 @@ -137,7 +137,7 @@ LLMProvider( ## 许可 -[MIT](./LICENSE) —— 使用、修改、商用均可。无任何担保。 +[OSGKeyboard 源码可见许可协议](./LICENSE) —— 仅限个人学习与非商用本地使用;禁止商用、再分发及公开 fork。商业授权请联系 [rocky.hk@gmail.com](mailto:rocky.hk@gmail.com)。 --- diff --git a/docs/index.html b/docs/index.html index b49fada..4feb030 100644 --- a/docs/index.html +++ b/docs/index.html @@ -719,7 +719,7 @@