diff --git a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml index de97b7dfe..c2d0f068a 100644 --- a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml +++ b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml @@ -1,6 +1,6 @@ name: 反馈问题 description: 反馈问题 -title: "🐞 反馈问题:" +title: "🐞 反馈问题:{{请填写标题,不要留空}}" labels: ["bug"] body: @@ -22,18 +22,30 @@ body: id: description attributes: label: 问题描述 - description: 请详细描述你所遇到的问题,确保开发者能够理解、重现该问题。如果上下文信息不足,开发者无法定位,问题会被降低优先级或忽略。 + description: 请详细描述你所遇到的问题,确保开发者能够理解、重现该问题。如果上下文信息不足,导致开发者无法定位,问题会被降低优先级或忽略。 placeholder: 问题描述 validations: required: true + - type: dropdown + id: reproducible + attributes: + label: 该问题是否可以稳定重现? + multiple: false + options: + - 可重现 + - 不可重现 + validations: + required: true + - type: textarea id: reproduce attributes: label: 重现步骤 description: | - 请描述如何重现该问题。如果该问题是偶发性的,或者需要特定的操作步骤才能重现,请一定要详细提供重现步骤,否则开发者无法定位问题。 - (如果遇到一些很奇怪的问题,可以先尝试重启 Easydict,重启电脑,或卸载重装应用,看能否解决问题 🤔) + 如果该问题可重现,请一定要详细提供重现步骤,否则开发者无法定位问题。 + 如果该问题是偶发性的,可以先尝试重启 Easydict,重启电脑,或卸载重装应用,看能否解决问题 🤔 + 注意:鉴于开发者精力有限,目前只会处理可稳定重现的问题。对于不可重现的问题,当前只会简单记录下来,观察后续。 placeholder: 重现步骤 validations: required: true diff --git a/.github/ISSUE_TEMPLATE/cn_feature_request.yml b/.github/ISSUE_TEMPLATE/cn_feature_request.yml index 93baaa805..07b4f5297 100644 --- a/.github/ISSUE_TEMPLATE/cn_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/cn_feature_request.yml @@ -1,6 +1,6 @@ name: 功能建议 description: 功能建议 -title: "🚀 功能建议:" +title: "🚀 功能建议:{{请填写标题,不要留空}}" labels: ["enhancement"] body: @@ -15,6 +15,8 @@ body: required: true - label: Easydict 已升级到 [最新版本](https://github.com/tisfeng/Easydict/releases) required: true + - label: 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** + required: true - type: textarea id: feature_description @@ -29,7 +31,9 @@ body: id: feature_usecase attributes: label: 使用场景 - description: 请描述你希望功能的使用场景,有无其他类似可供参考的 App 功能等。 + description: | + 请描述你希望功能的使用场景,有无其他类似可供参考的 App 功能等。 + 如果该功能没有明确的使用场景,或是无法被开发者理解,可能会被降低优先级或忽略,因此请务必清晰描述。 placeholder: 使用场景 validations: required: true diff --git a/.github/ISSUE_TEMPLATE/en_bug_report.yml b/.github/ISSUE_TEMPLATE/en_bug_report.yml index fc13b2380..2f87973a8 100644 --- a/.github/ISSUE_TEMPLATE/en_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/en_bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Report an issue -title: "🐞 Bug Report: " +title: "🐞 Bug Report: {{Please fill in the title, don't leave it blank}} " labels: ["bug"] body: @@ -27,14 +27,26 @@ body: validations: required: true + - type: dropdown + id: reproducible + attributes: + label: Is the issue consistently reproducible? + multiple: false + options: + - Reproducible + - Non-reproducible + validations: + required: true + - type: textarea id: reproduce attributes: - label: Reproduction steps + label: Steps to Reproduce description: | - Please describe how to reproduce the problem. If the problem is episodic or requires specific action steps to reproduce, please describe it in as much detail as possible, otherwise the developer will not be able to locate the problem. - (If the problem is episodic, try restarting Easydict, restarting the device, or uninstalling and reinstalling the app to see if that solves the problem 🤔) - placeholder: Reproduction steps + If the issue is reproducible, please provide detailed steps to reproduce it. Otherwise, the developer may not be able to locate the issue. + If the issue is intermittent, you can try restarting Easydict, restarting your computer, or uninstalling and reinstalling the application to see if the problem can be resolved 🤔 + Note: Given the limited resources of the developer, only issues that can be reliably reproduced will be addressed at this time. For non-reproducible issues, they will be simply recorded for future observation. + placeholder: Steps to Reproduce validations: required: true diff --git a/.github/ISSUE_TEMPLATE/en_feature_request.yml b/.github/ISSUE_TEMPLATE/en_feature_request.yml index 5b50e36f2..f63be4bf5 100644 --- a/.github/ISSUE_TEMPLATE/en_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/en_feature_request.yml @@ -1,6 +1,6 @@ name: Feature request description: Request a new feature -title: "🚀 Feature Request: " +title: "🚀 Feature Request: {{Please fill in the title, don't leave it blank}}" labels: ["enhancement"] body: @@ -15,6 +15,8 @@ body: required: true - label: Easydict has been upgraded to the [latest version](https://github.com/tisfeng/Easydict/releases) required: true + - label: I understand and agree to the above, and understand that the project maintainer has limited energy, **issues that do not follow the rules may be ignored or closed directly** + required: true - type: textarea id: feature_description @@ -29,7 +31,9 @@ body: id: feature_usecase attributes: label: Use case - description: Please describe the use case of the feature you're requesting, and whether there are any similar app features for reference. + description: | + Please describe the use case of the feature you're requesting, and whether there are any similar app features for reference. + If the feature has no actual usage scenarios or is not well understood by the developers it might get deprioritized or even ignored, so be sure to describe it clearly. placeholder: Use case validations: required: true diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 00aad8201..c120c7813 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 03008B2B2940D3230062B821 /* EZDeepLTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B2A2940D3230062B821 /* EZDeepLTranslate.m */; }; 03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B2D2941956D0062B821 /* EZURLSchemeHandler.m */; }; 03008B3F29444B0A0062B821 /* NSView+EZAnimatedHidden.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B3E29444B0A0062B821 /* NSView+EZAnimatedHidden.m */; }; + 03022F192B3591AE00B63209 /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F182B3591AE00B63209 /* GoogleGenerativeAI */; }; 03022F1C2B35DEBA00B63209 /* Hue in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F1B2B35DEBA00B63209 /* Hue */; }; 03022F1F2B36CF3100B63209 /* SwiftShell in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F1E2B36CF3100B63209 /* SwiftShell */; }; 03022F222B36D1A400B63209 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F212B36D1A400B63209 /* SnapKit */; }; @@ -19,6 +20,7 @@ 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1EC292B439A00AFB76A /* EZTextView.m */; }; 0309E1F0292B4A5E00AFB76A /* NSView+EZGetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1EF292B4A5E00AFB76A /* NSView+EZGetViewController.m */; }; 0309E1F4292BD6A100AFB76A /* EZQueryModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1F3292BD6A100AFB76A /* EZQueryModel.m */; }; + 030DB2612B56CC6500E27DEA /* BingLanguageVoice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */; }; 0310C8272A94F5DF00B1D81E /* apple-dictionary.html in Resources */ = {isa = PBXBuildFile; fileRef = 0310C8262A94EFA100B1D81E /* apple-dictionary.html */; }; 0313F8702AD5577400A5CFB0 /* EasydictTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0313F86F2AD5577400A5CFB0 /* EasydictTests.m */; }; 031DBD792AE01E130071CF85 /* easydict in Resources */ = {isa = PBXBuildFile; fileRef = 031DBD782AE01E130071CF85 /* easydict */; }; @@ -66,10 +68,10 @@ 037852B329583F5200D0E2CF /* EZServiceCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 037852B229583F5200D0E2CF /* EZServiceCell.m */; }; 037852B629588EDE00D0E2CF /* EZCustomTableRowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 037852B529588EDE00D0E2CF /* EZCustomTableRowView.m */; }; 037852B9295D49F900D0E2CF /* EZTableRowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 037852B8295D49F900D0E2CF /* EZTableRowView.m */; }; - 037BEFCD2A98FDF700D0F17F /* EZBingLanguageVoice.m in Sources */ = {isa = PBXBuildFile; fileRef = 037BEFCC2A98FDF700D0F17F /* EZBingLanguageVoice.m */; }; 037E006D2B3DC098006491C6 /* EZOpenAIService+EZPromptMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 03CF27FA2B3A787900E19B57 /* EZOpenAIService+EZPromptMessages.m */; }; 038030952B4106800009230C /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 038030942B4106800009230C /* CocoaLumberjack */; }; 038030972B4106800009230C /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 038030962B4106800009230C /* CocoaLumberjackSwift */; }; + 03832F542B5F6BE200D0DC64 /* AdvancedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */; }; 0383914C292FBE120009828C /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = 03839140292FBE120009828C /* Main.strings */; }; 0383914D292FBE120009828C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03839143292FBE120009828C /* Assets.xcassets */; }; 0383914E292FBE120009828C /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03839144292FBE120009828C /* ViewController.m */; }; @@ -82,6 +84,7 @@ 03882F9029D95044005B5A52 /* ToastWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03882F8829D95044005B5A52 /* ToastWindowController.xib */; }; 03882F9129D95044005B5A52 /* CTCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 03882F8929D95044005B5A52 /* CTCommon.m */; }; 03882F9229D95044005B5A52 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 03882F8C29D95044005B5A52 /* Info.plist */; }; + 038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A723F2B62C0B9004995E3 /* String+Regex.swift */; }; 038EA1AA2B41169C008A6DD1 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1A92B41169C008A6DD1 /* ZipArchive */; }; 038EA1AD2B41282F008A6DD1 /* MJExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1AC2B41282F008A6DD1 /* MJExtension */; }; 0396D611292C932F006A11D9 /* EZSelectLanguageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */; }; @@ -228,6 +231,25 @@ 03FC699A2B39D13A0035D2DA /* EZOpenAIChatResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 03FC69992B39D13A0035D2DA /* EZOpenAIChatResponse.m */; }; 03FD68BB2B1DC59600FD388E /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03FD68BA2B1DC59600FD388E /* CryptoSwift */; }; 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */; }; + 0A057D6D2B499A000025C51D /* ServiceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A057D6C2B499A000025C51D /* ServiceTab.swift */; }; + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */; }; + 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */; }; + 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */; }; + 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8685C72B552A590022534F /* DisabledAppTab.swift */; }; + 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */; }; + 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */; }; + 0AC8A8352B6641A7006DA5CC /* TencentService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8342B6641A7006DA5CC /* TencentService+ConfigurableService.swift */; }; + 0AC8A8372B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8362B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift */; }; + 0AC8A8392B666F07006DA5CC /* CaiyunService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8382B666F07006DA5CC /* CaiyunService+ConfigurableService.swift */; }; + 0AC8A83B2B6682D4006DA5CC /* AliService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A83A2B6682D4006DA5CC /* AliService+ConfigurableService.swift */; }; + 0AC8A83D2B6685EE006DA5CC /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */; }; + 0AC8A83F2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */; }; + 0AC8A8412B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8402B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift */; }; + 0AC8A8432B6957B0006DA5CC /* BingService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8422B6957B0006DA5CC /* BingService+ConfigurableService.swift */; }; + 0AC8A8452B6A4D97006DA5CC /* ServiceConfigurationCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8442B6A4D97006DA5CC /* ServiceConfigurationCells.swift */; }; + 0AC8A8472B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8462B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift */; }; + 0AC8A84B2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A84A2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift */; }; + 0AC8A84F2B6DFDD4006DA5CC /* SettingsAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 0AC8A84E2B6DFDD4006DA5CC /* SettingsAccess */; }; 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */; }; 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF62B0DFF9000A7D372 /* EZNiuTransTranslate.m */; }; 2721E4D02AFE920700A059AC /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 2721E4CF2AFE920700A059AC /* Alamofire */; }; @@ -249,17 +271,47 @@ 62E2BF4B2B4082BA00E42D38 /* AliResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E2BF482B4082BA00E42D38 /* AliResponse.swift */; }; 62E2BF4C2B4082BA00E42D38 /* AliTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E2BF492B4082BA00E42D38 /* AliTranslateType.swift */; }; 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 62ED29A12B15F1F500901F51 /* EZWrapView.m */; }; + 960835502B6791F200C6A931 /* Shortcut+Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */; }; + 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96099AE12B5D40330055C4DD /* ShortcutTab.swift */; }; + 9627F9382B59956800B1E999 /* GlobalShortcutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9352B59956800B1E999 /* GlobalShortcutSetting.swift */; }; + 9627F9392B59956800B1E999 /* KeyHolderWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9362B59956800B1E999 /* KeyHolderWrapper.swift */; }; + 9643D9392B6F49E0000FBEA6 /* AppShortcutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9382B6F49E0000FBEA6 /* AppShortcutSetting.swift */; }; + 9643D93D2B6F829C000FBEA6 /* MainMenuCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */; }; + 9643D9402B6FC426000FBEA6 /* MainMenuShortcutCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */; }; + 9643D9422B6FE4AF000FBEA6 /* Shortcut+Bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */; }; + 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */; }; + 9643D9462B71D103000FBEA6 /* KeyHolderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */; }; + 9643D94A2B71EABE000FBEA6 /* KeyHolderAlterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */; }; + 9643D94C2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */; }; + 9643D9562B73B3CD000FBEA6 /* Shortcut+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */; }; 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */; }; + 967712EA2B5B913600105E0F /* KeyHolder in Frameworks */ = {isa = PBXBuildFile; productRef = 967712E92B5B913600105E0F /* KeyHolder */; }; + 967712EE2B5B943400105E0F /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967712ED2B5B943400105E0F /* Shortcut.swift */; }; A0B65CA0F31AC8ECFB8347CC /* Pods_EasydictTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 378E73A7EA8FC8FB9C975A63 /* Pods_EasydictTests.framework */; }; B87AC7E36367075BA5D13234 /* Pods_Easydict.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */; }; + C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415C0AC2B450D4800A9D231 /* GeminiService.swift */; }; C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01E82B12B3C80025EE8E /* TencentService.swift */; }; C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */; }; C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */; }; C4DE3D6D2AC00EB500C2B85D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = C4DE3D6C2AC00EB500C2B85D /* Localizable.xcstrings */; }; C98CAE75239F4619005F7DCA /* EasydictHelper.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = C90BE309239F38EB00ADE88B /* EasydictHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DC3C643F2B187119008EEDD8 /* ChangeFontSizeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3C643E2B187119008EEDD8 /* ChangeFontSizeView.swift */; }; + DC46DF802B4417B900DEAE3E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC46DF7F2B4417B900DEAE3E /* Configuration.swift */; }; DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */; }; DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C882B3969510055EFFC /* Appearance.swift */; }; + DCF176F22B57CED700CA6026 /* Configuration+UserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */; }; + EA1013442B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */; }; + EA3B81F92B5254AA004C0E8B /* Configuration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */; }; + EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = EA3B81FB2B52555C004C0E8B /* Defaults */; }; + EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */; }; + EA9943E82B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */; }; + EA9943EE2B5353AB00EE7B97 /* WindowTypeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */; }; + EA9943F02B5354C400EE7B97 /* ShowWindowPositionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943EF2B5354C400EE7B97 /* ShowWindowPositionExtensions.swift */; }; + EA9943F22B5358BF00EE7B97 /* LanguageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943F12B5358BF00EE7B97 /* LanguageExtensions.swift */; }; + EAE3D3502B62E9DE001EE3E3 /* GlobalContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE3D34F2B62E9DE001EE3E3 /* GlobalContext.swift */; }; + EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */; }; + EAED41EF2B54B1430005FE0A /* ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */; }; + EAED41F22B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -312,6 +364,7 @@ 0309E1EF292B4A5E00AFB76A /* NSView+EZGetViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSView+EZGetViewController.m"; sourceTree = ""; }; 0309E1F2292BD6A100AFB76A /* EZQueryModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZQueryModel.h; sourceTree = ""; }; 0309E1F3292BD6A100AFB76A /* EZQueryModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZQueryModel.m; sourceTree = ""; }; + 030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BingLanguageVoice.swift; sourceTree = ""; }; 0310C8262A94EFA100B1D81E /* apple-dictionary.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "apple-dictionary.html"; sourceTree = ""; }; 0313F86D2AD5577400A5CFB0 /* EasydictTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EasydictTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0313F86F2AD5577400A5CFB0 /* EasydictTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EasydictTests.m; sourceTree = ""; }; @@ -401,8 +454,7 @@ 037852B529588EDE00D0E2CF /* EZCustomTableRowView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZCustomTableRowView.m; sourceTree = ""; }; 037852B7295D49F900D0E2CF /* EZTableRowView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZTableRowView.h; sourceTree = ""; }; 037852B8295D49F900D0E2CF /* EZTableRowView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZTableRowView.m; sourceTree = ""; }; - 037BEFCB2A98FDF700D0F17F /* EZBingLanguageVoice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZBingLanguageVoice.h; sourceTree = ""; }; - 037BEFCC2A98FDF700D0F17F /* EZBingLanguageVoice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZBingLanguageVoice.m; sourceTree = ""; }; + 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedTab.swift; sourceTree = ""; }; 03839141292FBE120009828C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 03839142292FBE120009828C /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 03839143292FBE120009828C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -424,6 +476,7 @@ 03882F8A29D95044005B5A52 /* CTView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTView.h; sourceTree = ""; }; 03882F8B29D95044005B5A52 /* CoolToast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoolToast.h; sourceTree = ""; }; 03882F8C29D95044005B5A52 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 038A723F2B62C0B9004995E3 /* String+Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Regex.swift"; sourceTree = ""; }; 0396D60F292C932F006A11D9 /* EZSelectLanguageCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZSelectLanguageCell.h; sourceTree = ""; }; 0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZSelectLanguageCell.m; sourceTree = ""; }; 0396D613292CC4C3006A11D9 /* EZLocalStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZLocalStorage.h; sourceTree = ""; }; @@ -689,6 +742,24 @@ 03FC69992B39D13A0035D2DA /* EZOpenAIChatResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EZOpenAIChatResponse.m; sourceTree = ""; }; 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+EncryptAES.swift"; sourceTree = ""; }; 06E15747A7BD34D510ADC6A8 /* Pods-Easydict.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.debug.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.debug.xcconfig"; sourceTree = ""; }; + 0A057D6C2B499A000025C51D /* ServiceTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTab.swift; sourceTree = ""; }; + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppInfo.swift"; sourceTree = ""; }; + 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+DidSet.swift"; sourceTree = ""; }; + 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name.swift"; sourceTree = ""; }; + 0A8685C72B552A590022534F /* DisabledAppTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppTab.swift; sourceTree = ""; }; + 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; + 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapHandlerView.swift; sourceTree = ""; }; + 0AC8A8342B6641A7006DA5CC /* TencentService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TencentService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8362B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NiuTransTranslate+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8382B666F07006DA5CC /* CaiyunService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaiyunService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A83A2B6682D4006DA5CC /* AliService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AliService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; + 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceSecretConfigreValidatable.swift; sourceTree = ""; }; + 0AC8A8402B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeepLTranslate+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8422B6957B0006DA5CC /* BingService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BingService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8442B6A4D97006DA5CC /* ServiceConfigurationCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceConfigurationCells.swift; sourceTree = ""; }; + 0AC8A8462B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceConfigurationSecretSectionView.swift; sourceTree = ""; }; + 0AC8A84A2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GeminiService+ConfigurableService.swift"; sourceTree = ""; }; 17BCAEF32B0DFF9000A7D372 /* EZNiuTransTranslateResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslateResponse.h; sourceTree = ""; }; 17BCAEF42B0DFF9000A7D372 /* EZNiuTransTranslate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslate.h; sourceTree = ""; }; 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EZNiuTransTranslateResponse.m; sourceTree = ""; }; @@ -724,9 +795,24 @@ 62ED29A12B15F1F500901F51 /* EZWrapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZWrapView.m; sourceTree = ""; }; 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Easydict.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91E3E579C6DB88658B4BB102 /* Pods-Easydict.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.release.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.release.xcconfig"; sourceTree = ""; }; + 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Validator.swift"; sourceTree = ""; }; + 96099AE12B5D40330055C4DD /* ShortcutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutTab.swift; sourceTree = ""; }; + 9627F9352B59956800B1E999 /* GlobalShortcutSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalShortcutSetting.swift; sourceTree = ""; }; + 9627F9362B59956800B1E999 /* KeyHolderWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyHolderWrapper.swift; sourceTree = ""; }; + 9643D9382B6F49E0000FBEA6 /* AppShortcutSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutSetting.swift; sourceTree = ""; }; + 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuCommand.swift; sourceTree = ""; }; + 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommand.swift; sourceTree = ""; }; + 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Bind.swift"; sourceTree = ""; }; + 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Default.swift"; sourceTree = ""; }; + 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderRowView.swift; sourceTree = ""; }; + 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderAlterView.swift; sourceTree = ""; }; + 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommandItem.swift; sourceTree = ""; }; + 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Menu.swift"; sourceTree = ""; }; 9672D7D02B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MASShortcutBinder+EZMASShortcutBinder.h"; sourceTree = ""; }; 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutBinder+EZMASShortcutBinder.m"; sourceTree = ""; }; + 967712ED2B5B943400105E0F /* Shortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcut.swift; sourceTree = ""; }; A230E9A2358C7FBC7FB26189 /* Pods-EasydictTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EasydictTests.debug.xcconfig"; path = "Target Support Files/Pods-EasydictTests/Pods-EasydictTests.debug.xcconfig"; sourceTree = ""; }; + C415C0AC2B450D4800A9D231 /* GeminiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiService.swift; sourceTree = ""; }; C4DD01E82B12B3C80025EE8E /* TencentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentService.swift; sourceTree = ""; }; C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentResponse.swift; sourceTree = ""; }; C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentTranslateType.swift; sourceTree = ""; }; @@ -735,8 +821,21 @@ C90BE309239F38EB00ADE88B /* EasydictHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EasydictHelper.app; sourceTree = BUILT_PRODUCTS_DIR; }; C99EEB182385796700FEE666 /* Easydict-debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Easydict-debug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; DC3C643E2B187119008EEDD8 /* ChangeFontSizeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeFontSizeView.swift; sourceTree = ""; }; + DC46DF7F2B4417B900DEAE3E /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeHintView.swift; sourceTree = ""; }; DC6D9C882B3969510055EFFC /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; + DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+UserData.swift"; sourceTree = ""; }; + EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyCombo+Defaults.Serializable.swift"; sourceTree = ""; }; + EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+Defaults.swift"; sourceTree = ""; }; + EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSServiceType.swift; sourceTree = ""; }; + EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageDetectOptimizeExtensions.swift; sourceTree = ""; }; + EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTypeExtensions.swift; sourceTree = ""; }; + EA9943EF2B5354C400EE7B97 /* ShowWindowPositionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowWindowPositionExtensions.swift; sourceTree = ""; }; + EA9943F12B5358BF00EE7B97 /* LanguageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageExtensions.swift; sourceTree = ""; }; + EAE3D34F2B62E9DE001EE3E3 /* GlobalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalContext.swift; sourceTree = ""; }; + EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceConfigurationSection.swift; sourceTree = ""; }; + EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableService.swift; sourceTree = ""; }; + EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAIService+ConfigurableService.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -766,6 +865,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0AC8A84F2B6DFDD4006DA5CC /* SettingsAccess in Frameworks */, + 03022F192B3591AE00B63209 /* GoogleGenerativeAI in Frameworks */, 2721E4D02AFE920700A059AC /* Alamofire in Frameworks */, 03022F1F2B36CF3100B63209 /* SwiftShell in Frameworks */, 038030952B4106800009230C /* CocoaLumberjack in Frameworks */, @@ -778,8 +879,10 @@ 03A830922B4073E700112834 /* AppCenterCrashes in Frameworks */, 03022F1C2B35DEBA00B63209 /* Hue in Frameworks */, 03A830952B4076FC00112834 /* FirebaseAnalyticsSwift in Frameworks */, + EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */, 03CF27FE2B3DA7D500E19B57 /* Realm in Frameworks */, 03A830902B4073E700112834 /* AppCenterAnalytics in Frameworks */, + 967712EA2B5B913600105E0F /* KeyHolder in Frameworks */, 03B63ABF2A86967800E155ED /* CoreServices.framework in Frameworks */, 038030972B4106800009230C /* CocoaLumberjackSwift in Frameworks */, 038EA1AA2B41169C008A6DD1 /* ZipArchive in Frameworks */, @@ -1157,6 +1260,14 @@ path = Kit; sourceTree = ""; }; + 038A723E2B62C07B004995E3 /* String */ = { + isa = PBXGroup; + children = ( + 038A723F2B62C0B9004995E3 /* String+Regex.swift */, + ); + path = String; + sourceTree = ""; + }; 0396D612292CBDFD006A11D9 /* Storage */ = { isa = PBXGroup; children = ( @@ -1265,6 +1376,8 @@ 03542A572937CC3200C34C33 /* EZConfiguration.m */, 03D8A65A2A433B4100D9A968 /* EZConfiguration+EZUserData.h */, 03D8A65B2A433B4100D9A968 /* EZConfiguration+EZUserData.m */, + DC46DF7F2B4417B900DEAE3E /* Configuration.swift */, + DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */, DC6D9C882B3969510055EFFC /* Appearance.swift */, ); path = Configuration; @@ -1274,6 +1387,7 @@ isa = PBXGroup; children = ( 62E2BF462B4082BA00E42D38 /* Ali */, + C415C0AB2B450C4500A9D231 /* Gemini */, 17BCAEF22B0DFF9000A7D372 /* Niutrans */, 2746AEBF2AF95040005FE0A1 /* Caiyun */, C4DD01E72B12B3B00025EE8E /* Tencent */, @@ -1792,6 +1906,9 @@ 03CF88602B137ECB0030C199 /* Swift */ = { isa = PBXGroup; children = ( + 0A2A05A42B59755F00EEA142 /* Bundle */, + 0A2BA9622B4A3CBB002872A4 /* Notification */, + 0A2BA95E2B49A967002872A4 /* Binding */, 03FD68BC2B1E14B500FD388E /* String */, 03CF88612B137ED60030C199 /* Array */, ); @@ -1970,6 +2087,30 @@ path = String; sourceTree = ""; }; + 0A2A05A42B59755F00EEA142 /* Bundle */ = { + isa = PBXGroup; + children = ( + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */, + ); + path = Bundle; + sourceTree = ""; + }; + 0A2BA95E2B49A967002872A4 /* Binding */ = { + isa = PBXGroup; + children = ( + 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */, + ); + path = Binding; + sourceTree = ""; + }; + 0A2BA9622B4A3CBB002872A4 /* Notification */ = { + isa = PBXGroup; + children = ( + 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */, + ); + path = Notification; + sourceTree = ""; + }; 17BCAEF22B0DFF9000A7D372 /* Niutrans */ = { isa = PBXGroup; children = ( @@ -1994,6 +2135,10 @@ 27FE98032B3DCA9F000AD654 /* NewApp */ = { isa = PBXGroup; children = ( + 967712EB2B5B93E200105E0F /* Feature */, + EA9943E12B534C2900EE7B97 /* Model */, + EA9943DD2B534BAE00EE7B97 /* Utility */, + EA3B81F72B52549B004C0E8B /* Configuration */, 27FE95262B3DC55F000AD654 /* EasydictApp.swift */, 27FE98042B3DCB09000AD654 /* NewAppManager.swift */, 27FE98062B3DD525000AD654 /* View */, @@ -2004,7 +2149,10 @@ 27FE98062B3DD525000AD654 /* View */ = { isa = PBXGroup; children = ( + 9643D93E2B6FC405000FBEA6 /* MenuView */, 27FE980A2B3DD5D1000AD654 /* MenuItemView.swift */, + 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */, + 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */, 27FE98072B3DD52B000AD654 /* SettingView */, ); path = View; @@ -2022,9 +2170,15 @@ 27FE980C2B3DD749000AD654 /* Tabs */ = { isa = PBXGroup; children = ( + 9627F9332B59956800B1E999 /* View */, + EAED41EA2B54A4900005FE0A /* ServiceConfiguration */, 278540332B3DE04F004E9488 /* GeneralTab.swift */, + 0A057D6C2B499A000025C51D /* ServiceTab.swift */, + 0A8685C72B552A590022534F /* DisabledAppTab.swift */, 276742042B3DC230002A2C75 /* PrivacyTab.swift */, 276742052B3DC230002A2C75 /* AboutTab.swift */, + 96099AE12B5D40330055C4DD /* ShortcutTab.swift */, + 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */, ); path = Tabs; sourceTree = ""; @@ -2040,8 +2194,7 @@ 6295DE302A84D82E006145F4 /* EZBingTranslateModel.m */, 6295DE322A84EF76006145F4 /* EZBingLookupModel.h */, 6295DE332A84EF76006145F4 /* EZBingLookupModel.m */, - 037BEFCB2A98FDF700D0F17F /* EZBingLanguageVoice.h */, - 037BEFCC2A98FDF700D0F17F /* EZBingLanguageVoice.m */, + 030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */, 03F639932AA6CFBB009B9914 /* EZBingConfig.h */, 03F639942AA6CFBB009B9914 /* EZBingConfig.m */, ); @@ -2077,6 +2230,56 @@ name = Frameworks; sourceTree = ""; }; + 9627F9332B59956800B1E999 /* View */ = { + isa = PBXGroup; + children = ( + 9627F9342B59956800B1E999 /* Shortcut */, + ); + path = View; + sourceTree = ""; + }; + 9627F9342B59956800B1E999 /* Shortcut */ = { + isa = PBXGroup; + children = ( + 9627F9352B59956800B1E999 /* GlobalShortcutSetting.swift */, + 9627F9362B59956800B1E999 /* KeyHolderWrapper.swift */, + 9643D9382B6F49E0000FBEA6 /* AppShortcutSetting.swift */, + 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */, + 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */, + ); + path = Shortcut; + sourceTree = ""; + }; + 9643D93E2B6FC405000FBEA6 /* MenuView */ = { + isa = PBXGroup; + children = ( + 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */, + 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */, + 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */, + ); + path = MenuView; + sourceTree = ""; + }; + 967712EB2B5B93E200105E0F /* Feature */ = { + isa = PBXGroup; + children = ( + 967712EC2B5B941600105E0F /* Shortcut */, + ); + path = Feature; + sourceTree = ""; + }; + 967712EC2B5B941600105E0F /* Shortcut */ = { + isa = PBXGroup; + children = ( + 967712ED2B5B943400105E0F /* Shortcut.swift */, + 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */, + 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */, + 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */, + 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */, + ); + path = Shortcut; + sourceTree = ""; + }; 9CB57B9B45EC322A11ED8865 /* Pods */ = { isa = PBXGroup; children = ( @@ -2088,6 +2291,14 @@ path = Pods; sourceTree = ""; }; + C415C0AB2B450C4500A9D231 /* Gemini */ = { + isa = PBXGroup; + children = ( + C415C0AC2B450D4800A9D231 /* GeminiService.swift */, + ); + path = Gemini; + sourceTree = ""; + }; C4A40A9B2AC0168400B8E6EF /* Recovered References */ = { isa = PBXGroup; children = ( @@ -2143,6 +2354,89 @@ path = ChangeFontSizeView; sourceTree = ""; }; + EA1013412B5DBDA5005E43F9 /* Defaults */ = { + isa = PBXGroup; + children = ( + EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */, + ); + path = Defaults; + sourceTree = ""; + }; + EA3B81F72B52549B004C0E8B /* Configuration */ = { + isa = PBXGroup; + children = ( + EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */, + ); + path = Configuration; + sourceTree = ""; + }; + EA9943DD2B534BAE00EE7B97 /* Utility */ = { + isa = PBXGroup; + children = ( + EAE3D34F2B62E9DE001EE3E3 /* GlobalContext.swift */, + EAED41ED2B54B1390005FE0A /* Protocol */, + EA9943E62B534D7C00EE7B97 /* Extensions */, + ); + path = Utility; + sourceTree = ""; + }; + EA9943E12B534C2900EE7B97 /* Model */ = { + isa = PBXGroup; + children = ( + EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */, + ); + path = Model; + sourceTree = ""; + }; + EA9943E62B534D7C00EE7B97 /* Extensions */ = { + isa = PBXGroup; + children = ( + EA1013412B5DBDA5005E43F9 /* Defaults */, + 038A723E2B62C07B004995E3 /* String */, + EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */, + EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */, + EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */, + EA9943EF2B5354C400EE7B97 /* ShowWindowPositionExtensions.swift */, + EA9943F12B5358BF00EE7B97 /* LanguageExtensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + EAED41EA2B54A4900005FE0A /* ServiceConfiguration */ = { + isa = PBXGroup; + children = ( + 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */, + EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */, + 0AC8A8462B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift */, + 0AC8A8442B6A4D97006DA5CC /* ServiceConfigurationCells.swift */, + ); + path = ServiceConfiguration; + sourceTree = ""; + }; + EAED41ED2B54B1390005FE0A /* Protocol */ = { + isa = PBXGroup; + children = ( + EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */, + 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */ = { + isa = PBXGroup; + children = ( + EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */, + 0AC8A8402B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift */, + 0AC8A8342B6641A7006DA5CC /* TencentService+ConfigurableService.swift */, + 0AC8A8362B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift */, + 0AC8A8382B666F07006DA5CC /* CaiyunService+ConfigurableService.swift */, + 0AC8A83A2B6682D4006DA5CC /* AliService+ConfigurableService.swift */, + 0AC8A8422B6957B0006DA5CC /* BingService+ConfigurableService.swift */, + 0AC8A84A2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift */, + ); + path = "QueryService+ConfigurableService"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2206,7 +2500,7 @@ buildConfigurationList = C99EEB2C2385796900FEE666 /* Build configuration list for PBXNativeTarget "Easydict" */; buildPhases = ( 21D768ECC6D11E109E6EB73A /* [CP] Check Pods Manifest.lock */, - 03B04B582B2D4B8E00E30823 /* ShellScript */, + 03B04B582B2D4B8E00E30823 /* Run Script */, C99EEB142385796700FEE666 /* Sources */, C99EEB152385796700FEE666 /* Frameworks */, C99EEB162385796700FEE666 /* Resources */, @@ -2234,6 +2528,10 @@ 038030962B4106800009230C /* CocoaLumberjackSwift */, 038EA1A92B41169C008A6DD1 /* ZipArchive */, 038EA1AC2B41282F008A6DD1 /* MJExtension */, + EA3B81FB2B52555C004C0E8B /* Defaults */, + 967712E92B5B913600105E0F /* KeyHolder */, + 03022F182B3591AE00B63209 /* GoogleGenerativeAI */, + 0AC8A84E2B6DFDD4006DA5CC /* SettingsAccess */, ); productName = Bob; productReference = C99EEB182385796700FEE666 /* Easydict-debug.app */; @@ -2292,6 +2590,10 @@ 038030932B4106800009230C /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, 038EA1A82B41169C008A6DD1 /* XCRemoteSwiftPackageReference "ZipArchive" */, 038EA1AB2B41282F008A6DD1 /* XCRemoteSwiftPackageReference "MJExtension" */, + EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */, + 967712E82B5B913600105E0F /* XCRemoteSwiftPackageReference "KeyHolder" */, + 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */, + 0AC8A84D2B6DFDD4006DA5CC /* XCRemoteSwiftPackageReference "SettingsAccess" */, ); productRefGroup = C99EEB192385796700FEE666 /* Products */; projectDirPath = ""; @@ -2363,23 +2665,24 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 03B04B582B2D4B8E00E30823 /* ShellScript */ = { + 03B04B582B2D4B8E00E30823 /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 2147483647; + buildActionMask = 12; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); + name = "Run Script"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\" \"$SRCROOT/$TARGET_NAME\" --swiftversion 5.7\n"; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ \"${ENABLE_PREVIEWS}\" = \"YES\" ]; then\n echo \"SwiftFormat skipped for Xcode Previews.\"\n exit 0;\nfi\n\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\" \"$SRCROOT/$TARGET_NAME\" --swiftversion 5.7\n"; }; 124D690EE7236D6430CF945E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -2493,11 +2796,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EA9943EE2B5353AB00EE7B97 /* WindowTypeExtensions.swift in Sources */, 030570E22ADB919900C9905E /* EZAppleScriptManager.m in Sources */, 03991158292927E000E1B06D /* EZTitlebar.m in Sources */, 03D8A65C2A433B4100D9A968 /* EZConfiguration+EZUserData.m in Sources */, 03BD282229486CF200F5891A /* EZBlueTextButton.m in Sources */, - 037BEFCD2A98FDF700D0F17F /* EZBingLanguageVoice.m in Sources */, + 0AC8A8472B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift in Sources */, 03BDA7C22A26DA280079D04F /* NSString+Indenter.m in Sources */, 03542A462937B4C300C34C33 /* EZBaiduTranslateResponse.m in Sources */, 0309E1F0292B4A5E00AFB76A /* NSView+EZGetViewController.m in Sources */, @@ -2506,6 +2810,7 @@ 62E2BF4A2B4082BA00E42D38 /* AliService.swift in Sources */, 03B0233729231FA6001C7E63 /* MMMake.m in Sources */, 03B0232E29231FA6001C7E63 /* MMCrashSignalExceptionHandler.m in Sources */, + 9627F9382B59956800B1E999 /* GlobalShortcutSetting.swift in Sources */, 03BDA7C42A26DA280079D04F /* NSDictionary+RubyDescription.m in Sources */, 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */, C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */, @@ -2523,12 +2828,15 @@ 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */, 03B0230729231FA6001C7E63 /* EZCommonView.m in Sources */, 03B0233329231FA6001C7E63 /* MMLog.m in Sources */, + 0AC8A8352B6641A7006DA5CC /* TencentService+ConfigurableService.swift in Sources */, + DCF176F22B57CED700CA6026 /* Configuration+UserData.swift in Sources */, 0309E1F4292BD6A100AFB76A /* EZQueryModel.m in Sources */, 03BFFC7129612E10004E033E /* NSString+EZConvenience.m in Sources */, 03BDA7BD2A26DA280079D04F /* XPMArguments_Coalescer_Internal.m in Sources */, 03B3B8B22925D5B200168E8D /* EZPopButtonWindow.m in Sources */, 03B0231529231FA6001C7E63 /* SnipWindow.m in Sources */, 033363A0293A05D200FED9C8 /* EZSelectLanguageButton.m in Sources */, + 960835502B6791F200C6A931 /* Shortcut+Validator.swift in Sources */, 03542A522937B69200C34C33 /* EZYoudaoTranslateResponse.m in Sources */, 03B0230129231FA6001C7E63 /* EZQueryView.m in Sources */, 03542A3D2937AF4F00C34C33 /* EZQueryResult.m in Sources */, @@ -2536,6 +2844,7 @@ 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */, 03BDA7BF2A26DA280079D04F /* NSScanner+EscapedScanning.m in Sources */, 03542A4C2937B5F100C34C33 /* EZYoudaoTranslate.m in Sources */, + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */, 037852B329583F5200D0E2CF /* EZServiceCell.m in Sources */, 03247E362968158B00AFCD67 /* EZScriptExecutor.m in Sources */, 03882F8E29D95044005B5A52 /* ToastWindowController.m in Sources */, @@ -2547,23 +2856,30 @@ 03F14A3B2956016B00CB7379 /* EZVolcanoTranslate.m in Sources */, 03B0230429231FA6001C7E63 /* EZHoverButton.m in Sources */, 0342A9812AD64924002A9F5F /* NSString+EZSplit.m in Sources */, + EAED41EF2B54B1430005FE0A /* ConfigurableService.swift in Sources */, 03BD2825294875AE00F5891A /* EZMyLabel.m in Sources */, 03B0233029231FA6001C7E63 /* MMCrashUncaughtExceptionHandler.m in Sources */, 03D5FCFF2A5EF4E400AD26BE /* EZDeviceSystemInfo.m in Sources */, 27FE95272B3DC55F000AD654 /* EasydictApp.swift in Sources */, 03882F9129D95044005B5A52 /* CTCommon.m in Sources */, 276742082B3DC230002A2C75 /* PrivacyTab.swift in Sources */, + 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */, 03882F8F29D95044005B5A52 /* CTScreen.m in Sources */, + 0AC8A8392B666F07006DA5CC /* CaiyunService+ConfigurableService.swift in Sources */, 27FE980B2B3DD5D1000AD654 /* MenuItemView.swift in Sources */, 03DC7C6A2A3CA852000BF7C9 /* EZAppCell.m in Sources */, + 9643D9392B6F49E0000FBEA6 /* AppShortcutSetting.swift in Sources */, + 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */, 0399C6AC29A860AA00B4AFCC /* EZOpenAIService.m in Sources */, 03542A432937B45E00C34C33 /* EZBaiduTranslate.m in Sources */, 03BB2DEB29F57DC000447EDD /* NSImage+EZSymbolmage.m in Sources */, 03B0230629231FA6001C7E63 /* EZLabel.m in Sources */, 03F25CB329327BC200E66A12 /* EZShortcut.m in Sources */, + EA1013442B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift in Sources */, 033B7134293CE2430096E2DF /* EZWebViewTranslator.m in Sources */, 03CF88632B137F650030C199 /* Array+Convenience.swift in Sources */, 03B0231229231FA6001C7E63 /* NSObject+DarkMode.m in Sources */, + 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */, 03B0233829231FA6001C7E63 /* MMOrderedDictionary.m in Sources */, 278540342B3DE04F004E9488 /* GeneralTab.swift in Sources */, 03BDA7BC2A26DA280079D04F /* XPMArgumentSignature.m in Sources */, @@ -2572,6 +2888,8 @@ 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */, 039F5506294B6E29004AB940 /* EZSettingViewController.m in Sources */, 03BD281E29481C0400F5891A /* EZAudioPlayer.m in Sources */, + 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */, + 9643D94A2B71EABE000FBEA6 /* KeyHolderAlterView.swift in Sources */, 03E02A2629250D1D00A10260 /* EZEventMonitor.m in Sources */, 03B0233429231FA6001C7E63 /* MMConsoleLogFormatter.m in Sources */, 037852B9295D49F900D0E2CF /* EZTableRowView.m in Sources */, @@ -2579,9 +2897,11 @@ 039CC914292FB3180037B91E /* EZPopUpButton.m in Sources */, 0399C6B829A9F4B800B4AFCC /* EZSchemeParser.m in Sources */, 03542A3A2937AE6400C34C33 /* EZQueryService.m in Sources */, + 0AC8A84B2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift in Sources */, 03B0230529231FA6001C7E63 /* EZButton.m in Sources */, 03B0232329231FA6001C7E63 /* NSString+MM.m in Sources */, 036196772A000F5900806370 /* NSData+CommonCrypto.m in Sources */, + 0A057D6D2B499A000025C51D /* ServiceTab.swift in Sources */, 03882F8D29D95044005B5A52 /* CTView.m in Sources */, 278322602B0FB0EA0026644C /* CaiyunResponse.swift in Sources */, 03B3B8B52925DD3D00168E8D /* EZPopButtonViewController.m in Sources */, @@ -2591,28 +2911,38 @@ 03B0233229231FA6001C7E63 /* MMLog.swift in Sources */, 03DC7C5E2A3ABE28000BF7C9 /* EZConstKey.m in Sources */, 62E2BF4C2B4082BA00E42D38 /* AliTranslateType.swift in Sources */, + EA3B81F92B5254AA004C0E8B /* Configuration+Defaults.swift in Sources */, 03E3E7C22ADE318800812C84 /* EZQueryMenuTextView.m in Sources */, 03B0231829231FA6001C7E63 /* SnipWindowController.m in Sources */, 03542A342936F70F00C34C33 /* EZLanguageManager.m in Sources */, 6295DE312A84D82E006145F4 /* EZBingTranslateModel.m in Sources */, 0361967B2A0037F700806370 /* NSData+EZMD5.m in Sources */, + 967712EE2B5B943400105E0F /* Shortcut.swift in Sources */, 03BFFC68295F4B87004E033E /* EZYoudaoDictModel.m in Sources */, 03247E3A296AE8EC00AFCD67 /* EZLoadingAnimationView.m in Sources */, 0396D615292CC4C3006A11D9 /* EZLocalStorage.m in Sources */, 0329CD6F29EE924500963F78 /* EZRightClickDetector.m in Sources */, 033C30FC2A7409C40095926A /* TTTDictionary.m in Sources */, + 9643D93D2B6F829C000FBEA6 /* MainMenuCommand.swift in Sources */, 03B0232D29231FA6001C7E63 /* NSArray+MM.m in Sources */, 039E5021296E5D9900072344 /* EZScrollViewController.m in Sources */, + 9643D94C2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift in Sources */, + 0AC8A8412B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift in Sources */, 039CC90D292F664E0037B91E /* NSObject+EZWindowType.m in Sources */, 03B0232229231FA6001C7E63 /* NSImage+MM.m in Sources */, 03BB2DEF29F59C8A00447EDD /* EZSymbolImageButton.m in Sources */, + 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */, + C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */, + 9643D9402B6FC426000FBEA6 /* MainMenuShortcutCommand.swift in Sources */, 62A2D03F2A82967F007EEB01 /* EZBingRequest.m in Sources */, 03BDA7BE2A26DA280079D04F /* XPMCountedArgument.m in Sources */, + 038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */, 03D35DAA2AA6C49B00B023FE /* NSString+EZRegex.m in Sources */, 03B022FE29231FA6001C7E63 /* EZBaseQueryViewController.m in Sources */, DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */, 0396D611292C932F006A11D9 /* EZSelectLanguageCell.m in Sources */, 036196752A000F5900806370 /* FWEncryptorAES.m in Sources */, + 9643D9462B71D103000FBEA6 /* KeyHolderRowView.swift in Sources */, 0399C6A829A74E0F00B4AFCC /* EZQueryResult+EZDeepLTranslateResponse.m in Sources */, 039B694F2A9D9F370063709D /* EZWebViewManager.m in Sources */, 03B0231629231FA6001C7E63 /* SnipFocusView.m in Sources */, @@ -2627,6 +2957,8 @@ 0333FDA62A035D5700891515 /* NSString+EZChineseText.m in Sources */, 03E02A222924E77100A10260 /* EZMenuItemManager.m in Sources */, 039D119929D5E26300C93F46 /* EZAudioUtils.m in Sources */, + 030DB2612B56CC6500E27DEA /* BingLanguageVoice.swift in Sources */, + EA9943F22B5358BF00EE7B97 /* LanguageExtensions.swift in Sources */, 03B0231729231FA6001C7E63 /* Snip.m in Sources */, 03BFFC6E295FE59C004E033E /* EZQueryResult+EZYoudaoDictModel.m in Sources */, DC3C643F2B187119008EEDD8 /* ChangeFontSizeView.swift in Sources */, @@ -2635,6 +2967,7 @@ 03D043522928935300E7559E /* EZMainQueryWindow.m in Sources */, 03D8B26E292DBD2000D5A811 /* EZCoordinateUtils.m in Sources */, 03B0232029231FA6001C7E63 /* NSWindow+MM.m in Sources */, + 0AC8A8432B6957B0006DA5CC /* BingService+ConfigurableService.swift in Sources */, 03542A30293645DF00C34C33 /* EZAppleService.m in Sources */, 03BB2DE329F5772F00447EDD /* EZAudioButton.m in Sources */, 03262C2529EFE97B00EFECA0 /* NSViewController+EZWindow.m in Sources */, @@ -2642,12 +2975,18 @@ 27FE98052B3DCB09000AD654 /* NewAppManager.swift in Sources */, 0399116A292AA2EF00E1B06D /* EZLayoutManager.m in Sources */, 0320C5872B29F35700861B3D /* QueryServiceRecord.swift in Sources */, + 9643D9422B6FE4AF000FBEA6 /* Shortcut+Bind.swift in Sources */, 03FC699A2B39D13A0035D2DA /* EZOpenAIChatResponse.m in Sources */, 03B022FA29231FA6001C7E63 /* EZServiceTypes.m in Sources */, + EAE3D3502B62E9DE001EE3E3 /* GlobalContext.swift in Sources */, + EA9943F02B5354C400EE7B97 /* ShowWindowPositionExtensions.swift in Sources */, 03B0233129231FA6001C7E63 /* MMCrash.m in Sources */, 03B0232629231FA6001C7E63 /* NSAttributedString+MM.m in Sources */, 03542A402937B3C900C34C33 /* EZOCRResult.m in Sources */, + 9627F9392B59956800B1E999 /* KeyHolderWrapper.swift in Sources */, C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */, + 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */, + EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */, 036A0DBB2AD941F9006E6D4F /* EZReplaceTextButton.m in Sources */, 03DC7C662A3CA465000BF7C9 /* HWSegmentedControl.m in Sources */, 037E006D2B3DC098006491C6 /* EZOpenAIService+EZPromptMessages.m in Sources */, @@ -2658,10 +2997,13 @@ 03DC38C1292CC97900922CB2 /* EZServiceInfo.m in Sources */, 03B0232A29231FA6001C7E63 /* NSColor+MyColors.m in Sources */, C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */, + 0AC8A83D2B6685EE006DA5CC /* SecureTextField.swift in Sources */, + 0AC8A8372B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift in Sources */, 03D043562928940500E7559E /* EZBaseQueryWindow.m in Sources */, 03BDA7B92A26DA280079D04F /* NSProcessInfo+XPMArgumentParser.m in Sources */, 03542A4F2937B64B00C34C33 /* EZYoudaoOCRResponse.m in Sources */, 03B0233929231FA6001C7E63 /* MMTool.m in Sources */, + 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */, 03542A552937B7DE00C34C33 /* EZError.m in Sources */, 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */, 03BDA7B82A26DA280079D04F /* XPMValuedArgument.m in Sources */, @@ -2670,6 +3012,7 @@ 03BDA7BA2A26DA280079D04F /* XPMMutableAttributedArray.m in Sources */, 037852B629588EDE00D0E2CF /* EZCustomTableRowView.m in Sources */, 03F0DB382953428300EBF9C1 /* EZLog.m in Sources */, + 0AC8A83B2B6682D4006DA5CC /* AliService+ConfigurableService.swift in Sources */, 03B0231429231FA6001C7E63 /* DarkModeManager.m in Sources */, 03BDA7C02A26DA280079D04F /* XPMArgumentPackage.m in Sources */, 2746AEC12AF95138005FE0A1 /* CaiyunService.swift in Sources */, @@ -2677,14 +3020,21 @@ 6220AD5B2A82812300BBFB52 /* EZBingService.m in Sources */, 039F5508294B6E29004AB940 /* EZAboutViewController.m in Sources */, 03D8A6592A42A1A300D9A968 /* EZAppModel.m in Sources */, + DC46DF802B4417B900DEAE3E /* Configuration.swift in Sources */, 036E7D7B293F4FC8002675DF /* EZOpenLinkButton.m in Sources */, + EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */, + 03832F542B5F6BE200D0DC64 /* AdvancedTab.swift in Sources */, + 0AC8A8452B6A4D97006DA5CC /* ServiceConfigurationCells.swift in Sources */, 276742092B3DC230002A2C75 /* AboutTab.swift in Sources */, 03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */, DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */, 03B0232429231FA6001C7E63 /* NSUserDefaults+MM.m in Sources */, 03542A5E2938F05B00C34C33 /* EZLanguageModel.m in Sources */, + EA9943E82B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift in Sources */, 03F639952AA6CFBB009B9914 /* EZBingConfig.m in Sources */, + 0AC8A83F2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift in Sources */, 03D2A3E329F4C6F50035CED4 /* EZNetworkManager.m in Sources */, + 9643D9562B73B3CD000FBEA6 /* Shortcut+Menu.swift in Sources */, 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */, 03B0232B29231FA6001C7E63 /* NSMutableAttributedString+MM.m in Sources */, 03B022E829231FA6001C7E63 /* entry.m in Sources */, @@ -2692,6 +3042,7 @@ 03008B3F29444B0A0062B821 /* NSView+EZAnimatedHidden.m in Sources */, 03B022FD29231FA6001C7E63 /* EZFixedQueryWindow.m in Sources */, 03B0232C29231FA6001C7E63 /* NSView+MM.m in Sources */, + EAED41F22B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift in Sources */, 033C31002A74CECE0095926A /* EZAppleDictionary.m in Sources */, 03E2BF752A298F2B00E010F3 /* NSString+EZUtils.m in Sources */, 03B022F529231FA6001C7E63 /* EZDetectManager.m in Sources */, @@ -2754,7 +3105,7 @@ DEVELOPMENT_TEAM = 79NQA2XYHM; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.1; MARKETING_VERSION = 1.0; @@ -2782,7 +3133,7 @@ DEVELOPMENT_TEAM = 79NQA2XYHM; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.1; MARKETING_VERSION = 1.0; @@ -2809,7 +3160,7 @@ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; @@ -2834,7 +3185,7 @@ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; @@ -2859,7 +3210,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; INFOPLIST_FILE = EasydictHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2885,7 +3236,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; INFOPLIST_FILE = EasydictHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3044,7 +3395,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_TESTING_SEARCH_PATHS = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; INFOPLIST_FILE = "$(TARGET_NAME)/App/Info-debug.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -3084,7 +3435,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_TESTING_SEARCH_PATHS = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; INFOPLIST_FILE = "$(TARGET_NAME)/App/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -3155,6 +3506,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/google/generative-ai-swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.4.4; + }; + }; 03022F1A2B35DEBA00B63209 /* XCRemoteSwiftPackageReference "Hue" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/zenangst/Hue"; @@ -3243,6 +3602,14 @@ minimumVersion = 1.8.0; }; }; + 0AC8A84D2B6DFDD4006DA5CC /* XCRemoteSwiftPackageReference "SettingsAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/orchetect/SettingsAccess"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.4.0; + }; + }; 2721E4CE2AFE920700A059AC /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire.git"; @@ -3251,9 +3618,30 @@ minimumVersion = 5.8.1; }; }; + 967712E82B5B913600105E0F /* XCRemoteSwiftPackageReference "KeyHolder" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Clipy/KeyHolder.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.2.0; + }; + }; + EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Defaults.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.3.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 03022F182B3591AE00B63209 /* GoogleGenerativeAI */ = { + isa = XCSwiftPackageProductDependency; + package = 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */; + productName = GoogleGenerativeAI; + }; 03022F1B2B35DEBA00B63209 /* Hue */ = { isa = XCSwiftPackageProductDependency; package = 03022F1A2B35DEBA00B63209 /* XCRemoteSwiftPackageReference "Hue" */; @@ -3324,11 +3712,26 @@ package = 03FD68B92B1DC59600FD388E /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; + 0AC8A84E2B6DFDD4006DA5CC /* SettingsAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 0AC8A84D2B6DFDD4006DA5CC /* XCRemoteSwiftPackageReference "SettingsAccess" */; + productName = SettingsAccess; + }; 2721E4CF2AFE920700A059AC /* Alamofire */ = { isa = XCSwiftPackageProductDependency; package = 2721E4CE2AFE920700A059AC /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; + 967712E92B5B913600105E0F /* KeyHolder */ = { + isa = XCSwiftPackageProductDependency; + package = 967712E82B5B913600105E0F /* XCRemoteSwiftPackageReference "KeyHolder" */; + productName = KeyHolder; + }; + EA3B81FB2B52555C004C0E8B /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C99EEB102385796700FEE666 /* Project object */; diff --git a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4516ef259..6e22eb216 100644 --- a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -54,6 +54,15 @@ "version" : "1.8.0" } }, + { + "identity" : "defaults", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/Defaults.git", + "state" : { + "revision" : "3efef5a28ebdbbe922d4a2049493733ed14475a6", + "version" : "7.3.1" + } + }, { "identity" : "firebase-ios-sdk", "kind" : "remoteSourceControl", @@ -63,6 +72,15 @@ "version" : "10.19.1" } }, + { + "identity" : "generative-ai-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/generative-ai-swift", + "state" : { + "revision" : "dcbdb5e591e1aa2bb68851dc7515f6b0a59026cd", + "version" : "0.4.7" + } + }, { "identity" : "googleappmeasurement", "kind" : "remoteSourceControl", @@ -126,6 +144,15 @@ "version" : "100.0.0" } }, + { + "identity" : "keyholder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Clipy/KeyHolder.git", + "state" : { + "revision" : "5da0ee06fd8709f7d812504bff1555462a3f6956", + "version" : "4.2.0" + } + }, { "identity" : "leveldb", "kind" : "remoteSourceControl", @@ -135,6 +162,15 @@ "version" : "1.22.3" } }, + { + "identity" : "magnet", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Clipy/Magnet", + "state" : { + "revision" : "26d672a031fd33eac94887b4e19aca0ab4761f45", + "version" : "3.4.0" + } + }, { "identity" : "mjextension", "kind" : "remoteSourceControl", @@ -189,6 +225,24 @@ "version" : "10.45.2" } }, + { + "identity" : "sauce", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Clipy/Sauce", + "state" : { + "revision" : "8f8fabaa8509c1a653d6c2c3c87396a4c493d876", + "version" : "2.4.0" + } + }, + { + "identity" : "settingsaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/orchetect/SettingsAccess", + "state" : { + "revision" : "0fd73c8b5892e88acb13adb7f36a4ba9293a0061", + "version" : "1.4.0" + } + }, { "identity" : "snapkit", "kind" : "remoteSourceControl", diff --git a/Easydict/App/AppDelegate+EZURLScheme.m b/Easydict/App/AppDelegate+EZURLScheme.m index eef2674c5..f3ce651bd 100644 --- a/Easydict/App/AppDelegate+EZURLScheme.m +++ b/Easydict/App/AppDelegate+EZURLScheme.m @@ -10,7 +10,7 @@ #import #import "EZWindowManager.h" #import "EZSchemeParser.h" -#import "EZConfiguration.h" +#import "Easydict-Swift.h" @implementation AppDelegate (EZURLScheme) @@ -70,8 +70,8 @@ - (void)registerRouters { - (void)showFloatingWindowAndAutoQueryText:(NSString *)text { EZWindowManager *windowManager = [EZWindowManager shared]; - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; - + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; + [windowManager showFloatingWindowType:windowType queryText:text.trim autoQuery:YES diff --git a/Easydict/App/AppDelegate.h b/Easydict/App/AppDelegate.h index d604f4d1b..2ecaff2c6 100644 --- a/Easydict/App/AppDelegate.h +++ b/Easydict/App/AppDelegate.h @@ -7,10 +7,7 @@ // #import -#import @interface AppDelegate : NSObject -@property (weak) IBOutlet SPUStandardUpdaterController *updaterController; - @end diff --git a/Easydict/App/AppDelegate.m b/Easydict/App/AppDelegate.m index 8aa9e5f27..4d3f7c9ee 100644 --- a/Easydict/App/AppDelegate.m +++ b/Easydict/App/AppDelegate.m @@ -7,20 +7,12 @@ // #import "AppDelegate.h" -#import "EZMenuItemManager.h" #import "EZShortcut.h" #import "MMCrash.h" -#import "EZWindowManager.h" -#import "EZLanguageManager.h" -#import "EZConfiguration.h" -#import "EZLog.h" -#import "EZSchemeParser.h" #import "AppDelegate+EZURLScheme.h" -#import -#import #import "Easydict-Swift.h" -@interface AppDelegate () +@interface AppDelegate () @end @@ -31,19 +23,22 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Capturing crash logs must be placed first. [MMCrash registerHandler]; + [EZLog setupCrashLogService]; [EZLog logAppInfo]; if (!EasydictNewAppManager.shared.enable) { [EZMenuItemManager.shared setup]; + [EZShortcut setup]; + } else { + [Shortcut setupShortcut]; } - - [EZShortcut setup]; [EZWindowManager.shared showMainWindowIfNedded]; [self registerRouters]; + [[DarkModeManager manager] updateDarkMode:Configuration.shared.appearance]; // Change App icon manually. // NSApplication.sharedApplication.applicationIconImage = [NSImage imageNamed:@"white-black-icon"]; } @@ -99,20 +94,4 @@ - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)applica return NO; } -#pragma mark - SUUpdaterDelegate - -- (NSString *)feedURLStringForUpdater:(SPUUpdater *)updater { - NSString *feedURLString = @"https://raw.githubusercontent.com/tisfeng/Easydict/main/appcast.xml"; -#if DEBUG - feedURLString = @"http://localhost:8000/appcast.xml"; -#endif - return feedURLString; -} - -#pragma mark - SPUStandardUserDriverDelegate - -- (BOOL)supportsGentleScheduledUpdateReminders { - return YES; -} - @end diff --git a/Easydict/App/Assets.xcassets/Colors/Contents.json b/Easydict/App/Assets.xcassets/Colors/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json new file mode 100644 index 000000000..a83dbf8e7 --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE9", + "green" : "0xE9", + "red" : "0xEA" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3A", + "green" : "0x39", + "red" : "0x3B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json new file mode 100644 index 000000000..d60e3bb57 --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDE", + "green" : "0xDD", + "red" : "0xDE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0x50", + "red" : "0x50" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/Contents.json b/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/Contents.json similarity index 70% rename from Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/Contents.json rename to Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/Contents.json index d0d1860d8..ca238cdf9 100644 --- a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/Contents.json +++ b/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/Contents.json @@ -1,17 +1,15 @@ { "images" : [ { - "filename" : "release_18.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "release_36.png", + "filename" : "rounded.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "release_54.png", "idiom" : "universal", "scale" : "3x" } @@ -19,5 +17,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "localizable" : true } } diff --git a/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/rounded.png b/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/rounded.png new file mode 100644 index 000000000..72cd3f200 Binary files /dev/null and b/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/rounded.png differ diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/Contents.json b/Easydict/App/Assets.xcassets/menu-icon/square_menu_bar_icon.imageset/Contents.json similarity index 70% rename from Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/Contents.json rename to Easydict/App/Assets.xcassets/menu-icon/square_menu_bar_icon.imageset/Contents.json index cdd79c551..b8568e6df 100644 --- a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/Contents.json +++ b/Easydict/App/Assets.xcassets/menu-icon/square_menu_bar_icon.imageset/Contents.json @@ -1,17 +1,15 @@ { "images" : [ { - "filename" : "debug_20 1.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "debug_40 1.png", + "filename" : "square.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "debug_60 1.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Easydict/App/Assets.xcassets/menu-icon/square_menu_bar_icon.imageset/square.png b/Easydict/App/Assets.xcassets/menu-icon/square_menu_bar_icon.imageset/square.png new file mode 100644 index 000000000..946f0b75b Binary files /dev/null and b/Easydict/App/Assets.xcassets/menu-icon/square_menu_bar_icon.imageset/square.png differ diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_18.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_18.png deleted file mode 100644 index 24e2328eb..000000000 Binary files a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_18.png and /dev/null differ diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_36.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_36.png deleted file mode 100644 index 2a7dbe540..000000000 Binary files a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_36.png and /dev/null differ diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_54.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_54.png deleted file mode 100644 index 22fa1bbc3..000000000 Binary files a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_54.png and /dev/null differ diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_20 1.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_20 1.png deleted file mode 100644 index 9f51401bc..000000000 Binary files a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_20 1.png and /dev/null differ diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_40 1.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_40 1.png deleted file mode 100644 index 76d13685b..000000000 Binary files a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_40 1.png and /dev/null differ diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_60 1.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_60 1.png deleted file mode 100644 index 0e7cca491..000000000 Binary files a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_60 1.png and /dev/null differ diff --git a/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json new file mode 100644 index 000000000..f0bbce672 --- /dev/null +++ b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Gemini.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png new file mode 100644 index 000000000..de0d56846 Binary files /dev/null and b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png differ diff --git a/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json b/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json index d6c01d3c7..4d3912c0e 100644 --- a/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json +++ b/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json @@ -10,7 +10,6 @@ "scale" : "2x" }, { - "filename" : "Privacy.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Privacy.png b/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Privacy.png deleted file mode 100644 index 12ccf9bf3..000000000 Binary files a/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Privacy.png and /dev/null differ diff --git a/Easydict/App/Base.lproj/Main.storyboard b/Easydict/App/Base.lproj/Main.storyboard index 9df59d2cb..52009ade9 100644 --- a/Easydict/App/Base.lproj/Main.storyboard +++ b/Easydict/App/Base.lproj/Main.storyboard @@ -716,18 +716,8 @@ DQ - - - - - + - - - - - - @@ -799,7 +789,7 @@ DQ - + diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 2747a587f..bfa0f2504 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -20,6 +20,20 @@ #import "entry.h" #import "AppDelegate.h" -#import "EZConfiguration.h" + +#import "EZAppModel.h" +#import "EZLocalStorage.h" #import "NSString+EZConvenience.h" +#import "EZWindowManager.h" +#import "NSViewController+EZWindow.h" +#import "EZLog.h" +#import "EZLanguageManager.h" +#import "DarkModeManager.h" +#import "EZScriptExecutor.h" +#import "EZOpenAIService.h" +#import "EZNiuTransTranslate.h" +#import "EZDeepLTranslate.h" +#import "EZBingService.h" + + diff --git a/Easydict/App/Info-debug.plist b/Easydict/App/Info-debug.plist index defc09926..d94aeb894 100644 --- a/Easydict/App/Info-debug.plist +++ b/Easydict/App/Info-debug.plist @@ -51,7 +51,7 @@ NSHumanReadableCopyright - Copyright © 2022 tisfeng. All rights reserved. + Copyright © 2023-2024 tisfeng. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/Easydict/App/Info.plist b/Easydict/App/Info.plist index 6e735f833..99a9b1c94 100644 --- a/Easydict/App/Info.plist +++ b/Easydict/App/Info.plist @@ -51,7 +51,7 @@ NSHumanReadableCopyright - Copyright © 2022 tisfeng. All rights reserved. + Copyright © 2023-2024 tisfeng. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 0ee5bae52..db79188c4 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1,6 +1,16 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } + }, "about" : { "comment" : "about", "localizations" : { @@ -34,6 +44,22 @@ } } }, + "advanced" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advanced" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高级" + } + } + } + }, "ali_translate" : { "extractionState" : "manual", "localizations" : { @@ -143,6 +169,23 @@ } } }, + "app_shortcut_setting" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Shortcut" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用内快捷键" + } + } + } + }, "appearenceType_dark" : { "localizations" : { "en" : { @@ -487,6 +530,12 @@ }, "beta_new_app" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "[Beta] SwiftUI App mode" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -557,6 +606,12 @@ }, "check_updates" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check for Updates" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -597,22 +652,6 @@ } } }, - "clear_input" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear Input:" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "清空查询内容:" - } - } - } - }, "clear_input_when_translating" : { "localizations" : { "en" : { @@ -845,7 +884,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "划词内容为空时生效" + "value" : "划词内容为空时禁用复制提示音" } } } @@ -884,13 +923,33 @@ } }, "Easydict" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "Easydict 🍃" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "enable_beta_new_app" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launching in SwiftUI App mode, changes take effect upon restarting the app." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1044,6 +1103,16 @@ } } }, + "Export Log" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出日志" + } + } + } + }, "Failed to allocate memory" : { "comment" : "Error reason", "localizations" : { @@ -1055,6 +1124,16 @@ } } }, + "Feedback" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "反馈问题" + } + } + } + }, "first_language" : { "localizations" : { "en" : { @@ -1243,6 +1322,23 @@ } } }, + "gemini_translate" : { + "comment" : "The name of Gemini Translate", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemini Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gemini 翻译" + } + } + } + }, "GitHub:" : { "localizations" : { "zh-Hans" : { @@ -1253,6 +1349,23 @@ } } }, + "global_shortcut_setting" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Global Shortcut" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全局快捷键" + } + } + } + }, "google_translate" : { "localizations" : { "en" : { @@ -1269,6 +1382,16 @@ } } }, + "Help" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "帮助" + } + } + } + }, "hide" : { "localizations" : { "en" : { @@ -1388,13 +1511,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Input Translate:" + "value" : "Input Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "输入翻译:" + "value" : "输入翻译" } } } @@ -1420,12 +1543,46 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "请前往服务官网注册申请个人的 API key。" } } } }, + "keep_prev_result_when_selected_text_is_empty" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keep previous result when selected text is empty" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "划词翻译未选中文本时,保留上次结果" + } + } + } + }, + "keep_result" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keep Result:" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留结果:" + } + } + } + }, "language_detect_optimize" : { "localizations" : { "en" : { @@ -1555,6 +1712,16 @@ } } }, + "Log Directory" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日志目录" + } + } + } + }, "main_window" : { "localizations" : { "en" : { @@ -1598,252 +1765,351 @@ } } }, - "mini_window" : { + "menu_input_translate" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Mini window" + "value" : "Input Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "迷你窗口" + "value" : "输入翻译" } } } }, - "mouse_select_translate_window_type" : { + "menu_screenshot_Translate" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Mouse Window Type:" + "value" : "Screenshot Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "鼠标划词窗口类型:" + "value" : "截图翻译" } } } }, - "niuTrans_translate" : { + "menu_selectWord_Translate" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "NiuTrans" + "value" : "Select Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "小牛翻译" + "value" : "划词翻译" } } } }, - "no_results_found" : { + "menu_show_mini_window" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "⚠ No results found" + "value" : "Show Mini Window" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "⚠ 未查询到结果" + "value" : "显示迷你窗口" } } } }, - "ocr_result_is_empty" : { + "menu_silent_screenshot_OCR" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "⚠️ OCR result is empty.\n⚠️ Please manually select language and try again." + "value" : "Silent Screenshot OCR" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "⚠️ OCR 结果为空。\n⚠️ 请手动选择识别语言再次尝试。" + "value" : "静默截图 OCR" } } } }, - "ok" : { + "mini_window" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Ok" + "value" : "Mini window" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "确定" + "value" : "迷你窗口" } } } }, - "open_in_apple_dictionary" : { + "modify_menubar_icon" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Open in Apple Dictionary" + "value" : "Menu bar icon" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "在苹果词典中打开" + "value" : "菜单栏图标" } } } }, - "open_in_eudic" : { + "mouse_select_translate_window_type" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Open in Eudic" + "value" : "Mouse Window Type:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "在欧路词典中打开" + "value" : "鼠标划词窗口类型:" } } } }, - "open_in_google" : { + "niuTrans_translate" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Open in Google" + "value" : "NiuTrans" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "通过 Google 搜索" + "value" : "小牛翻译" } } } }, - "open_web_link" : { + "no_results_found" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Open Web Link" + "value" : "⚠ No results found" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "在网页中打开" + "value" : "⚠ 未查询到结果" } } } }, - "openai_translate" : { + "none_window" : { "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "OpenAI Translate" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI 翻译" + "value" : "" } } } }, - "other" : { + "ocr_result_is_empty" : { "localizations" : { - "zh-Hans" : { + "en" : { "stringUnit" : { "state" : "translated", - "value" : "其他" + "value" : "⚠️ OCR result is empty.\n⚠️ Please manually select language and try again." } - } - } - }, - "Parameter Error" : { - "comment" : "Error description", - "localizations" : { + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "" + "value" : "⚠️ OCR 结果为空。\n⚠️ 请手动选择识别语言再次尝试。" } } } }, - "past" : { + "ok" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Past tense" + "value" : "Ok" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "过去式" + "value" : "确定" } } } }, - "past_participle" : { + "open_in_apple_dictionary" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Past participle" + "value" : "Open in Apple Dictionary" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "过去分词" + "value" : "在苹果词典中打开" } } } }, - "pin" : { + "open_in_eudic" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Pin" + "value" : "Open in Eudic" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "钉住窗口" + "value" : "在欧路词典中打开" } } } }, - "placeholder" : { + "open_in_google" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open in Google" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通过 Google 搜索" + } + } + } + }, + "open_web_link" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Web Link" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在网页中打开" + } + } + } + }, + "openai_translate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenAI Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenAI 翻译" + } + } + } + }, + "Parameter Error" : { + "comment" : "Error description", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } + }, + "past" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Past tense" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "过去式" + } + } + } + }, + "past_participle" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Past participle" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "过去分词" + } + } + } + }, + "pin" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pin" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "钉住窗口" + } + } + } + }, + "placeholder" : { "localizations" : { "en" : { "stringUnit" : { @@ -1854,317 +2120,1666 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "输入内容,Enter 查询,Shift + Enter 换行" + "value" : "输入内容,Enter 查询,Shift + Enter 换行" + } + } + } + }, + "play_audio" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play Audio" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "播放音频" + } + } + } + }, + "play_word_audio" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play Word Pronunciation:" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "播放单词发音:" + } + } + } + }, + "please_look" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please see 👉" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "参见 👉" + } + } + } + }, + "plural" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Plural" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复数" + } + } + } + }, + "present_participle" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Present participle" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "现在分词" + } + } + } + }, + "privacy" : { + "comment" : "privacy", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐私" + } + } + } + }, + "privacy_statement" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Privacy Statement" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐私声明" + } + } + } + }, + "privacy_statement_content" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Easydict uses AppCenter and Firebase to collect crash logs (for bug fixes) and anonymous analytics data, but these data will not be associated with your identity, only for improving user experience, and will never be shared with other platforms.\n\nEasydict is open source software, if you are interested, you can check the code." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Easydict 使用了 AppCenter 和 Firebase,它们会收集用户的崩溃日志(用于修复 bug)和匿名统计数据,但这些数据都不会关联到您的身份,仅用于改进用户体验,绝不会分享给其他平台。\n\nEasydict 是开源软件,如果你感兴趣,可以查看代码。" + } + } + } + }, + "query_failed" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Query failed" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询失败" + } + } + } + }, + "query_in_app" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Query in App" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用内查询" + } + } + } + }, + "quick_link" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quick Link:" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷功能:" + } + } + } + }, + "quit" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quit" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "退出" + } + } + } + }, + "replace_text" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Replace With Translation" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用译文替换" + } + } + } + }, + "retry" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Retry" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重试" + } + } + } + }, + "root" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Root" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "词根" + } + } + } + }, + "second_language" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Second Language:" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第二语言:" + } + } + } + }, + "select_query_text_when_window_activate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select query text when window activate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开窗口时自动选中查询文本" + } + } + } + }, + "select_translate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select Text Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "划词翻译" + } + } + } + }, + "service" : { + "comment" : "service", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Service" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "服务" + } + } + } + }, + "service.configuration.ali.access_key_id.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey ID" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey ID" + } + } + } + }, + "service.configuration.ali.access_key_secret.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey Secret" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey Secret" + } + } + } + }, + "service.configuration.bing.cookie.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cookie Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cookie Key" + } + } + } + }, + "service.configuration.caiyun.token.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Token" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Token" + } + } + } + }, + "service.configuration.deepl.auth_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key" + } + } + } + }, + "service.configuration.deepl.authkey_first.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key First" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "优先使用 Auth Key" + } + } + } + }, + "service.configuration.deepl.authkey_only.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key Only" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仅使用 Auth Key" + } + } + } + }, + "service.configuration.deepl.endpoint.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://api-free.deepl.com/v2/translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://api-free.deepl.com/v2/translate" + } + } + } + }, + "service.configuration.deepl.endpoint.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Endpoint" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完整接口地址" + } + } + } + }, + "service.configuration.deepl.translation.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Usage Priority" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API 使用优先级" + } + } + } + }, + "service.configuration.deepl.web_first.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Web API first" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "优先使用 Web API" + } + } + } + }, + "service.configuration.gemini.api_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + } + } + }, + "service.configuration.input.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "xxxxxxxxxx" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "xxxxxxxxxx" + } + } + } + }, + "service.configuration.niutrans.api_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + } + } + }, + "service.configuration.openai.api_key.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "sk-xxxxxxxxxx" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "sk-xxxxxxxxxx" + } + } + } + }, + "service.configuration.openai.api_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + } + } + }, + "service.configuration.openai.dictionary.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dictionary" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查单词" + } + } + } + }, + "service.configuration.openai.endpoint.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://api.openai.com/v1/chat/completions" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://api.openai.com/v1/chat/completions" + } + } + } + }, + "service.configuration.openai.endpoint.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Endpoint" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完整接口地址" + } + } + } + }, + "service.configuration.openai.model.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Model" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "模型" + } + } + } + }, + "service.configuration.openai.sentence.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sentence" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "句子分析" + } + } + } + }, + "service.configuration.openai.translation.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Translation" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "翻译" + } + } + } + }, + "service.configuration.openai.usage_status_always_off.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Always Off" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "始终关闭" + } + } + } + }, + "service.configuration.openai.usage_status_always_on.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Always On" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "始终打开" + } + } + } + }, + "service.configuration.openai.usage_status_default.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Default" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认" + } + } + } + }, + "service.configuration.openai.usage_status.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usage Status" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用状态" + } + } + } + }, + "service.configuration.reset" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重置" + } + } + } + }, + "service.configuration.tencent.secret_id.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Id" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Id" + } + } + } + }, + "service.configuration.tencent.secret_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Key" + } + } + } + }, + "service.configuration.validate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证" + } + } + } + }, + "service.configuration.validation_fail" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validation failed!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证失败!" + } + } + } + }, + "service.configuration.validation_success" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validation success!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证成功!" + } + } + } + }, + "setting_general" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "General" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通用" + } + } + } + }, + "setting.disabled.import_app_error.message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to add Application" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法添加应用" + } + } + } + }, + "setting.general.advance.default_tts_service" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Default TTS Service" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认 TTS 服务" + } + } + } + }, + "setting.general.advance.enable_beta_feature" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable Beta features" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用 Beta 特性" + } + } + } + }, + "setting.general.advance.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advance" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高级" + } + } + } + }, + "setting.general.appearance.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appearance" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "外观" + } + } + } + }, + "setting.general.appearance.light_dark_appearance" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appearance" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "外观" + } + } + } + }, + "setting.general.auto_copy.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto Copy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动复制" + } + } + } + }, + "setting.general.auto_query.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto Query" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动查询" + } + } + } + }, + "setting.general.font.font_size.label" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Font Size" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字体大小" + } + } + } + }, + "setting.general.font.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Font" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字体" + } + } + } + }, + "setting.general.input.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Input Field" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入框" + } + } + } + }, + "setting.general.language.duplicated_alert %@%@%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The first language should not be the same as the second language (%1$@). The %2$@ was replaced with %3$@." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一语言不能与第二语言相同(%1$@)。%2$@已被替换为%3$@。" + } + } + } + }, + "setting.general.language.duplicated_alert.field.first" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "first language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第二语言" + } + } + } + }, + "setting.general.language.duplicated_alert.field.second" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "second language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一语言" + } + } + } + }, + "setting.general.language.duplicated_alert.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language Duplicated" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语言重复" + } + } + } + }, + "setting.general.language.first_language" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "First Language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一语言" + } + } + } + }, + "setting.general.language.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语言" + } + } + } + }, + "setting.general.language.language_detect_optimize" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language Detection" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语种识别" + } + } + } + }, + "setting.general.language.second_language" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Second Language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第二语言" + } + } + } + }, + "setting.general.mouse_query.adjust_pop_button_origin" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adjust Query Icon Position to avoid conflict with PopClip" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "调整查询图标位置(避免和 PopClip 显示冲突)" } } } }, - "play_audio" : { + "setting.general.mouse_query.header" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Play Audio" + "value" : "Query with Mouse" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "播放音频" + "value" : "鼠标查询" } } } }, - "play_word_audio" : { + "setting.general.other.header" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Play Word Pronunciation:" + "value" : "Other" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "播放单词发音:" + "value" : "其他" } } } }, - "please_look" : { + "setting.general.quick_link.header" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Please see 👉" + "value" : "Quick Link" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "参见 👉" + "value" : "快捷功能" } } } }, - "plural" : { + "setting.general.voice.auto_play_word_audio" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Plural" + "value" : "Auto play pronunciation after querying English words" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "复数" + "value" : "查询英语单词后自动播放发音" } } } }, - "present_participle" : { + "setting.general.voice.disable_empty_copy_beep_msg" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Present participle" + "value" : "Disable 'beep' when selected text is empty" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "现在分词" + "value" : "划词内容为空时" } } } }, - "privacy" : { - "comment" : "privacy", + "setting.general.voice.header" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Privacy" + "value" : "Sound" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "隐私" + "value" : "声音" } } } }, - "privacy_statement" : { + "setting.general.window.fixed_window_position" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Privacy Statement" + "value" : "Floating Window Position" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "隐私声明" + "value" : "侧悬浮窗口位置" } } } }, - "privacy_statement_content" : { + "setting.general.window.mouse_select_translate_window_type" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Easydict uses AppCenter and Firebase to collect crash logs (for bug fixes) and anonymous analytics data, but these data will not be associated with your identity, only for improving user experience, and will never be shared with other platforms.\n\nEasydict is open source software, if you are interested, you can check the code." + "value" : "Mouse Window Type" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Easydict 使用了 AppCenter 和 Firebase,它们会收集用户的崩溃日志(用于修复 bug)和匿名统计数据,但这些数据都不会关联到您的身份,仅用于改进用户体验,绝不会分享给其他平台。\n\nEasydict 是开源软件,如果你感兴趣,可以查看代码。" + "value" : "鼠标划词窗口类型" } } } }, - "query_failed" : { + "setting.general.window.shortcut_select_translate_window_type" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Query failed" + "value" : "Shortcut Window Type" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "查询失败" + "value" : "快捷键划词窗口类型" } } } }, - "query_in_app" : { + "setting.general.windows.header" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Query in App" + "value" : "Window" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "应用内查询" + "value" : "窗口" } } } }, - "quick_link" : { + "setting.service.detail.no_configuration %@" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Quick Link:" + "value" : "No configuration for %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "快捷功能:" + "value" : "%@没有可供配置的选项" } } } }, - "quit" : { + "setting.service.detail.no_selection" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select a service to show configuration" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "退出" + "value" : "选择服务以查看配置" } } } }, - "replace_text" : { + "setting.tts_service.options.apple" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Replace With Translation" + "value" : "Apple" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "使用译文替换" + "value" : "苹果" } } } }, - "retry" : { + "setting.tts_service.options.baidu" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Retry" + "value" : "Baidu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重试" + "value" : "百度" } } } }, - "root" : { + "setting.tts_service.options.bing" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Root" + "value" : "Bing" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "词根" + "value" : "Bing" } } } }, - "second_language" : { + "setting.tts_service.options.google" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Second Language:" + "value" : "Google" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "第二语言:" + "value" : "Google" } } } }, - "select_translate" : { + "setting.tts_service.options.youdao" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Select Text Translate:" + "value" : "Youdao" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "划词翻译:" + "value" : "有道" } } } }, - "service" : { - "comment" : "service", + "Settings..." : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置..." + } + } + } + }, + "shortcut" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Service" + "value" : "Shortcut" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "服务" + "value" : "快捷键" } } } }, - "setting_general" : { + "shortcut_clear_all" : { + "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "Clear All" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "通用" + "value" : "清空所有" } } } }, - "Settings..." : { + "shortcut_clear_input" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clear Input" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空查询内容" + } + } + } + }, + "shortcut_confict %@" : { + "extractionState" : "manual", "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "设置..." + "value" : "%@" + } + } + } + }, + "shortcut_confict_confirm" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定" + } + } + } + }, + "shortcut_confict_message %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The current shortcut can't be used because it is already used \"%@\"" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前快捷键无法使用,因为它已被\n\"%@\"占用" + } + } + } + }, + "shortcut_confict_title %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut Confict With \"%@\" Unable Use This" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷键\"%@\",无法使用" + } + } + } + }, + "shortcut_copy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制文本" + } + } + } + }, + "shortcut_copy_first_translated_text" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy first translated text" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制第一个翻译结果" + } + } + } + }, + "shortcut_decrease_font" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Decrease Font Size" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "减小字体" + } + } + } + }, + "shortcut_focus" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Focus" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "聚焦" + } + } + } + }, + "shortcut_increase_font" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Increase Font Size" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "增大字体" + } + } + } + }, + "shortcut_play" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "播放单词发音" } } } @@ -2270,13 +3885,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Show Mini Window:" + "value" : "Show Mini Window" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "显示迷你窗口:" + "value" : "显示迷你窗口" } } } @@ -2286,13 +3901,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Silent Screenshot OCR:" + "value" : "Silent Screenshot OCR" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "静默截图 OCR:" + "value" : "静默截图 OCR" } } } @@ -2335,13 +3950,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Snip Translate:" + "value" : "Snip Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "截图翻译:" + "value" : "截图翻译" } } } @@ -2440,7 +4055,14 @@ } }, "Tisfeng" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "toggle_languages" : { "localizations" : { @@ -2496,6 +4118,16 @@ } } }, + "unknown_option" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } + }, "unpin" : { "localizations" : { "en" : { diff --git a/Easydict/Feature/Configuration/Appearance.swift b/Easydict/Feature/Configuration/Appearance.swift index 3a59f1124..eb49593cc 100644 --- a/Easydict/Feature/Configuration/Appearance.swift +++ b/Easydict/Feature/Configuration/Appearance.swift @@ -6,9 +6,10 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import Foundation -@objc enum AppearenceType: Int, CaseIterable { +@objc enum AppearenceType: Int, CaseIterable, Defaults.Serializable { case followSystem = 0 case light case dark diff --git a/Easydict/Feature/Configuration/Configuration+UserData.swift b/Easydict/Feature/Configuration/Configuration+UserData.swift new file mode 100644 index 000000000..0b6784aac --- /dev/null +++ b/Easydict/Feature/Configuration/Configuration+UserData.swift @@ -0,0 +1,63 @@ +// +// Configuration+UserData.swift +// Easydict +// +// Created by ljk on 2024/1/17. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Configuration { + var userDefaultsData: [String: Any] { + let userDefaults = UserDefaults.standard + + var userConfigDict = [String: Any]() + if let bundleIdentifier = Bundle.main.bundleIdentifier, let appUserDefaultsData = userDefaults.persistentDomain(forName: bundleIdentifier) { + for (key, value) in appUserDefaultsData { + if !key.hasPrefix("MASPreferences"), !(value is Data) { + userConfigDict[key] = value + } + } + } + + return userConfigDict + } + + func saveUserDefaultsDataToDownloadFolder() { + writeDictToDownloadFolder(userDefaultsData) + } + + func resetUserDefaultsData() { + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } + UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier) + } + + func writeDictToDownloadFolder(_ dict: [String: Any]) { + let downloadPath = downloadPath + let name = ProcessInfo.processInfo.processName + let date = currentDate + let fileName = "\(name)_\(date).plist" + let plistPath = (downloadPath as NSString?)?.appendingPathComponent(fileName) + guard let path = plistPath else { return } + + let plistData = try? PropertyListSerialization.data(fromPropertyList: dict, format: .binary, options: 0) + try? plistData?.write(to: URL(fileURLWithPath: path)) + } + + var downloadPath: String? { + NSSearchPathForDirectoriesInDomains(.downloadsDirectory, .userDomainMask, true).first + } + + var currentDate: String { + let currentDate = Date() + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + + let formattedDate = formatter.string(from: currentDate) + print("Formatted Date: ", formattedDate) + + return formattedDate + } +} diff --git a/Easydict/Feature/Configuration/Configuration.swift b/Easydict/Feature/Configuration/Configuration.swift new file mode 100644 index 000000000..774f4a193 --- /dev/null +++ b/Easydict/Feature/Configuration/Configuration.swift @@ -0,0 +1,703 @@ +// +// Configuration.swift +// Easydict +// +// Created by ljk on 2024/1/2. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import Defaults +import Foundation + +@objc enum LanguageDetectOptimize: Int { + case none = 0 + case baidu = 1 + case google = 2 +} + +let kEnableBetaNewAppKey = "EZConfiguration_kEnableBetaNewAppKey" +let kHideMenuBarIconKey = "EZConfiguration_kHideMenuBarIconKey" + +@objcMembers class Configuration: NSObject { + private(set) static var shared = Configuration() + + override private init() { + super.init() + DispatchQueue.main.async { [weak self] in + guard let self else { return } + observeKeys() + } + } + + @DefaultsWrapper(.firstLanguage) + var firstLanguage: Language + + @DefaultsWrapper(.secondLanguage) + var secondLanguage: Language + + @DefaultsWrapper(.queryFromLanguage) + var fromLanguage: Language + + @DefaultsWrapper(.queryToLanguage) + var toLanguage: Language + + @DefaultsWrapper(.autoSelectText) + var autoSelectText: Bool + + @DefaultsWrapper(.forceAutoGetSelectedText) + var forceAutoGetSelectedText: Bool + + @DefaultsWrapper(.disableEmptyCopyBeep) + var disableEmptyCopyBeep: Bool // Some apps will beep when empty copy. + + @DefaultsWrapper(.clickQuery) + var clickQuery: Bool + + @DefaultsWrapper(.launchAtStartup) + var launchAtStartup: Bool + + let updater = GlobalContext.shared.updaterController.updater + + var automaticallyChecksForUpdates: Bool { + get { + updater.automaticallyChecksForUpdates + } + set { + updater.automaticallyChecksForUpdates = newValue + logSettings(["automatically_checks_for_updates": newValue]) + } + } + + @DefaultsWrapper(.hideMainWindow) + var hideMainWindow: Bool + + @DefaultsWrapper(.autoQueryOCRText) + var autoQueryOCRText: Bool + + @DefaultsWrapper(.autoQuerySelectedText) + var autoQuerySelectedText: Bool + + @DefaultsWrapper(.autoQueryPastedText) + var autoQueryPastedText: Bool + + @DefaultsWrapper(.autoPlayAudio) + var autoPlayAudio: Bool + + @DefaultsWrapper(.autoCopySelectedText) + var autoCopySelectedText: Bool + + @DefaultsWrapper(.autoCopyOCRText) + var autoCopyOCRText: Bool + + @DefaultsWrapper(.autoCopyFirstTranslatedText) + var autoCopyFirstTranslatedText: Bool + + @DefaultsWrapper(.languageDetectOptimize) + var languageDetectOptimize: LanguageDetectOptimize + + var defaultTTSServiceType: ServiceType { + get { + ServiceType(rawValue: Defaults[.defaultTTSServiceType].rawValue) + } + set { + Defaults[.defaultTTSServiceType] = TTSServiceType(rawValue: newValue.rawValue) ?? .youdao + } + } + + @DefaultsWrapper(.showGoogleQuickLink) + var showGoogleQuickLink: Bool + + @DefaultsWrapper(.showEudicQuickLink) + var showEudicQuickLink: Bool + + @DefaultsWrapper(.showAppleDictionaryQuickLink) + var showAppleDictionaryQuickLink: Bool + + @DefaultsWrapper(.hideMenuBarIcon) + var hideMenuBarIcon: Bool + + @DefaultsWrapper(.enableBetaNewApp) + var enableBetaNewApp: Bool + + @DefaultsWrapper(.fixedWindowPosition) + var fixedWindowPosition: EZShowWindowPosition + + @DefaultsWrapper(.mouseSelectTranslateWindowType) + var mouseSelectTranslateWindowType: EZWindowType + + @DefaultsWrapper(.shortcutSelectTranslateWindowType) + var shortcutSelectTranslateWindowType: EZWindowType + + @DefaultsWrapper(.adjustPopButtonOrigin) + var adjustPopButtomOrigin: Bool + + @DefaultsWrapper(.allowCrashLog) + var allowCrashLog: Bool + + @DefaultsWrapper(.allowAnalytics) + var allowAnalytics: Bool + + @DefaultsWrapper(.clearInput) + var clearInput: Bool + + @DefaultsWrapper(.keepPrevResultWhenEmpty) + var keepPrevResultWhenEmpty: Bool + + @DefaultsWrapper(.selectQueryTextWhenWindowActivate) + var selectQueryTextWhenWindowActivate: Bool + + var disabledAutoSelect: Bool = false + + var isRecordingSelectTextShortcutKey: Bool = false + + let fontSizes: [CGFloat] = [1, 1.1, 1.2, 1.3, 1.4] + + var fontSizeRatio: CGFloat { + fontSizes[Int(fontSizeIndex)] + } + + @DefaultsWrapper(.fontSizeOptionIndex) + var fontSizeIndex: UInt + + @DefaultsWrapper(.appearanceType) + var appearance: AppearenceType + + @DefaultsWrapper(.enableBetaFeature) + private(set) var beta: Bool + + static func destroySharedInstance() { + shared = Configuration() + } + + private func observeKeys() { + cancellables.append( + Defaults.publisher(.firstLanguage) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetFirstLanguage() + } + ) + + cancellables.append( + Defaults.publisher(.secondLanguage) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetSecondLanguage() + } + ) + + cancellables.append( + Defaults.publisher(.autoSelectText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoSelectText() + } + ) + + cancellables.append( + Defaults.publisher(.forceAutoGetSelectedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetForceAutoGetSelectedText() + } + ) + + cancellables.append( + Defaults.publisher(.disableEmptyCopyBeep) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetDisableEmptyCopyBeep() + } + ) + + cancellables.append( + Defaults.publisher(.clickQuery) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetClickQuery() + } + ) + + cancellables.append( + Defaults.publisher(.launchAtStartup) + .removeDuplicates() + .sink { [weak self] change in + self?.didSetLaunchAtStartup(change.oldValue, new: change.newValue) + } + ) + + cancellables.append( + Defaults.publisher(.hideMainWindow) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetHideMainWindow() + } + ) + + cancellables.append( + Defaults.publisher(.autoQueryOCRText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoQueryOCRText() + } + ) + + cancellables.append( + Defaults.publisher(.autoQuerySelectedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoQuerySelectedText() + } + ) + + cancellables.append( + Defaults.publisher(.autoQueryPastedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoQueryPastedText() + } + ) + + cancellables.append( + Defaults.publisher(.autoPlayAudio) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoPlayAudio() + } + ) + + cancellables.append( + Defaults.publisher(.autoCopySelectedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoCopySelectedText() + } + ) + + cancellables.append( + Defaults.publisher(.autoCopyOCRText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoCopyOCRText() + } + ) + + cancellables.append( + Defaults.publisher(.autoCopyFirstTranslatedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoCopyFirstTranslatedText() + } + ) + + cancellables.append( + Defaults.publisher(.languageDetectOptimize) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetLanguageDetectOptimize() + } + ) + + cancellables.append( + Defaults.publisher(.defaultTTSServiceType) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetDefaultTTSServiceType() + } + ) + + cancellables.append( + Defaults.publisher(.showGoogleQuickLink) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShowGoogleQuickLink() + } + ) + + cancellables.append( + Defaults.publisher(.showEudicQuickLink) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShowEudicQuickLink() + } + ) + + cancellables.append( + Defaults.publisher(.showAppleDictionaryQuickLink) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShowAppleDictionaryQuickLink() + } + ) + + cancellables.append( + Defaults.publisher(.hideMenuBarIcon) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetHideMenuBarIcon() + } + ) + + cancellables.append( + Defaults.publisher(.enableBetaNewApp) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetEnableBetaNewApp() + } + ) + + cancellables.append( + Defaults.publisher(.fixedWindowPosition) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetFixedWindowPosition() + } + ) + + cancellables.append( + Defaults.publisher(.mouseSelectTranslateWindowType) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetMouseSelectTranslateWindowType() + } + ) + + cancellables.append( + Defaults.publisher(.shortcutSelectTranslateWindowType) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShortcutSelectTranslateWindowType() + } + ) + + cancellables.append( + Defaults.publisher(.adjustPopButtonOrigin) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAdjustPopButtomOrigin() + } + ) + + cancellables.append( + Defaults.publisher(.allowCrashLog) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAllowCrashLog() + } + ) + + cancellables.append( + Defaults.publisher(.allowAnalytics) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAllowAnalytics() + } + ) + + cancellables.append( + Defaults.publisher(.clearInput) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetClearInput() + } + ) + + cancellables.append( + Defaults.publisher(.fontSizeOptionIndex) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetFontSizeIndex() + } + ) + + cancellables.append( + Defaults.publisher(.appearanceType) + .removeDuplicates() + .sink { [weak self] change in + let newValue = change.newValue + + self?.didSetAppearance(newValue) + } + ) + } + + var cancellables: [AnyCancellable] = [] + + func enableBetaFeaturesIfNeeded() { + guard beta else { return } + } +} + +// MARK: setter + +private extension Configuration { + func didSetFirstLanguage() { + logSettings(["first_language": firstLanguage]) + } + + func didSetSecondLanguage() { + logSettings(["second_language": secondLanguage]) + } + + func didSetAutoSelectText() { + logSettings(["auto_select_sext": autoSelectText]) + } + + func didSetForceAutoGetSelectedText() { + logSettings(["force_get_selected_text": forceAutoGetSelectedText]) + } + + func didSetDisableEmptyCopyBeep() { + logSettings(["disableEmptyCopyBeep": disableEmptyCopyBeep]) + } + + func didSetClickQuery() { + EZWindowManager.shared().updatePopButtonQueryAction() + + logSettings(["click_query": clickQuery]) + } + + func didSetLaunchAtStartup(_ old: Bool, new: Bool) { + if new != old { + updateLoginItemWithLaunchAtStartup(new) + } + + logSettings(["launch_at_startup": new]) + } + + func didSetAutomaticallyChecksForUpdates() { + logSettings(["automatically_checks_for_updates": automaticallyChecksForUpdates]) + } + + func didSetHideMainWindow() { + let windowManger = EZWindowManager.shared() + windowManger.updatePopButtonQueryAction() + if hideMainWindow { + windowManger.closeMainWindowIfNeeded() + } + + logSettings(["hide_main_window": hideMainWindow]) + } + + func didSetAutoQueryOCRText() { + logSettings(["auto_query_ocr_text": autoQueryOCRText]) + } + + func didSetAutoQuerySelectedText() { + logSettings(["auto_query_selected_text": autoQuerySelectedText]) + } + + func didSetAutoQueryPastedText() { + logSettings(["auto_query_pasted_text": autoQueryPastedText]) + } + + func didSetAutoPlayAudio() { + logSettings(["auto_play_word_audio": autoPlayAudio]) + } + + func didSetAutoCopySelectedText() { + logSettings(["auto_copy_selected_text": autoCopySelectedText]) + } + + func didSetAutoCopyOCRText() { + logSettings(["auto_copy_ocr_text": autoCopyOCRText]) + } + + func didSetAutoCopyFirstTranslatedText() { + logSettings(["auto_copy_first_translated_text": autoCopyFirstTranslatedText]) + } + + func didSetLanguageDetectOptimize() { + logSettings(["detect_optimize": languageDetectOptimize]) + } + + func didSetDefaultTTSServiceType() { + let value = defaultTTSServiceType + logSettings(["tts": value]) + } + + func didSetShowGoogleQuickLink() { + postUpdateQuickLinkButtonNotification() + + EZMenuItemManager.shared().googleItem?.isHidden = !showGoogleQuickLink + + logSettings(["show_google_link": showGoogleQuickLink]) + } + + func didSetShowEudicQuickLink() { + postUpdateQuickLinkButtonNotification() + + EZMenuItemManager.shared().eudicItem?.isHidden = !showEudicQuickLink + + logSettings(["show_eudic_link": showEudicQuickLink]) + } + + func didSetShowAppleDictionaryQuickLink() { + postUpdateQuickLinkButtonNotification() + + EZMenuItemManager.shared().appleDictionaryItem?.isHidden = !showAppleDictionaryQuickLink + + logSettings(["show_apple_dictionary_link": showAppleDictionaryQuickLink]) + } + + func didSetHideMenuBarIcon() { + if !NewAppManager.shared.enable { + hideMenuBarIcon(hidden: hideMenuBarIcon) + } + + logSettings(["hide_menu_bar_icon": hideMenuBarIcon]) + } + + func didSetEnableBetaNewApp() { + logSettings(["enable_beta_new_app": enableBetaNewApp]) + } + + func didSetFixedWindowPosition() { + logSettings(["show_fixed_window_position": fixedWindowPosition]) + } + + func didSetMouseSelectTranslateWindowType() { + logSettings(["show_mouse_window_type": mouseSelectTranslateWindowType]) + } + + func didSetShortcutSelectTranslateWindowType() { + logSettings(["show_shortcut_window_type": shortcutSelectTranslateWindowType]) + } + + func didSetAdjustPopButtomOrigin() { + logSettings(["adjust_pop_buttom_origin": adjustPopButtomOrigin]) + } + + func didSetAllowCrashLog() { + EZLog.setCrashEnabled(allowCrashLog) + logSettings(["allow_crash_log": allowCrashLog]) + } + + func didSetAllowAnalytics() { + logSettings(["allow_analytics": allowAnalytics]) + } + + func didSetClearInput() { + logSettings(["clear_input": clearInput]) + } + + func didSetFontSizeIndex() { + NotificationCenter.default.post(name: .init(ChangeFontSizeView.changeFontSizeNotificationName), object: nil) + } + + func didSetAppearance(_ appearance: AppearenceType) { + DarkModeManager.sharedManager().updateDarkMode(appearance.rawValue) + } +} + +// MARK: Window Frame + +extension Configuration { + func windowFrameWithType(_ windowType: EZWindowType) -> CGRect { + Defaults[.windorFrame(for: windowType)] + } + + func setWindowFrame(_ frame: CGRect, windowType: EZWindowType) { + Defaults[.windorFrame(for: windowType)] = frame + } +} + +// MARK: Intelligent Query Text Type of Service + +extension Configuration { + func setIntelligentQueryTextType(_ queryTextType: EZQueryTextType, serviceType: ServiceType) { + Defaults[.intelligentQueryTextType(for: serviceType)] = queryTextType + } + + func intelligentQueryTextTypeForServiceType(_ serviceType: ServiceType) -> EZQueryTextType { + Defaults[.intelligentQueryTextType(for: serviceType)] + } +} + +// MARK: Intelligent Query Text Type of Service + +extension Configuration { + func setQueryTextType(_ queryTextType: EZQueryTextType, serviceType: ServiceType) { + Defaults[.queryTextType(for: serviceType)] = queryTextType + } + + func queryTextTypeForServiceType(_ serviceType: ServiceType) -> EZQueryTextType { + Defaults[.queryTextType(for: serviceType)] + } +} + +// MARK: Intelligent Query Mode + +extension Configuration { + func setIntelligentQueryMode(_ enabled: Bool, windowType: EZWindowType) { + let key = EZConstKey.constkey("IntelligentQueryMode", windowType: windowType) + let stringValue = "\(enabled)" + UserDefaults.standard.set(stringValue, forKey: key) + + let parameters = [ + "enabled": enabled, + "window_type": windowType.rawValue, + ] as [String: Any] + + EZLog.logEvent(withName: "intelligent_query_mode", parameters: parameters) + } + + func intelligentQueryModeForWindowType(_ windowType: EZWindowType) -> Bool { + let key = EZConstKey.constkey("IntelligentQueryMode", windowType: windowType) + let defaultValue = "0" + // Turn on intelligent query mode by default in mini window. + if windowType == .mini { + return true + } + return UserDefaults.standard.string(forKey: key) ?? defaultValue == "1" + } +} + +private extension Configuration { + func postUpdateQuickLinkButtonNotification() { + let notification = Notification(name: .init("EZQuickLinkButtonUpdateNotification"), object: nil) + NotificationCenter.default.post(notification) + } + + func hideMenuBarIcon(hidden: Bool) { + if hidden { + EZMenuItemManager.shared().remove() + } else { + EZMenuItemManager.shared().setup() + } + } + + func updateLoginItemWithLaunchAtStartup(_ launchAtStartup: Bool) { + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String + let appBundlePath = Bundle.main.bundlePath + + let script = """ + tell application "System Events" to get the name of every login item + tell application "System Events" + set loginItems to every login item + repeat with aLoginItem in loginItems + if (aLoginItem's name is "\(appName ?? "")") then + delete aLoginItem + end if + end repeat + if \(launchAtStartup) then + make login item at end with properties {path:"\(appBundlePath)", hidden:false} + end if + end tell + """ + + let exeCommand = EZScriptExecutor() + exeCommand.runAppleScript(script) { result, error in + if let error { + MMLogInfo("launchAtStartup error: error: \(error)") + } else { + print("launchAtStartup result:", result) + } + } + } + + func logSettings(_ parameters: [String: Any]) { + EZLog.logEvent(withName: "settings", parameters: parameters) + } +} diff --git a/Easydict/Feature/Configuration/EZConfiguration.h b/Easydict/Feature/Configuration/EZConfiguration.h index bc07911ee..5cf592d6c 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.h +++ b/Easydict/Feature/Configuration/EZConfiguration.h @@ -9,6 +9,7 @@ #import #import "EZLanguageManager.h" #import "EZLayoutManager.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -23,6 +24,7 @@ static NSString *const EZFontSizeUpdateNotification = @"EZFontSizeUpdateNotifica static NSString *const EZIntelligentQueryModeKey = @"IntelligentQueryMode"; + typedef NS_ENUM(NSUInteger, EZLanguageDetectOptimize) { EZLanguageDetectOptimizeNone = 0, EZLanguageDetectOptimizeBaidu = 1, @@ -71,6 +73,7 @@ typedef NS_ENUM(NSUInteger, EZAppearenceType) { @property (nonatomic, assign) BOOL allowCrashLog; @property (nonatomic, assign) BOOL allowAnalytics; @property (nonatomic, assign) BOOL clearInput; +@property (nonatomic, assign) BOOL keepPrevResult; // TODO: Need to move them. These are read/write properties only and will not be stored locally, only for external use. /// Only use when showing NSOpenPanel to select disabled apps. @@ -83,6 +86,8 @@ typedef NS_ENUM(NSUInteger, EZAppearenceType) { @property (nonatomic, assign) EZAppearenceType appearance; +@property (nonatomic, strong, readonly) SPUUpdater *updater; + + (instancetype)shared; + (void)destroySharedInstance; diff --git a/Easydict/Feature/Configuration/EZConfiguration.m b/Easydict/Feature/Configuration/EZConfiguration.m index 939744f59..407f9883c 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.m +++ b/Easydict/Feature/Configuration/EZConfiguration.m @@ -52,6 +52,7 @@ static NSString *const kAllowCrashLogKey = @"EZConfiguration_kAllowCrashLogKey"; static NSString *const kAllowAnalyticsKey = @"EZConfiguration_kAllowAnalyticsKey"; static NSString *const kClearInputKey = @"EZConfiguration_kClearInputKey"; +static NSString *const kKeepPrevResultKey = @"EZConfiguration_kKeepPrevResultKey"; static NSString *const kTranslationControllerFontKey = @"EZConfiguration_kTranslationControllerFontKey"; static NSString *const kApperanceKey = @"EZConfiguration_kApperanceKey"; @@ -63,7 +64,7 @@ @interface EZConfiguration () @property (nonatomic, strong) AppDelegate *appDelegate; -@property (nonatomic, strong) SPUUpdater *updater; +@property (nonatomic, strong, readwrite) SPUUpdater *updater; @end @@ -126,6 +127,7 @@ - (void)setup { self.allowCrashLog = [NSUserDefaults mm_readBool:kAllowCrashLogKey defaultValue:YES]; self.allowAnalytics = [NSUserDefaults mm_readBool:kAllowAnalyticsKey defaultValue:YES]; self.clearInput = [NSUserDefaults mm_readBool:kClearInputKey defaultValue:NO]; + self.keepPrevResult = [NSUserDefaults mm_readBool:kKeepPrevResultKey defaultValue:YES]; self.fontSizes = @[@(1), @(1.1), @(1.2), @(1.3), @(1.4)]; [[NSUserDefaults standardUserDefaults]registerDefaults:@{kTranslationControllerFontKey: @(0)}]; @@ -142,12 +144,12 @@ - (BOOL)launchAtStartup { return launchAtStartup; } -- (BOOL)automaticallyChecksForUpdates { - return self.updater.automaticallyChecksForUpdates; +- (SPUUpdater *)updater { + return GlobalContext.shared.updaterController.updater; } -- (SPUUpdater *)updater { - return self.appDelegate.updaterController.updater; +- (BOOL)automaticallyChecksForUpdates { + return self.updater.automaticallyChecksForUpdates; } #pragma mark - setter @@ -432,6 +434,14 @@ - (void)setClearInput:(BOOL)clearInput { [self logSettings:@{@"clear_input" : @(clearInput)}]; } +- (void)setKeepPrevResult:(BOOL)keepPrevResult { + _keepPrevResult = keepPrevResult; + + [NSUserDefaults mm_write:@(keepPrevResult) forKey:kKeepPrevResultKey]; + + [self logSettings:@{@"keep_prev_result": @(keepPrevResult)}]; +} + - (void)setFontSizeIndex:(NSInteger)fontSizeIndex { NSInteger targetIndex = MIN(_fontSizes.count-1, MAX(fontSizeIndex, 0)); @@ -455,7 +465,7 @@ - (void)setAppearance:(EZAppearenceType)appearance { [NSUserDefaults mm_write:@(appearance) forKey:kApperanceKey]; - [[DarkModeManager manager] updateDarkMode]; + [[DarkModeManager manager] updateDarkMode:appearance]; } #pragma mark - Window Frame @@ -665,7 +675,7 @@ - (void)setBeta:(BOOL)beta { [NSUserDefaults mm_write:stringValue forKey:EZBetaFeatureKey]; } - (BOOL)isBeta { - NSString *stringValue = [NSUserDefaults mm_readString:EZBetaFeatureKey defaultValue:@"0"]; + NSString *stringValue = [NSUserDefaults mm_read:EZBetaFeatureKey]; BOOL isBeta = [stringValue boolValue]; return isBeta; } diff --git a/Easydict/Feature/DarkMode/DarkModeManager.h b/Easydict/Feature/DarkMode/DarkModeManager.h index 722bee8a3..446803e26 100644 --- a/Easydict/Feature/DarkMode/DarkModeManager.h +++ b/Easydict/Feature/DarkMode/DarkModeManager.h @@ -10,15 +10,14 @@ NS_ASSUME_NONNULL_BEGIN - @interface DarkModeManager : NSObject @property (nonatomic, assign, readonly) BOOL systemDarkMode; -+ (instancetype)manager; ++ (instancetype)manager NS_SWIFT_NAME(sharedManager()); - (void)excuteLight:(void (^)(void))light dark:(void (^)(void))dark; -- (void)updateDarkMode; +- (void)updateDarkMode:(NSInteger)apperance; @end diff --git a/Easydict/Feature/DarkMode/DarkModeManager.m b/Easydict/Feature/DarkMode/DarkModeManager.m index af749d661..7dcdf5e59 100644 --- a/Easydict/Feature/DarkMode/DarkModeManager.m +++ b/Easydict/Feature/DarkMode/DarkModeManager.m @@ -42,14 +42,18 @@ - (void)excuteLight:(void (^)(void))light dark:(void (^)(void))dark { - (void)monitor { NSString *const darkModeNotificationName = @"AppleInterfaceThemeChangedNotification"; - [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDarkMode) name:darkModeNotificationName object:nil]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(themeDidChange) name:darkModeNotificationName object:nil]; } -- (void)updateDarkMode { +- (void)themeDidChange { + [self updateDarkMode:Configuration.shared.appearance]; +} + +- (void)updateDarkMode:(NSInteger)apperance { BOOL isDarkMode = [self isDarkMode]; NSLog(@"%@", isDarkMode ? @"深色模式" : @"浅色模式"); - AppearenceType type = (AppearenceType)EZConfiguration.shared.appearance; + AppearenceType type = apperance; switch (type) { case AppearenceTypeDark: self.systemDarkMode = true; diff --git a/Easydict/Feature/EventMonitor/EZEventMonitor.m b/Easydict/Feature/EventMonitor/EZEventMonitor.m index 2578efd37..f3acfe5ab 100644 --- a/Easydict/Feature/EventMonitor/EZEventMonitor.m +++ b/Easydict/Feature/EventMonitor/EZEventMonitor.m @@ -17,6 +17,7 @@ #import "EZLocalStorage.h" #import "EZAppleScriptManager.h" #import "EZSystemUtility.h" +#import "Easydict-Swift.h" static CGFloat const kDismissPopButtonDelayTime = 0.1; static NSTimeInterval const kDelayGetSelectedTextTime = 0.1; @@ -300,7 +301,7 @@ - (void)getSelectedText:(BOOL)checkTextFrame completion:(void (^)(NSString *_Nul self.selectTextType = EZSelectTextTypeAccessibility; // Monitor CGEventTap must be required after using Accessibility successfully. - if (EZConfiguration.shared.autoSelectText) { + if (Configuration.shared.autoSelectText) { [self monitorCGEventTap]; } @@ -407,7 +408,7 @@ - (void)autoGetSelectedText:(BOOL)checkTextFrame { } - (BOOL)enabledAutoSelectText { - EZConfiguration *config = [EZConfiguration shared]; + Configuration *config = [Configuration shared]; BOOL enabled = config.autoSelectText && !config.disabledAutoSelect; if (!enabled) { NSLog(@"disabled autoSelectText"); @@ -437,7 +438,7 @@ - (void)getSelectedTextBySimulatedKey:(void (^)(NSString *_Nullable))completion // If playing audio, we do not silence system volume. [EZAudioUtils isPlayingAudio:^(BOOL isPlaying) { - BOOL shouldTurnOffSoundTemporarily = EZConfiguration.shared.disableEmptyCopyBeep && !isPlaying; + BOOL shouldTurnOffSoundTemporarily = Configuration.shared.disableEmptyCopyBeep && !isPlaying; // Set volume to 0 to avoid system alert. if (shouldTurnOffSoundTemporarily) { @@ -596,7 +597,7 @@ - (BOOL)isAccessibilityEnabled { /// Check if should use simulation key to get selected text. - (BOOL)shouldUseSimulatedKey:(NSString *)text error:(AXError)error { BOOL isAutoSelectQuery = self.actionType == EZActionTypeAutoSelectQuery; - BOOL allowedForceAutoGetSelectedText = [EZConfiguration.shared forceAutoGetSelectedText]; + BOOL allowedForceAutoGetSelectedText = [Configuration.shared forceAutoGetSelectedText]; NSString *easydictBundleID = [[NSBundle mainBundle] bundleIdentifier]; @@ -610,7 +611,7 @@ - (BOOL)shouldUseSimulatedKey:(NSString *)text error:(AXError)error { FIX: https://github.com/tisfeng/Easydict/issues/192#issuecomment-1797878909 */ - if (isInEasydict && EZConfiguration.shared.isRecordingSelectTextShortcutKey) { + if (isInEasydict && Configuration.shared.isRecordingSelectTextShortcutKey) { return NO; } diff --git a/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m b/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m index 477b392e3..8df326346 100644 --- a/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m +++ b/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m @@ -16,6 +16,7 @@ #import "EZConfiguration.h" #import "NSImage+EZSymbolmage.h" #import "NSImage+EZResize.h" +#import "Easydict-Swift.h" static CGFloat const kMargin = 20; static CGFloat const kRowHeight = 45; @@ -56,7 +57,7 @@ - (void)viewDidLoad { } - (void)setup { - self.appModelList = [[EZLocalStorage.shared selectTextTypeAppModelList] mutableCopy]; + [self setupAppModelList]; [self.titleTextField mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.inset(kMargin + 5); // ???: Why is the actual inset is 18? @@ -77,6 +78,18 @@ - (void)setup { }]; } +- (void)setupAppModelList { + self.appModelList = [[NSMutableArray alloc] init]; + NSArray *allAppModelList = [EZLocalStorage.shared selectTextTypeAppModelList]; + NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; + for (EZAppModel *appModel in allAppModelList) { + NSURL *appURL = [workspace URLForApplicationWithBundleIdentifier:appModel.appBundleID]; + if (appURL) { + [self.appModelList addObject:appModel]; + } + } +} + #pragma mark - Getter && Setter @@ -254,7 +267,7 @@ - (void)selectApp { [openPanel setAllowedContentTypes:allowedTypes]; // ???: Since [auto select] will cause lag when dragging select apps, I don't know why 😰 - EZConfiguration.shared.disabledAutoSelect = YES; + Configuration.shared.disabledAutoSelect = YES; NSModalResponse result = [openPanel runModal]; if (result == NSModalResponseOK) { @@ -267,7 +280,7 @@ - (void)selectApp { [self.tableView reloadData]; } - EZConfiguration.shared.disabledAutoSelect = NO; + Configuration.shared.disabledAutoSelect = NO; } - (NSArray *)appModelsFromBundleIDDict:(NSDictionary *)appBundleIDDict { diff --git a/Easydict/Feature/PerferenceWindow/EZAboutViewController.m b/Easydict/Feature/PerferenceWindow/EZAboutViewController.m index 67fc4d277..9196c696a 100644 --- a/Easydict/Feature/PerferenceWindow/EZAboutViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZAboutViewController.m @@ -10,6 +10,7 @@ #import "EZBlueTextButton.h" #import "EZConfiguration.h" #import "EZMenuItemManager.h" +#import "Easydict-Swift.h" @interface EZAboutViewController () @@ -35,7 +36,7 @@ - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; - self.autoCheckUpdateButton.mm_isOn = EZConfiguration.shared.automaticallyChecksForUpdates; + self.autoCheckUpdateButton.mm_isOn = Configuration.shared.automaticallyChecksForUpdates; [self updateViewSize]; @@ -177,7 +178,7 @@ - (void)updateViewConstraints { #pragma mark - Actions - (void)autoCheckUpdateButtonClicked:(NSButton *)sender { - EZConfiguration.shared.automaticallyChecksForUpdates = sender.mm_isOn; + Configuration.shared.automaticallyChecksForUpdates = sender.mm_isOn; } - (void)updateLatestVersion { diff --git a/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m b/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m index d0f75f40b..fb00a64e3 100644 --- a/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m @@ -11,6 +11,7 @@ #import "EZConfiguration.h" #import "NSViewController+EZWindow.h" #import "NSImage+EZSymbolmage.h" +#import "Easydict-Swift.h" @interface EZPrivacyViewController () @@ -61,7 +62,7 @@ - (void)setupUI { action:@selector(analyticsButtonClicked:)]; [self.contentView addSubview:self.analyticsButton]; - EZConfiguration *configuration = [EZConfiguration shared]; + Configuration *configuration = [Configuration shared]; self.crashLogButton.mm_isOn = configuration.allowCrashLog; self.analyticsButton.mm_isOn = configuration.allowAnalytics; } @@ -120,16 +121,16 @@ - (void)crashLogButtonClicked:(NSButton *)sender { } else { sender.mm_isOn = YES; } - EZConfiguration.shared.allowCrashLog = sender.mm_isOn; + Configuration.shared.allowCrashLog = sender.mm_isOn; }]; } else { - EZConfiguration.shared.allowCrashLog = YES; + Configuration.shared.allowCrashLog = YES; } } - (void)analyticsButtonClicked:(NSButton *)sender { - EZConfiguration.shared.allowAnalytics = sender.mm_isOn; + Configuration.shared.allowAnalytics = sender.mm_isOn; } #pragma mark - MASPreferencesViewController @@ -143,12 +144,14 @@ - (NSString *)toolbarItemLabel { } - (NSImage *)toolbarItemImage { - // NSImage *privacyImage = [NSImage imageWithSystemSymbolName:@"hand.raised.square.fill" accessibilityDescription:nil]; - // privacyImage = [privacyImage imageWithTintColor:[NSColor mm_colorWithHexString:@"#1296DB"]]; - // privacyImage = [privacyImage resizeToSize:CGSizeMake(14, 14)]; - NSImage *privacyImage = [NSImage imageNamed:@"toolbar_privacy"]; - privacyImage = [NSImage ez_imageWithSymbolName:@"hand.raised.square" size:CGSizeMake(18, 16)]; + /** + SF image "hand.raised.square" require macOS 12.0 + Fix: https://github.com/tisfeng/Easydict/pull/212#discussion_r1437951644 + */ + if (@available(macOS 12.0, *)) { + privacyImage = [NSImage ez_imageWithSymbolName:@"hand.raised.square" size:CGSizeMake(18, 16)]; + } privacyImage = [privacyImage imageWithTintColor:[NSColor ez_imageTintBlueColor]]; return privacyImage; diff --git a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m index 528ab13e0..a2d28b21e 100644 --- a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m @@ -17,7 +17,7 @@ @interface EZSettingViewController () -@property (nonatomic, strong) EZConfiguration *config; +@property (nonatomic, strong) Configuration *config; @property (nonatomic, strong) NSTextField *selectLabel; @property (nonatomic, strong) NSTextField *inputLabel; @@ -74,8 +74,10 @@ @interface EZSettingViewController () @property (nonatomic, strong) NSTextField *playAudioLabel; @property (nonatomic, strong) NSButton *autoPlayAudioButton; -@property (nonatomic, strong) NSTextField *clearInputLabel; +@property (nonatomic, strong) NSTextField *inputFieldLabel; @property (nonatomic, strong) NSButton *clearInputButton; +@property (nonatomic, strong) NSButton *keepPrevResultButton; +@property (nonatomic, strong) NSButton *selectQueryTextWhenWindowActivateButton; @property (nonatomic, strong) NSTextField *autoQueryLabel; @property (nonatomic, strong) NSButton *autoQueryOCRTextButton; @@ -153,7 +155,7 @@ - (void)viewDidLoad { [super viewDidLoad]; // Do view setup here. - self.config = [EZConfiguration shared]; + self.config = [Configuration shared]; [self setupUI]; @@ -165,7 +167,7 @@ - (void)viewDidLoad { // Observe selectionShortcutView.recording status. [self.KVOController observe:self.selectionShortcutView keyPath:@"recording" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZSettingViewController *settingVC, MASShortcutView *selectionShortcutView, NSDictionary *_Nonnull change) { - EZConfiguration.shared.isRecordingSelectTextShortcutKey = [change[NSKeyValueChangeNewKey] boolValue]; + Configuration.shared.isRecordingSelectTextShortcutKey = [change[NSKeyValueChangeNewKey] boolValue]; }]; } @@ -395,14 +397,23 @@ - (void)setupUI { self.autoPlayAudioButton = [NSButton checkboxWithTitle:autoPlayAudioTitle target:self action:@selector(autoPlayAudioButtonClicked:)]; [self.contentView addSubview:self.autoPlayAudioButton]; - NSTextField *clearInputLabel = [NSTextField labelWithString:NSLocalizedString(@"clear_input", nil)]; - clearInputLabel.font = font; - [self.contentView addSubview:clearInputLabel]; - self.clearInputLabel = clearInputLabel; + NSString *inputFieldLabelTitle = [NSString stringWithFormat:@"%@:", NSLocalizedString(@"setting.general.input.header", nil)]; + NSTextField *inputFieldLabel = [NSTextField labelWithString:inputFieldLabelTitle]; + inputFieldLabel.font = font; + [self.contentView addSubview:inputFieldLabel]; + self.inputFieldLabel = inputFieldLabel; NSString *clearInputTitle = NSLocalizedString(@"clear_input_when_translating", nil); self.clearInputButton = [NSButton checkboxWithTitle:clearInputTitle target:self action:@selector(clearInputButtonClicked:)]; [self.contentView addSubview:self.clearInputButton]; + + NSString *keepPrevResultTitle = NSLocalizedString(@"keep_prev_result_when_selected_text_is_empty", nil); + self.keepPrevResultButton = [NSButton checkboxWithTitle:keepPrevResultTitle target:self action:@selector(keepPrevResultButtonClicked:)]; + [self.contentView addSubview:self.keepPrevResultButton]; + + NSString *selectQueryTextWhenWindowActivateTitle = NSLocalizedString(@"select_query_text_when_window_activate", nil); + self.selectQueryTextWhenWindowActivateButton = [NSButton checkboxWithTitle:selectQueryTextWhenWindowActivateTitle target:self action:@selector(selectQueryTextWhenWindowActivateButtonClicked:)]; + [self.contentView addSubview:self.selectQueryTextWhenWindowActivateButton]; NSTextField *autoQueryLabel = [NSTextField labelWithString:NSLocalizedString(@"auto_query", nil)]; autoQueryLabel.font = font; @@ -495,7 +506,7 @@ - (void)setupUI { self.hideMenuBarIconButton = [NSButton checkboxWithTitle:hideMenuBarIcon target:self action:@selector(hideMenuBarIconButtonClicked:)]; [self.contentView addSubview:self.hideMenuBarIconButton]; - if (EasydictNewAppManager.shared.showEnableToggleUI) { + if (@available(macOS 13.0, *)) { NSTextField *betaNewAppLabel = [NSTextField labelWithString:NSLocalizedString(@"beta_new_app", nil)]; betaNewAppLabel.font = font; [self.contentView addSubview:betaNewAppLabel]; @@ -546,6 +557,8 @@ - (void)setupUI { self.autoPlayAudioButton.mm_isOn = self.config.autoPlayAudio; self.clearInputButton.mm_isOn = self.config.clearInput; + self.keepPrevResultButton.mm_isOn = self.config.keepPrevResultWhenEmpty; + self.selectQueryTextWhenWindowActivateButton.mm_isOn = self.config.selectQueryTextWhenWindowActivate; self.launchAtStartupButton.mm_isOn = self.config.launchAtStartup; self.hideMainWindowButton.mm_isOn = self.config.hideMainWindow; self.autoQueryOCRTextButton.mm_isOn = self.config.autoQueryOCRText; @@ -558,7 +571,9 @@ - (void)setupUI { self.showEudicQuickLinkButton.mm_isOn = self.config.showEudicQuickLink; self.showAppleDictionaryQuickLinkButton.mm_isOn = self.config.showAppleDictionaryQuickLink; self.hideMenuBarIconButton.mm_isOn = self.config.hideMenuBarIcon; - self.enableBetaNewAppButton.mm_isOn = self.config.enableBetaNewApp; + if (@available(macOS 13.0, *)) { + self.enableBetaNewAppButton.mm_isOn = self.config.enableBetaNewApp; + } } - (void)updateViewConstraints { @@ -745,19 +760,26 @@ - (void)updateViewConstraints { }]; - [self.clearInputLabel mas_remakeConstraints:^(MASConstraintMaker *make) { + [self.inputFieldLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.autoGetSelectedTextLabel); make.top.equalTo(self.autoPlayAudioButton.mas_bottom).offset(self.verticalPadding); }]; [self.clearInputButton mas_remakeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.clearInputLabel.mas_right).offset(self.horizontalPadding); - make.centerY.equalTo(self.clearInputLabel); + make.left.equalTo(self.inputFieldLabel.mas_right).offset(self.horizontalPadding); + make.centerY.equalTo(self.inputFieldLabel); + }]; + [self.keepPrevResultButton mas_remakeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.clearInputButton); + make.top.equalTo(self.clearInputButton.mas_bottom).offset(self.verticalPadding); + }]; + [self.selectQueryTextWhenWindowActivateButton mas_remakeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.clearInputButton); + make.top.equalTo(self.keepPrevResultButton.mas_bottom).offset(self.verticalPadding); }]; - [self.autoQueryLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.autoGetSelectedTextLabel); - make.top.equalTo(self.clearInputButton.mas_bottom).offset(self.verticalPadding); + make.top.equalTo(self.selectQueryTextWhenWindowActivateButton.mas_bottom).offset(self.verticalPadding); }]; [self.autoQueryOCRTextButton mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.autoQueryLabel.mas_right).offset(self.horizontalPadding); @@ -865,8 +887,8 @@ - (void)updateViewConstraints { make.left.equalTo(self.menuBarIconLabel.mas_right).offset(self.horizontalPadding); make.centerY.equalTo(self.menuBarIconLabel); }]; - - if (EasydictNewAppManager.shared.showEnableToggleUI) { + + if (@available(macOS 13.0, *)) { [self.betaNewAppLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.autoGetSelectedTextLabel); make.top.equalTo(self.hideMenuBarIconButton.mas_bottom).offset(self.verticalPadding); @@ -875,14 +897,12 @@ - (void)updateViewConstraints { make.left.equalTo(self.betaNewAppLabel.mas_right).offset(self.horizontalPadding); make.centerY.equalTo(self.betaNewAppLabel); }]; - } - - self.topmostView = self.inputLabel; - if (EasydictNewAppManager.shared.showEnableToggleUI) { self.bottommostView = self.enableBetaNewAppButton; } else { self.bottommostView = self.hideMenuBarIconButton; } + + self.topmostView = self.inputLabel; if ([EZLanguageManager.shared isSystemChineseFirstLanguage]) { self.leftmostView = self.adjustQueryIconPostionLabel; @@ -967,6 +987,14 @@ - (void)clearInputButtonClicked:(NSButton *)sender { self.config.clearInput = sender.mm_isOn; } +- (void)keepPrevResultButtonClicked:(NSButton *)sender { + self.config.keepPrevResultWhenEmpty = sender.mm_isOn; +} + +- (void)selectQueryTextWhenWindowActivateButtonClicked:(NSButton *)sender { + self.config.selectQueryTextWhenWindowActivate = sender.mm_isOn; +} + - (void)autoCopySelectedTextButtonClicked:(NSButton *)sender { self.config.autoCopySelectedText = sender.mm_isOn; } diff --git a/Easydict/Feature/Service/Ali/AliService.swift b/Easydict/Feature/Service/Ali/AliService.swift index 6ba555109..f783ac636 100644 --- a/Easydict/Feature/Service/Ali/AliService.swift +++ b/Easydict/Feature/Service/Ali/AliService.swift @@ -8,6 +8,7 @@ import Alamofire import CryptoKit +import Defaults import Foundation @objc(EZAliService) @@ -36,6 +37,12 @@ class AliService: QueryService { NSLocalizedString("ali_translate", comment: "The name of Ali Translate") } + override func hasPrivateAPIKey() -> Bool { + let id = Defaults[.aliAccessKeyId] ?? "" + let secret = Defaults[.aliAccessKeySecret] ?? "" + return !id.isEmpty && !secret.isEmpty + } + override public func supportLanguagesDictionary() -> MMOrderedDictionary { // TODO: Replace MMOrderedDictionary in the API let orderedDict = MMOrderedDictionary() @@ -76,8 +83,10 @@ class AliService: QueryService { easydict://writeKeyValue?EZAliAccessKeyId= easydict://writeKeyValue?EZAliAccessKeySecret= */ - if let id = UserDefaults.standard.string(forKey: EZAliAccessKeyId), - let secret = UserDefaults.standard.string(forKey: EZAliAccessKeySecret), !id.isEmpty, !secret.isEmpty + if let id = Defaults[.aliAccessKeyId], + let secret = Defaults[.aliAccessKeySecret], + !id.isEmpty, + !secret.isEmpty { requestByAPI(id: id, secret: secret, transType: transType, text: text, from: from, to: to, completion: completion) } else { // use web api diff --git a/Easydict/Feature/Service/Apple/EZAppleService.m b/Easydict/Feature/Service/Apple/EZAppleService.m index e2a4b567c..e2a681b15 100644 --- a/Easydict/Feature/Service/Apple/EZAppleService.m +++ b/Easydict/Feature/Service/Apple/EZAppleService.m @@ -16,6 +16,7 @@ #import #import "NSString+EZUtils.h" #import "EZAppleDictionary.h" +#import "Easydict-Swift.h" static NSString *const kLineBreakText = @"\n"; static NSString *const kParagraphBreakText = @"\n\n"; @@ -23,7 +24,7 @@ static NSArray *const kAllowedCharactersInPoetryList = @[ @"《", @"》", @"〔", @"〕" ]; -static CGFloat const kParagraphLineHeightRatio = 1.2; +static CGFloat const kParagraphLineHeightRatio = 1.5; static NSInteger const kShortPoetryCharacterCountOfLine = 12; @@ -607,7 +608,7 @@ - (NLLanguage)detectUnkownText:(NSString *)text { NLLanguage language = NLLanguageEnglish; // 729 if ([text isNumbers]) { - EZLanguage firstLanguage = EZConfiguration.shared.firstLanguage; + EZLanguage firstLanguage = Configuration.shared.firstLanguage; language = [self appleLanguageFromLanguageEnum:firstLanguage]; } @@ -1210,18 +1211,6 @@ - (void)setupOCRResult:(EZOCRResult *)ocrResult CGFloat deltaY = prevBoundingBox.origin.y - (boundingBox.origin.y + boundingBox.size.height); CGFloat deltaX = boundingBox.origin.x - (prevBoundingBox.origin.x + prevBoundingBox.size.width); - // Note that line spacing is inaccurate, sometimes it's too small 😢 - BOOL isNewParagraph = NO; - if (deltaY > 0) { - // averageLineSpacing may too small, so deltaY should be much larger than averageLineSpacing - BOOL isBigLineSpacing = [self isBigSpacingLineOfTextObservation:textObservation - prevTextObservation:prevTextObservation - greaterThanLineHeightRatio:kParagraphLineHeightRatio]; - if (isBigLineSpacing) { - isNewParagraph = YES; - } - } - // Note that sometimes the line frames will overlap a little, then deltaY will less then 0 BOOL isNewLine = NO; if (deltaY > 0) { @@ -1250,10 +1239,9 @@ - (void)setupOCRResult:(EZOCRResult *)ocrResult if (isNeedRemoveLastDashOfText) { mergedText = [mergedText substringToIndex:mergedText.length - 1].mutableCopy; } - } else if (isNewParagraph || isNewLine) { + } else if (isNewLine) { joinedString = [self joinedStringOfTextObservation:textObservation - prevTextObservation:prevTextObservation - isNewParagraph:isNewParagraph]; + prevTextObservation:prevTextObservation]; } else { joinedString = @" "; // if the same line, just join two texts } @@ -1488,9 +1476,10 @@ - (BOOL)isPoetryOftextObservations:(NSArray *)tex /// Get joined string of text, according to its last char. - (NSString *)joinedStringOfTextObservation:(VNRecognizedTextObservation *)textObservation prevTextObservation:(VNRecognizedTextObservation *)prevTextObservation - isNewParagraph:(BOOL)isNewParagraph { +{ NSString *joinedString = @""; BOOL needLineBreak = NO; + BOOL isNewParagraph = NO; CGRect prevBoundingBox = prevTextObservation.boundingBox; CGFloat prevLineLength = prevBoundingBox.size.width; @@ -1508,7 +1497,7 @@ - (NSString *)joinedStringOfTextObservation:(VNRecognizedTextObservation *)textO BOOL hasPrevIndentation = [self hasIndentationOfTextObservation:prevTextObservation]; BOOL hasIndentation = [self hasIndentationOfTextObservation:textObservation]; - + BOOL isPrevLongText = [self isLongTextObservation:prevTextObservation isStrict:NO]; BOOL isEqualChineseText = [self isEqualChineseTextObservation:textObservation prevTextObservation:prevTextObservation]; @@ -1894,6 +1883,20 @@ - (BOOL)hasIndentationOfTextObservation:(VNRecognizedTextObservation *)textObser return hasIndentation; } +- (BOOL)hasIndentationOfTextObservation:(VNRecognizedTextObservation *)textObservation + prevTextObservation:(VNRecognizedTextObservation *)prevTextObservation { + BOOL isEqualX = [self isEqualXOfTextObservation:textObservation prevTextObservation:prevTextObservation]; + + CGFloat lineX = CGRectGetMinX(textObservation.boundingBox); + CGFloat prevLineX = CGRectGetMinX(prevTextObservation.boundingBox); + CGFloat dx = lineX - prevLineX; + + if (!isEqualX && dx < 0) { + return YES; + } + return NO; +} + - (BOOL)isEqualTextObservation:(VNRecognizedTextObservation *)textObservation prevTextObservation:(VNRecognizedTextObservation *)prevTextObservation { // 0.06 - 0.025 diff --git a/Easydict/Feature/Service/Apple/EZScriptExecutor.h b/Easydict/Feature/Service/Apple/EZScriptExecutor.h index a0b6259df..8ad161b9f 100644 --- a/Easydict/Feature/Service/Apple/EZScriptExecutor.h +++ b/Easydict/Feature/Service/Apple/EZScriptExecutor.h @@ -17,17 +17,17 @@ typedef void(^AppleScriptCompletionHandler)( NSString *_Nullable result, EZError /// Run translate shortcut with parameters. - (NSTask *)runTranslateShortcut:(NSDictionary *)parameters - completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; + completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; /// Run shortcut with parameters. - (NSTask *)runShortcut:(NSString *)shortcutName parameters:(NSDictionary *)parameters - completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; + completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; /// Use NSTask to run AppleScript. -- (NSTask *)runAppleScriptWithTask:(NSString *)script completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; +- (NSTask *)runAppleScriptWithTask:(NSString *)script completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; -- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; +- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; @end diff --git a/Easydict/Feature/Service/Apple/EZScriptExecutor.m b/Easydict/Feature/Service/Apple/EZScriptExecutor.m index 51ff38453..702f0fb08 100644 --- a/Easydict/Feature/Service/Apple/EZScriptExecutor.m +++ b/Easydict/Feature/Service/Apple/EZScriptExecutor.m @@ -94,7 +94,7 @@ - (NSTask *)runAppleScriptWithTask:(NSString *)script completionHandler:(void (^ /// Use NSAppleScript to run AppleScript, faster than NSTask. /// !!!: Note that this method may fail due to execution permissions, it will not automatically apply for permissions when I test. -- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError *error))completionHandler { +- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler { NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script]; CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ diff --git a/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m b/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m index 7ef68a030..ce07b3d7a 100644 --- a/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m +++ b/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m @@ -17,6 +17,7 @@ #import "EZServiceTypes.h" #import "EZConfiguration.h" #import +#import "Easydict-Swift.h" static NSString *const kFileExtendedAttributes = @"NSFileExtendedAttributes"; @@ -128,7 +129,7 @@ - (void)setIsPlaying:(BOOL)playing { // Note that user may change it when using, so we need to read it every time. - (EZQueryService *)defaultTTSService { - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; if (![_defaultTTSService.serviceType isEqualToString:defaultTTSServiceType]) { EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; _defaultTTSService = defaultTTSService; diff --git a/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m b/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m index 5b6137076..726add2ad 100644 --- a/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m +++ b/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m @@ -13,6 +13,7 @@ #import "EZNetworkManager.h" #import "EZConfiguration.h" #import "NSString+EZRegex.h" +#import "Easydict-Swift.h" static NSString *const kBaiduTranslateURL = @"https://fanyi.baidu.com"; @@ -124,7 +125,8 @@ - (EZServiceType)serviceType { - (EZQueryTextType)queryTextType { EZQueryTextType defaultType = EZQueryTextTypeDictionary | EZQueryTextTypeSentence | EZQueryTextTypeTranslation; - EZQueryTextType type = [EZConfiguration.shared queryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared queryTextTypeForServiceType:self.serviceType]; + if (type == 0) { type = defaultType; } @@ -132,7 +134,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Bing/BingLanguageVoice.swift b/Easydict/Feature/Service/Bing/BingLanguageVoice.swift new file mode 100644 index 000000000..3751e9433 --- /dev/null +++ b/Easydict/Feature/Service/Bing/BingLanguageVoice.swift @@ -0,0 +1,26 @@ +// +// BingLanguageVoice.swift +// Easydict +// +// Created by Jerry on 2023-10-14. +// Copyright © 2023 izual. All rights reserved. +// + +import Foundation + +// Docs: https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/language-support?tabs=tts + +@objc(EZBingLanguageVoice) +class BingLanguageVoice: NSObject { + @objc var lang: String // BCP-47, en-US + @objc var voiceName: String // en-US-JennyNeural + + init(lang: String, voiceName: String) { + self.lang = lang + self.voiceName = voiceName + } + + @objc class func voice(withLanguage language: String, voiceName: String) -> BingLanguageVoice { + BingLanguageVoice(lang: language, voiceName: voiceName) + } +} diff --git a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.h b/Easydict/Feature/Service/Bing/EZBingLanguageVoice.h deleted file mode 100644 index 2a193cbbd..000000000 --- a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// EZBingTTSVoice.h -// Easydict -// -// Created by tisfeng on 2023/8/25. -// Copyright © 2023 izual. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -// Docs: https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/language-support?tabs=tts - -@interface EZBingLanguageVoice : NSObject - -@property (nonatomic, copy) NSString *lang; // BCP-47, en-US -@property (nonatomic, copy) NSString *voiceName; // en-US-JennyNeural - -+ (instancetype)voiceWithLanguage:(NSString *)langauge voiceName:(NSString *)voiceName; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.m b/Easydict/Feature/Service/Bing/EZBingLanguageVoice.m deleted file mode 100644 index 4ba73a88d..000000000 --- a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.m +++ /dev/null @@ -1,20 +0,0 @@ -// -// EZBingTTSVoice.m -// Easydict -// -// Created by tisfeng on 2023/8/25. -// Copyright © 2023 izual. All rights reserved. -// - -#import "EZBingLanguageVoice.h" - -@implementation EZBingLanguageVoice - -+ (instancetype)voiceWithLanguage:(NSString *)langauge voiceName:(NSString *)voiceName { - EZBingLanguageVoice *voice = [[EZBingLanguageVoice alloc] init]; - voice.lang = langauge; - voice.voiceName = voiceName; - return voice; -} - -@end diff --git a/Easydict/Feature/Service/Bing/EZBingRequest.m b/Easydict/Feature/Service/Bing/EZBingRequest.m index 3d6849b35..512e31b87 100644 --- a/Easydict/Feature/Service/Bing/EZBingRequest.m +++ b/Easydict/Feature/Service/Bing/EZBingRequest.m @@ -8,8 +8,8 @@ #import "EZBingRequest.h" #import "EZError.h" -#import "EZBingLanguageVoice.h" #import "NSString+EZRegex.h" +#import "Easydict-Swift.h" static NSString *const kAudioMIMEType = @"audio/mpeg"; static NSString *const kBingConfigKey = @"kBingConfigKey"; diff --git a/Easydict/Feature/Service/Bing/EZBingService.m b/Easydict/Feature/Service/Bing/EZBingService.m index db282c86b..2b08d0ed1 100644 --- a/Easydict/Feature/Service/Bing/EZBingService.m +++ b/Easydict/Feature/Service/Bing/EZBingService.m @@ -12,6 +12,7 @@ #import "EZBingLookupModel.h" #import "EZConfiguration.h" #import "NSString+EZUtils.h" +#import "Easydict-Swift.h" @interface EZBingService () @property (nonatomic, strong) EZBingRequest *request; @@ -32,7 +33,7 @@ - (instancetype)init { #pragma mark - override - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Caiyun/CaiyunService.swift b/Easydict/Feature/Service/Caiyun/CaiyunService.swift index 2c6b3dcf3..55a2d8b74 100644 --- a/Easydict/Feature/Service/Caiyun/CaiyunService.swift +++ b/Easydict/Feature/Service/Caiyun/CaiyunService.swift @@ -7,6 +7,7 @@ // import Alamofire +import Defaults import Foundation @objc(EZCaiyunService) @@ -37,6 +38,10 @@ public final class CaiyunService: QueryService { throw QueryServiceError.notSupported } + override public func hasPrivateAPIKey() -> Bool { + token != CaiyunService.defaultTestToken + } + private var apiEndPoint = "https://api.interpreter.caiyunai.com/v1/translator" /// Official Test Token for Caiyun @@ -44,7 +49,7 @@ public final class CaiyunService: QueryService { // easydict://writeKeyValue?EZCaiyunToken= private var token: String { - let token = UserDefaults.standard.string(forKey: EZCaiyunToken) + let token = Defaults[.caiyunToken] if let token, !token.isEmpty { return token } else { diff --git a/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m b/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m index 44007d5ad..576c5de8c 100644 --- a/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m +++ b/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m @@ -390,7 +390,6 @@ - (void)deepLTranslate:(NSString *)text from:(EZLanguage)from to:(EZLanguage)to } */ NSString *translatedText = [responseObject[@"translations"] firstObject][@"text"]; - translatedText = [translatedText.trim removeExtraLineBreaks]; NSArray *translatedTextArray = [translatedText toParagraphs]; return translatedTextArray; diff --git a/Easydict/Feature/Service/Gemini/GeminiService.swift b/Easydict/Feature/Service/Gemini/GeminiService.swift new file mode 100644 index 000000000..43331a553 --- /dev/null +++ b/Easydict/Feature/Service/Gemini/GeminiService.swift @@ -0,0 +1,145 @@ +// +// GeminiService.swift +// Easydict +// +// Created by Jerry on 2024-01-02. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation +import GoogleGenerativeAI + +@objc(EZGeminiService) +public final class GeminiService: QueryService { + override public func serviceType() -> ServiceType { + .gemini + } + + override public func link() -> String? { + "https://bard.google.com/chat" + } + + override public func name() -> String { + NSLocalizedString("gemini_translate", comment: "The name of Gemini Translate") + } + + // https://ai.google.dev/available_regions + private static let unsupportedLanguages: [Language] = [.persian, .filipino, .khmer, .lao, .malay, .mongolian, .burmese, .telugu, .tamil, .urdu] + + override public func supportLanguagesDictionary() -> MMOrderedDictionary { + // TODO: Replace MMOrderedDictionary. + let orderedDict = MMOrderedDictionary() + for language in EZLanguageManager.shared().allLanguages { + let value = language.rawValue + if !GeminiService.unsupportedLanguages.contains(language) { + orderedDict.setObject(value as NSString, forKey: language.rawValue as NSString) + } + } + + return orderedDict + } + + override public func ocr(_: EZQueryModel) async throws -> EZOCRResult { + NSLog("Gemini Translate does not support OCR") + throw QueryServiceError.notSupported + } + + override public func needPrivateAPIKey() -> Bool { + true + } + + override public func hasPrivateAPIKey() -> Bool { + if apiKey == defaultAPIKey { + return false + } + return true + } + + private let defaultAPIKey = "" /* .decryptAES() */ + + // easydict://writeKeyValue?EZGeminiAPIKey=xxx + private var apiKey: String { + let apiKey = Defaults[.geminiAPIKey] + if let apiKey, !apiKey.isEmpty { + return apiKey + } else { + return defaultAPIKey + } + } + + // Set Gemini safety level to BLOCK_NONE + private static let harassmentSafety = SafetySetting(harmCategory: .harassment, threshold: .blockNone) + private static let hateSpeechSafety = SafetySetting(harmCategory: .hateSpeech, threshold: .blockNone) + private static let sexuallyExplicitSafety = SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockNone) + private static let dangerousContentSafety = SafetySetting(harmCategory: .dangerousContent, threshold: .blockNone) + + private static let translationPrompt = "You are a translation expert proficient in various languages that can only translate text and cannot interpret it. You are able to accurately understand the meaning of proper nouns, idioms, metaphors, allusions or other obscure words in sentences and translate them into appropriate words by combining the context and language environment. The result of the translation should be natural and fluent, you can only return the translated text, do not show additional information and notes." + + override public func autoConvertTraditionalChinese() -> Bool { + true + } + + override public func translate(_ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> Void) { + Task { + do { + let prompt = GeminiService.translationPrompt + "Translate the following \(from.rawValue) text into \(to.rawValue): \(text)" + print("gemini prompt: \(prompt)") + let model = GenerativeModel( + name: "gemini-pro", + apiKey: apiKey, + safetySettings: [ + GeminiService.harassmentSafety, + GeminiService.hateSpeechSafety, + GeminiService.sexuallyExplicitSafety, + GeminiService.dangerousContentSafety, + ] + ) + + if #available(macOS 12.0, *) { + var resultString = "" + let outputContentStream = model.generateContentStream(prompt) + + // stream response + for try await outputContent in outputContentStream { + guard let line = outputContent.text else { + return + } + + resultString += line + result.translatedResults = [resultString] + await MainActor.run { + completion(result, nil) + } + } + + } else { + let outputContent = try await model.generateContent(prompt) + guard let resultString = outputContent.text else { + return + } + + result.translatedResults = [resultString] + await MainActor.run { + completion(result, nil) + } + } + } catch { + /** + https://github.com/google/generative-ai-swift/issues/89 + + String(describing: error) + + "internalError(underlying: GoogleGenerativeAI.RPCError(httpResponseCode: 400, message: \"API key not valid. Please pass a valid API key.\", status: GoogleGenerativeAI.RPCStatus.invalidArgument))" + */ + let ezError = EZError(nsError: error) + let errorString = String(describing: error) + let errorMessage = errorString.extract(withPattern: "message: \"([^\"]*)\"") ?? errorString + ezError?.errorDataMessage = errorMessage + await MainActor.run { + completion(result, ezError) + } + } + } + } +} diff --git a/Easydict/Feature/Service/Google/EZGoogleTranslate.m b/Easydict/Feature/Service/Google/EZGoogleTranslate.m index 5c65d0e22..f2af0a9ae 100644 --- a/Easydict/Feature/Service/Google/EZGoogleTranslate.m +++ b/Easydict/Feature/Service/Google/EZGoogleTranslate.m @@ -11,6 +11,7 @@ #import #import "NSString+EZUtils.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" static NSString *const kGoogleTranslateURL = @"https://translate.google.com"; @@ -115,7 +116,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Language/EZLanguageManager.m b/Easydict/Feature/Service/Language/EZLanguageManager.m index f2e430349..6650763d5 100644 --- a/Easydict/Feature/Service/Language/EZLanguageManager.m +++ b/Easydict/Feature/Service/Language/EZLanguageManager.m @@ -9,6 +9,7 @@ #import "EZLanguageManager.h" #import "EZAppleService.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZLanguageManager () @@ -170,7 +171,7 @@ - (EZLanguage)systemSecondLanguage { } - (EZLanguage)userFirstLanguage { - EZLanguage firstLanguage = EZConfiguration.shared.firstLanguage; + EZLanguage firstLanguage = Configuration.shared.firstLanguage; if (!firstLanguage) { firstLanguage = [self systemPreferredTwoLanguages][0]; } @@ -178,7 +179,7 @@ - (EZLanguage)userFirstLanguage { } - (EZLanguage)userSecondLanguage { - EZLanguage secondLanguage = EZConfiguration.shared.secondLanguage; + EZLanguage secondLanguage = Configuration.shared.secondLanguage; if (!secondLanguage) { secondLanguage = [self systemPreferredTwoLanguages][1]; } diff --git a/Easydict/Feature/Service/Model/EZConstKey.h b/Easydict/Feature/Service/Model/EZConstKey.h index be00eeea2..cf422670b 100644 --- a/Easydict/Feature/Service/Model/EZConstKey.h +++ b/Easydict/Feature/Service/Model/EZConstKey.h @@ -35,6 +35,7 @@ static NSString *const EZNiuTransAPIKey = @"EZNiuTransAPIKey"; static NSString *const EZCaiyunToken = @"EZCaiyunToken"; static NSString *const EZTencentSecretId = @"EZTencentSecretId"; static NSString *const EZTencentSecretKey = @"EZTencentSecretKey"; +static NSString *const EZGeminiAPIKey = @"EZGeminiAPIKey"; static NSString *const EZAliAccessKeyId = @"EZAliAccessKeyId"; static NSString *const EZAliAccessKeySecret = @"EZAliAccessKeySecret"; diff --git a/Easydict/Feature/Service/Model/EZDetectManager.m b/Easydict/Feature/Service/Model/EZDetectManager.m index 40d6c9bff..a4c5575ee 100644 --- a/Easydict/Feature/Service/Model/EZDetectManager.m +++ b/Easydict/Feature/Service/Model/EZDetectManager.m @@ -12,6 +12,7 @@ #import "EZConfiguration.h" #import "EZYoudaoTranslate.h" #import "EZConfiguration+EZUserData.h" +#import "Easydict-Swift.h" @interface EZDetectManager () @@ -101,7 +102,7 @@ - (void)detectText:(NSString *)queryText completion:(void (^)(EZQueryModel *_Non [self.appleService detectText:queryText completion:^(EZLanguage appleDetectdedLanguage, NSError *_Nullable error) { NSMutableArray *preferredLanguages = [[EZLanguageManager.shared preferredLanguages] mutableCopy]; - EZLanguageDetectOptimize languageDetectOptimize = EZConfiguration.shared.languageDetectOptimize; + LanguageDetectOptimize languageDetectOptimize = Configuration.shared.languageDetectOptimize; // Add English and Chinese to the preferred language list, in general, sysytem detect English and Chinese is relatively accurate, so we don't need to use google or baidu to detect again. [preferredLanguages addObjectsFromArray:@[ @@ -111,7 +112,7 @@ - (void)detectText:(NSString *)queryText completion:(void (^)(EZQueryModel *_Non ]]; BOOL isPreferredLanguage = [preferredLanguages containsObject:appleDetectdedLanguage]; - if (isPreferredLanguage || languageDetectOptimize == EZLanguageDetectOptimizeNone) { + if (isPreferredLanguage || languageDetectOptimize == LanguageDetectOptimizeNone) { [self handleDetectedLanguage:appleDetectdedLanguage error:error completion:completion]; return; } @@ -237,7 +238,7 @@ - (void)handleOCRResult:(EZOCRResult *_Nullable)ocrResult error:(NSError *_Nulla Sometimes Apple OCR may fail, like Japanese text, but we have set Japanese as preferred language and OCR again when OCR result is empty, currently it seems work, but we do not guarantee it is always work in other languages. */ - if ([EZConfiguration.shared isBeta]) { + if (Configuration.shared.beta) { [self.youdaoService ocr:self.queryModel completion:^(EZOCRResult *_Nullable youdaoOCRResult, NSError *_Nullable youdaoOCRError) { if (!youdaoOCRError) { completion(youdaoOCRResult, nil); diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.h b/Easydict/Feature/Service/Model/EZEnumTypes.h index 28dbeea3a..cad41e525 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.h +++ b/Easydict/Feature/Service/Model/EZEnumTypes.h @@ -43,6 +43,7 @@ FOUNDATION_EXPORT EZServiceType const EZServiceTypeNiuTrans; FOUNDATION_EXPORT EZServiceType const EZServiceTypeCaiyun; FOUNDATION_EXPORT EZServiceType const EZServiceTypeTencent; FOUNDATION_EXPORT EZServiceType const EZServiceTypeAli; +FOUNDATION_EXPORT EZServiceType const EZServiceTypeGemini; FOUNDATION_EXPORT NSString *const EZQueryTextTypeKey; FOUNDATION_EXPORT NSString *const EZIntelligentQueryTextTypeKey; diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.m b/Easydict/Feature/Service/Model/EZEnumTypes.m index 76e778da2..879b9102b 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.m +++ b/Easydict/Feature/Service/Model/EZEnumTypes.m @@ -22,6 +22,7 @@ NSString *const EZServiceTypeCaiyun = @"Caiyun"; NSString *const EZServiceTypeTencent = @"Tencent"; NSString *const EZServiceTypeAli = @"Alibaba"; +NSString *const EZServiceTypeGemini = @"Gemini"; NSString *const EZServiceTypeAppleDictionary = @"AppleDictionary"; diff --git a/Easydict/Feature/Service/Model/EZQueryService.h b/Easydict/Feature/Service/Model/EZQueryService.h index 8d5e1746e..f2616bfe4 100644 --- a/Easydict/Feature/Service/Model/EZQueryService.h +++ b/Easydict/Feature/Service/Model/EZQueryService.h @@ -61,6 +61,8 @@ NS_SWIFT_NAME(QueryService) /// Get TTS langauge code. - (NSString *)getTTSLanguageCode:(EZLanguage)language; +- (EZQueryResult *)resetServiceResult; + - (void)startQuery:(EZQueryModel *)queryModel completion:(void (^)(EZQueryResult *result, NSError *_Nullable error))completion; @end diff --git a/Easydict/Feature/Service/Model/EZQueryService.m b/Easydict/Feature/Service/Model/EZQueryService.m index b7cc43e1c..2a1646fea 100644 --- a/Easydict/Feature/Service/Model/EZQueryService.m +++ b/Easydict/Feature/Service/Model/EZQueryService.m @@ -12,6 +12,8 @@ #import "NSString+EZChineseText.h" #import "NSString+EZUtils.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" +#import "EZEventMonitor.h" #define MethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ @@ -54,7 +56,7 @@ - (BOOL)enabledAutoQuery { return NO; } - if ([EZConfiguration.shared intelligentQueryModeForWindowType:self.windowType]) { + if ([Configuration.shared intelligentQueryModeForWindowType:self.windowType]) { // We usually don't want to lookup dictionary if text word > 1. EZQueryTextType queryType = [self.queryModel.queryText queryTypeWithLanguage:self.queryModel.queryFromLanguage maxWordCount:1]; if ((queryType & self.intelligentQueryTextType) != queryType) { @@ -83,6 +85,28 @@ - (void)setResult:(EZQueryResult *)translateResult { _result.queryText = self.queryModel.queryText; } +- (EZQueryResult *)resetServiceResult { + EZQueryResult *result = self.result; + [result reset]; + if (!result) { + result = [[EZQueryResult alloc] init]; + } + + NSArray *enabledReplaceTypes = @[ + EZActionTypeAutoSelectQuery, + EZActionTypeShortcutQuery, + EZActionTypeInvokeQuery, + ]; + if ([enabledReplaceTypes containsObject:self.queryModel.actionType]) { + result.showReplaceButton = EZEventMonitor.shared.isSelectedTextEditable; + } else { + result.showReplaceButton = NO; + } + + self.result = result; + return result; +} + - (MMOrderedDictionary *)langDict { if (!_langDict) { _langDict = [self supportLanguagesDictionary]; diff --git a/Easydict/Feature/Service/Model/EZServiceTypes.m b/Easydict/Feature/Service/Model/EZServiceTypes.m index d63ed6649..c40d7ceba 100644 --- a/Easydict/Feature/Service/Model/EZServiceTypes.m +++ b/Easydict/Feature/Service/Model/EZServiceTypes.m @@ -63,6 +63,7 @@ + (instancetype)allocWithZone:(struct _NSZone *)zone { EZServiceTypeCaiyun, [EZCaiyunService class], EZServiceTypeTencent, [EZTencentService class], EZServiceTypeAli, [EZAliService class], + EZServiceTypeGemini, [EZGeminiService class], nil]; return allServiceDict; } diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m b/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m index 2bfa712d6..2b32e8d74 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m +++ b/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m @@ -9,6 +9,7 @@ #import "EZOpenAIService+EZPromptMessages.h" #import "EZConfiguration.h" #import "NSString+EZUtils.h" +#import "Easydict-Swift.h" // You are a faithful translation assistant that can only translate text and cannot interpret it, you can only return the translated text, do not show additional descriptions and annotations. @@ -84,7 +85,7 @@ - (NSArray *)translatioMessages:(NSString *)text from:(EZLanguage)sourceLanguage /// Sentence messages. - (NSArray *)sentenceMessages:(NSString *)sentence from:(EZLanguage)sourceLanguage to:(EZLanguage)targetLanguage { - NSString *answerLanguage = EZConfiguration.shared.firstLanguage; + NSString *answerLanguage = Configuration.shared.firstLanguage; self.result.to = answerLanguage; NSString *prompt = @""; @@ -274,7 +275,7 @@ - (NSArray *)translatioMessages:(NSString *)text from:(EZLanguage)sourceLanguage // V5. prompt NSString *prompt = @""; - NSString *answerLanguage = EZConfiguration.shared.firstLanguage; + NSString *answerLanguage = Configuration.shared.firstLanguage; self.result.to = answerLanguage; NSString *pronunciation = @"Pronunciation"; diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m b/Easydict/Feature/Service/OpenAI/EZOpenAIService.m index ad01eff55..f97a0ea71 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m +++ b/Easydict/Feature/Service/OpenAI/EZOpenAIService.m @@ -28,6 +28,7 @@ @interface EZOpenAIService () @end + @implementation EZOpenAIService - (instancetype)init { @@ -56,7 +57,7 @@ - (NSString *)apiKey { // easydict://writeKeyValue?EZOpenAIAPIKey= NSString *apiKey = [[NSUserDefaults standardUserDefaults] stringForKey:EZOpenAIAPIKey] ?: @""; - if (apiKey.length == 0 && EZConfiguration.shared.isBeta) { + if (apiKey.length == 0 && Configuration.shared.beta) { apiKey = self.defaultAPIKey; } return apiKey; @@ -134,7 +135,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Tencent/TencentService.swift b/Easydict/Feature/Service/Tencent/TencentService.swift index cc75b7db0..aaa07b9ab 100644 --- a/Easydict/Feature/Service/Tencent/TencentService.swift +++ b/Easydict/Feature/Service/Tencent/TencentService.swift @@ -7,6 +7,7 @@ // import Alamofire +import Defaults import Foundation @objc(EZTencentService) @@ -64,7 +65,7 @@ public final class TencentService: QueryService { // easydict://writeKeyValue?EZTencentSecretId=xxx private var secretId: String { - let secretId = UserDefaults.standard.string(forKey: EZTencentSecretId) + let secretId = Defaults[.tencentSecretId] if let secretId, !secretId.isEmpty { return secretId } else { @@ -74,7 +75,7 @@ public final class TencentService: QueryService { // easydict://writeKeyValue?EZTencentSecretKey=xxx private var secretKey: String { - let secretKey = UserDefaults.standard.string(forKey: EZTencentSecretKey) + let secretKey = Defaults[.tencentSecretKey] if let secretKey, !secretKey.isEmpty { return secretKey } else { diff --git a/Easydict/Feature/Service/Tencent/TencentTranslateType.swift b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift index e82753154..ca8bcd91b 100644 --- a/Easydict/Feature/Service/Tencent/TencentTranslateType.swift +++ b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift @@ -14,24 +14,24 @@ struct TencentTranslateType: Equatable { static let unsupported = TencentTranslateType(sourceLanguage: "unsupported", targetLanguage: "unsupported") - // This docs missed traditionalChinese as target language if target languages contains simplifiedChinese. https://cloud.tencent.com/document/api/551/15619 + // https://cloud.tencent.com/document/api/551/15619 static let supportedTypes: [Language: [Language]] = [ .simplifiedChinese: [.english, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay], .traditionalChinese: [.english, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay], - .english: [.simplifiedChinese, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay, .arabic, .hindi], - .japanese: [.simplifiedChinese, .english, .korean], - .korean: [.simplifiedChinese, .english, .japanese], - .french: [.simplifiedChinese, .english, .spanish, .italian, .german, .turkish, .russian, .portuguese], - .spanish: [.simplifiedChinese, .english, .french, .italian, .german, .turkish, .russian, .portuguese], - .italian: [.simplifiedChinese, .english, .french, .spanish, .german, .turkish, .russian, .portuguese], - .german: [.simplifiedChinese, .english, .french, .spanish, .italian, .turkish, .russian, .portuguese], - .turkish: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .russian, .portuguese], - .russian: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .turkish, .portuguese], - .portuguese: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .turkish, .russian], - .vietnamese: [.simplifiedChinese, .english], - .indonesian: [.simplifiedChinese, .english], - .thai: [.simplifiedChinese, .english], - .malay: [.simplifiedChinese, .english], + .english: [.simplifiedChinese, .traditionalChinese, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay, .arabic, .hindi], + .japanese: [.simplifiedChinese, .traditionalChinese, .english, .korean], + .korean: [.simplifiedChinese, .traditionalChinese, .english, .japanese], + .french: [.simplifiedChinese, .traditionalChinese, .english, .spanish, .italian, .german, .turkish, .russian, .portuguese], + .spanish: [.simplifiedChinese, .traditionalChinese, .english, .french, .italian, .german, .turkish, .russian, .portuguese], + .italian: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .german, .turkish, .russian, .portuguese], + .german: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .turkish, .russian, .portuguese], + .turkish: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .german, .russian, .portuguese], + .russian: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .german, .turkish, .portuguese], + .portuguese: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .german, .turkish, .russian], + .vietnamese: [.simplifiedChinese, .traditionalChinese, .english], + .indonesian: [.simplifiedChinese, .traditionalChinese, .english], + .thai: [.simplifiedChinese, .traditionalChinese, .english], + .malay: [.simplifiedChinese, .traditionalChinese, .english], .arabic: [.english], .hindi: [.english], ] @@ -59,9 +59,16 @@ struct TencentTranslateType: Equatable { ] static func transType(from: Language, to: Language) -> TencentTranslateType { - // !!!: Tencent translate support traditionalChinese as target language if target languages contain simplifiedChinese. + /** + 1. zh <--> zh-TW + 2. zh --> zh + + Tencent Translate supports Simplified Chinese and Traditional Chinese translations of each other, but the documentation doesn't mention this, so we need to handle it ourselves. + + In addition, it also supports one language as both source and target language if the language is supported. + */ guard let targetLanguages = supportedTypes[from], - targetLanguages.containsChinese() || targetLanguages.contains(to) || from == to || from.isKindOfChinese() + targetLanguages.contains(to) || from == to || EZLanguageManager.shared().onlyContainsChineseLanguages([from, to]) else { return .unsupported } @@ -75,17 +82,3 @@ struct TencentTranslateType: Equatable { return TencentTranslateType(sourceLanguage: fromLanguage, targetLanguage: toLanguage) } } - -extension [Language] { - /// Contains Chinese language, - func containsChinese() -> Bool { - contains { $0.isKindOfChinese() } - } -} - -extension Language { - /// Is kind of Chinese language, means it is simplifiedChinese or traditionalChinese. - func isKindOfChinese() -> Bool { - self == .simplifiedChinese || self == .traditionalChinese - } -} diff --git a/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m b/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m index 0df3d35b1..3c9f476b4 100644 --- a/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m +++ b/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m @@ -174,7 +174,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/StatusItem/EZMenuItemManager.m b/Easydict/Feature/StatusItem/EZMenuItemManager.m index ca784ad32..81497fd5e 100644 --- a/Easydict/Feature/StatusItem/EZMenuItemManager.m +++ b/Easydict/Feature/StatusItem/EZMenuItemManager.m @@ -15,9 +15,8 @@ #import "EZRightClickDetector.h" #import "EZConfiguration.h" #import "Easydict-Swift.h" - -static CGFloat const kImageMenuItemHeightRatio = 1.4; -static CGFloat const kTitleMenuItemHeightRatio = 1.2; +#import +#import @interface EZMenuItemManager () @@ -73,7 +72,7 @@ - (void)setup { if (self.statusItem) { return; } - if (EZConfiguration.shared.hideMenuBarIcon) { + if (Configuration.shared.hideMenuBarIcon) { return; } @@ -84,9 +83,9 @@ - (void)setup { NSStatusBarButton *button = statusItem.button; #if DEBUG - NSImage *image = [NSImage imageNamed:@"status_icon_debug"]; + NSImage *image = [NSImage imageNamed:@"rounded_menu_bar_icon"]; #else - NSImage *image = [NSImage imageNamed:@"status_icon"]; + NSImage *image = [NSImage imageNamed:@"square_menu_bar_icon"]; #endif [button setImage:image]; @@ -96,10 +95,7 @@ - (void)setup { self.appVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; self.versionItem.title = self.versionTitle; - - NSArray *items = @[self.versionItem, self.settingsItem, self.checkForUpdateItem, self.helpItem, self.quitItem]; - [self increaseMenuItemsHeight:items lineHeightRatio:kTitleMenuItemHeightRatio]; - + [self updateVersionItem]; } @@ -187,6 +183,11 @@ - (IBAction)settingAction:(NSMenuItem *)sender { [EZPreferencesWindowController.shared show]; } +- (IBAction)checkForUpdateItem:(id)sender { + NSLog(@"checkForUpdate"); + [EZConfiguration.shared.updater checkForUpdates]; +} + - (IBAction)feedbackAction:(NSMenuItem *)sender { NSLog(@"反馈问题"); NSString *issueURL = [NSString stringWithFormat:@"%@/issues", EZGithubRepoEasydictURL]; @@ -279,12 +280,12 @@ - (IBAction)appleDictionaryAction:(NSMenuItem *)sender { } - (IBAction)increaseFontSizeAction:(NSMenuItem *)sender { - EZConfiguration.shared.fontSizeIndex += 1; + Configuration.shared.fontSizeIndex += 1; } - (IBAction)decreaseFontSizeAction:(NSMenuItem *)sender { - EZConfiguration.shared.fontSizeIndex -= 1; + Configuration.shared.fontSizeIndex -= 1; } @@ -308,8 +309,6 @@ - (void)menuWillOpen:(NSMenu *)menu { item.keyEquivalent = @""; item.keyEquivalentModifierMask = 0; } - - [self increaseMenuItemHeight:item lineHeightRatio:kImageMenuItemHeightRatio]; }; configItemShortcut(self.selectionItem, EZSelectionShortcutKey); @@ -319,24 +318,6 @@ - (void)menuWillOpen:(NSMenu *)menu { configItemShortcut(self.screenshotOCRItem, EZScreenshotOCRShortcutKey); } -#pragma mark - - -/// Increase menu item height. Ref: https://stackoverflow.com/questions/18031666/nsmenuitem-height -- (void)increaseMenuItemHeight:(NSMenuItem *)item lineHeightRatio:(CGFloat)lineHeightRatio { - NSFont *font = [NSFont systemFontOfSize:[NSFont systemFontSize]]; - CGFloat fontLineHeight = (font.ascender + fabs(font.descender)); - CGFloat lineHeight = fontLineHeight * lineHeightRatio; - // Ref stackoverflow: https://stackoverflow.com/a/18034142/8378840 - NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(1, lineHeight)]; - [item setImage:image]; -} - -- (void)increaseMenuItemsHeight:(NSArray *)itmes lineHeightRatio:(CGFloat)lineHeightRatio { - for (NSMenuItem *item in itmes) { - [self increaseMenuItemHeight:item lineHeightRatio:lineHeightRatio]; - } -} - #pragma mark - Fetch Github Repo Info - (void)updateVersionItem { @@ -347,7 +328,6 @@ - (void)updateVersionItem { versionTitle = [NSString stringWithFormat:@"%@ (✨ %@)", self.versionTitle, lastestVersion]; } self.versionItem.title = versionTitle; - [self increaseMenuItemHeight:self.versionItem lineHeightRatio:kTitleMenuItemHeightRatio]; }]; } diff --git a/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m b/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m index a32fb0dda..6cf871203 100644 --- a/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m +++ b/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m @@ -8,6 +8,7 @@ #import "EZAppleScriptManager.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZAppleScriptManager () @@ -225,7 +226,7 @@ - (void)checkApplicationSupportCopyAction:(NSString *)appBundleID completion:(vo NSString *edit; if (!appLanguage) { - appLanguage = EZConfiguration.shared.firstLanguage; + appLanguage = Configuration.shared.firstLanguage; } if ([appLanguage isEqualToString:EZLanguageEnglish]) { diff --git a/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m b/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m index c18ed7160..59ed2348d 100644 --- a/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m +++ b/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m @@ -34,7 +34,7 @@ + (float)getSystemVolume { AudioObjectPropertyAddress address = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster}; + kAudioObjectPropertyElementMain}; if (!AudioObjectHasProperty(outputDeviceID, &address)) { NSLog(@"No volume returned for device 0x%0x", outputDeviceID); @@ -72,7 +72,7 @@ + (void)setSystemVolume:(float)volume { AudioObjectPropertyAddress address = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster}; + kAudioObjectPropertyElementMain}; if (!AudioObjectHasProperty(outputDeviceID, &address)) { NSLog(@"No volume returned for device 0x%0x", outputDeviceID); @@ -92,7 +92,7 @@ + (AudioDeviceID)getDefaultOutputDeviceID { AudioObjectPropertyAddress address = { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster}; + kAudioObjectPropertyElementMain}; UInt32 dataSize = sizeof(AudioDeviceID); OSStatus status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &dataSize, &outputDeviceID); diff --git a/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m b/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m index df765c4c5..5a119c5df 100644 --- a/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m +++ b/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m @@ -515,8 +515,14 @@ - (BOOL)isStartAndEndWith:(NSString *)start end:(NSString *)end { } - (NSString *)removeStartAndEndWith:(NSString *)start end:(NSString *)end { - if ([self isStartAndEndWith:start end:end]) { - return [self substringWithRange:NSMakeRange(start.length, self.length - start.length - end.length)]; + /** + Fix crash + + SIGABRT: -[NSTaggedPointerString substringWithRange:]: Range {2, 18446744073709551614} out of bounds; string length 2 + */ + NSInteger substringLength = self.length - start.length - end.length; + if ([self isStartAndEndWith:start end:end] && substringLength > 0) { + return [self substringWithRange:NSMakeRange(start.length, substringLength)]; } return self; } diff --git a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h index de32711b0..9890d7b89 100644 --- a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h +++ b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @interface NSViewController (EZWindow) -- (NSWindow *)window; +- (nullable NSWindow *)window; @end diff --git a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m index a73e9e2aa..a46341981 100644 --- a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m +++ b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m @@ -10,7 +10,7 @@ @implementation NSViewController (EZWindow) -- (NSWindow *)window { +- (nullable NSWindow *)window { NSResponder *responder = self; while ((responder = [responder nextResponder])) { if ([responder isKindOfClass:[NSWindow class]]) { diff --git a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m index cd8ba1630..bdefc8e56 100644 --- a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m +++ b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m @@ -14,6 +14,7 @@ #import "EZConfiguration+EZUserData.h" #import "EZConfiguration.h" #import "EZLocalStorage.h" +#import "Easydict-Swift.h" @implementation EZSchemeParser @@ -110,14 +111,14 @@ - (BOOL)writeKeyValues:(NSDictionary *)keyValues { NSString *value = keyValues[key]; handled = [self enabledReadWriteKey:key]; if (handled) { - EZConfiguration *config = [EZConfiguration shared]; - BOOL isBeta = config.isBeta; + Configuration *config = [Configuration shared]; + BOOL isBeta = config.beta; [[NSUserDefaults standardUserDefaults] setObject:value forKey:key]; // If enabling beta feature, setup beta features. - if (!isBeta && config.isBeta) { - [EZConfiguration.shared enableBetaFeaturesIfNeeded]; + if (!isBeta && config.beta) { + [Configuration.shared enableBetaFeaturesIfNeeded]; } } } @@ -154,15 +155,15 @@ - (BOOL)enabledReadWriteKey:(NSString *)key { - (void)resetUserDefaultsData { // easydict://resetUserDefaultsData - [EZConfiguration.shared resetUserDefaultsData]; + [Configuration.shared resetUserDefaultsData]; [EZLocalStorage destroySharedInstance]; - [EZConfiguration destroySharedInstance]; + [Configuration destroySharedInstance]; } - (void)saveUserDefaultsDataToDownloadFolder { // easydict://saveUserDefaultsDataToDownloadFolder - [EZConfiguration.shared saveUserDefaultsDataToDownloadFolder]; + [Configuration.shared saveUserDefaultsDataToDownloadFolder]; } @@ -219,6 +220,7 @@ - (NSArray *)allowedReadWriteKeys { EZAliAccessKeyId, EZAliAccessKeySecret, + EZGeminiAPIKey, EZIntelligentQueryModeKey, ]; diff --git a/Easydict/Feature/Utility/EZLog/EZLog.m b/Easydict/Feature/Utility/EZLog/EZLog.m index 81ba2860b..c17f1e7b9 100644 --- a/Easydict/Feature/Utility/EZLog/EZLog.m +++ b/Easydict/Feature/Utility/EZLog/EZLog.m @@ -47,7 +47,7 @@ + (void)setCrashEnabled:(BOOL)enabled { + (void)logEventWithName:(NSString *)name parameters:(nullable NSDictionary *)dict { // NSLog(@"log event: %@, %@", name, dict); - if (![EZConfiguration.shared allowAnalytics]) { + if (![Configuration.shared allowAnalytics]) { return; } diff --git a/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift new file mode 100644 index 000000000..960afabdd --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift @@ -0,0 +1,30 @@ +// +// Binding+DidSet.swift +// Easydict +// +// Created by phlpsong on 2024/1/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +// Ref https://stackoverflow.com/a/62871938 +// Toggle onChange not trigger issue +extension Binding { + func didSet(execute: @escaping (Value) -> Void) -> Binding { + Binding( + get: { self.wrappedValue }, + set: { + self.wrappedValue = $0 + execute($0) + } + ) + } +} + +func ?? (lhs: Binding, rhs: T) -> Binding { + Binding( + get: { lhs.wrappedValue ?? rhs }, + set: { lhs.wrappedValue = $0 } + ) +} diff --git a/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift new file mode 100644 index 000000000..c095084cb --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift @@ -0,0 +1,23 @@ +// +// Bundle+AppInfo.swift +// Easydict +// +// Created by phlpsong on 2024/1/18. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Bundle { + var applicationName: String { + if let displayName: String = object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { + return displayName + } else if let name: String = object(forInfoDictionaryKey: "CFBundleName") as? String { + return name + } + if let executableURL { + return executableURL.deletingLastPathComponent().lastPathComponent + } + return "" + } +} diff --git a/Easydict/Feature/Utility/Swift/Notification/Notification+Name.swift b/Easydict/Feature/Utility/Swift/Notification/Notification+Name.swift new file mode 100644 index 000000000..71c773f81 --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Notification/Notification+Name.swift @@ -0,0 +1,17 @@ +// +// Notification+Name.swift +// Easydict +// +// Created by phlpsong on 2024/1/7. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Notification.Name { + static let serviceHasUpdated = Notification.Name(EZServiceHasUpdatedNotification) +} + +@objc public extension NSNotification { + static let serviceHasUpdated = Notification.Name.serviceHasUpdated +} diff --git a/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m b/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m index bc993c1df..1be65ee65 100644 --- a/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m +++ b/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m @@ -11,6 +11,7 @@ #import "EZConfiguration.h" #import "NSColor+MyColors.h" #import "EZHoverButton.h" +#import "Easydict-Swift.h" @interface EZSelectLanguageCell () @@ -86,8 +87,8 @@ - (void)setup { mm_strongify(self); self.queryModel.userSourceLanguage = selectedLanguage; - if (![selectedLanguage isEqualToString:EZConfiguration.shared.from]) { - EZConfiguration.shared.from = selectedLanguage; + if (![selectedLanguage isEqualToString:Configuration.shared.fromLanguage]) { + Configuration.shared.fromLanguage = selectedLanguage; [self enterAction]; } }]; @@ -103,8 +104,8 @@ - (void)setup { mm_strongify(self); self.queryModel.userTargetLanguage = selectedLanguage; - if (![selectedLanguage isEqualToString:EZConfiguration.shared.to]) { - EZConfiguration.shared.to = selectedLanguage; + if (![selectedLanguage isEqualToString:Configuration.shared.toLanguage]) { + Configuration.shared.toLanguage = selectedLanguage; [self enterAction]; } }]; @@ -164,8 +165,8 @@ - (void)toggleTranslationLanguages { EZLanguage toLang = self.queryModel.userTargetLanguage; if (![fromLang isEqualToString:toLang]) { - EZConfiguration.shared.from = toLang; - EZConfiguration.shared.to = fromLang; + Configuration.shared.fromLanguage = toLang; + Configuration.shared.toLanguage = fromLang; [self.fromLanguageButton setSelectedLanguage:toLang]; [self.toLanguageButton setSelectedLanguage:fromLang]; @@ -181,7 +182,7 @@ - (void)enterAction { [self setNeedsUpdateConstraints:YES]; if (self.enterActionBlock) { - self.enterActionBlock(EZConfiguration.shared.from, EZConfiguration.shared.to); + self.enterActionBlock(Configuration.shared.fromLanguage, Configuration.shared.toLanguage); } } diff --git a/Easydict/Feature/ViewController/Model/EZQueryModel.m b/Easydict/Feature/ViewController/Model/EZQueryModel.m index c398ffd87..810385940 100644 --- a/Easydict/Feature/ViewController/Model/EZQueryModel.m +++ b/Easydict/Feature/ViewController/Model/EZQueryModel.m @@ -13,6 +13,7 @@ #import "NSString+EZSplit.h" #import "EZAppleDictionary.h" #import "NSString+EZHandleInputText.h" +#import "Easydict-Swift.h" @interface EZQueryModel () @@ -29,10 +30,10 @@ @implementation EZQueryModel - (instancetype)init { if (self = [super init]) { - [self.KVOController observe:EZConfiguration.shared keyPath:@"from" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, EZConfiguration *config, NSDictionary *_Nonnull change) { + [self.KVOController observe:Configuration.shared keyPath:@"fromLanguage" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, Configuration *config, NSDictionary *_Nonnull change) { queryModel.userSourceLanguage = change[NSKeyValueChangeNewKey]; }]; - [self.KVOController observe:EZConfiguration.shared keyPath:@"to" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, EZConfiguration *config, NSDictionary *_Nonnull change) { + [self.KVOController observe:Configuration.shared keyPath:@"toLanguage" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, Configuration *config, NSDictionary *_Nonnull change) { queryModel.userTargetLanguage = change[NSKeyValueChangeNewKey]; }]; @@ -198,7 +199,7 @@ - (NSString *)handleInputText:(NSString *)inputText { } } - if (EZConfiguration.shared.isBeta) { + if (Configuration.shared.beta) { // Remove prefix [//,#,*,] and join texts. queryText = [queryText removeCommentBlockSymbols]; } diff --git a/Easydict/Feature/ViewController/Storage/EZLocalStorage.m b/Easydict/Feature/ViewController/Storage/EZLocalStorage.m index 44b9735e3..92d3f1663 100644 --- a/Easydict/Feature/ViewController/Storage/EZLocalStorage.m +++ b/Easydict/Feature/ViewController/Storage/EZLocalStorage.m @@ -62,12 +62,26 @@ - (void)setup { EZWindowType windowType = [number integerValue]; for (EZServiceType serviceType in allServiceTypes) { EZServiceInfo *serviceInfo = [self serviceInfoWithType:serviceType windowType:windowType]; + + // New service. if (!serviceInfo) { serviceInfo = [[EZServiceInfo alloc] init]; serviceInfo.type = serviceType; - serviceInfo.enabled = NO; // disable new service + serviceInfo.enabled = YES; + serviceInfo.enabledQuery = YES; - // Mini type should keep concise, services <= 4 + /** + Fix https://github.com/tisfeng/Easydict/issues/269 and https://github.com/tisfeng/Easydict/issues/372 + + If there is a new service, we enable it but disable auto query, and add it to the end of the array. + + If it is the user's first time, auto query should be all allowed. + */ + if (self.queryCount > 0) { + serviceInfo.enabledQuery = NO; + } + + // Mini window should keep concise, so default enabled services should <= 4 if (windowType == EZWindowTypeMini) { NSArray *defaultEnabledServices = @[ EZServiceTypeAppleDictionary, @@ -78,10 +92,6 @@ - (void)setup { serviceInfo.enabled = [defaultEnabledServices containsObject:serviceType]; } - // There is a very small probability that Volcano webView translator will crash. - if (serviceType != EZServiceTypeVolcano) { - serviceInfo.enabledQuery = YES; - } [self setServiceInfo:serviceInfo windowType:windowType]; } } @@ -100,10 +110,6 @@ - (void)setup { NSMutableArray *array = [NSMutableArray arrayWithArray:allStoredServiceTypes]; if (![allStoredServiceTypes isEqualToArray:allServiceTypes]) { for (EZServiceType type in allServiceTypes) { - /** - If there is a new service, add it to the end of the array. - Fix https://github.com/tisfeng/Easydict/issues/269 - */ if ([allStoredServiceTypes indexOfObject:type] == NSNotFound) { [array addObject:type]; } diff --git a/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m b/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m index 6a88cde89..ab46a8a8b 100644 --- a/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m +++ b/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m @@ -12,6 +12,7 @@ #import "EZCoordinateUtils.h" #import "EZLog.h" #import "NSString+EZUtils.h" +#import "Easydict-Swift.h" @interface EZQueryMenuTextView () @@ -54,10 +55,10 @@ - (void)queryInApp:(id)sender { EZWindowManager *windowManager = [EZWindowManager shared]; EZWindowType floatingWindowType = windowManager.floatingWindowType; - if (EZConfiguration.shared.mouseSelectTranslateWindowType == floatingWindowType) { - anotherWindowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + if (Configuration.shared.mouseSelectTranslateWindowType == floatingWindowType) { + anotherWindowType = Configuration.shared.shortcutSelectTranslateWindowType; } else { - anotherWindowType = EZConfiguration.shared.mouseSelectTranslateWindowType; + anotherWindowType = Configuration.shared.mouseSelectTranslateWindowType; } if (anotherWindowType != floatingWindowType) { diff --git a/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m b/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m index 964cd6483..a99a4d73a 100644 --- a/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m +++ b/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m @@ -74,7 +74,7 @@ - (void)setup { textView.delegate = self; textView.textStorage.delegate = self; textView.textContainerInset = CGSizeMake(6, 8); - textView.font = [NSFont systemFontOfSize:14 * EZConfiguration.shared.fontSizeRatio]; + textView.font = [NSFont systemFontOfSize:14 * Configuration.shared.fontSizeRatio]; mm_weakify(self); [textView setPasteTextBlock:^(NSString *_Nonnull text) { @@ -87,7 +87,7 @@ - (void)setup { [[NSNotificationCenter defaultCenter] addObserverForName:ChangeFontSizeView.changeFontSizeNotificationName object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { mm_strongify(self); - self.textView.font = [NSFont systemFontOfSize:14 * EZConfiguration.shared.fontSizeRatio]; + self.textView.font = [NSFont systemFontOfSize:14 * Configuration.shared.fontSizeRatio]; }]; // When programatically setting the text, like auto select text, or OCR text. diff --git a/Easydict/Feature/ViewController/View/TextView/EZTextView.m b/Easydict/Feature/ViewController/View/TextView/EZTextView.m index ba8edbc24..fcc3dc36e 100644 --- a/Easydict/Feature/ViewController/View/TextView/EZTextView.m +++ b/Easydict/Feature/ViewController/View/TextView/EZTextView.m @@ -246,6 +246,11 @@ - (void)updatePlaceholderVisibility { } - (void)setString:(NSString *)string { + // Fix: SIGABRT: *** -[EZTextView replaceCharactersInRange:withString:]: nil NSString given. + if (!string) { + string = @""; + } + [super setString:string]; [self updatePlaceholderVisibility]; } diff --git a/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m b/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m index e6cb2d6e7..2b2c364aa 100644 --- a/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m +++ b/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m @@ -13,6 +13,7 @@ #import "NSObject+EZDarkMode.h" #import "EZBaseQueryWindow.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZTitlebar () @@ -92,7 +93,7 @@ - (void)updateConstraints { // TODO: We should refactor it later. // Google - if (EZConfiguration.shared.showGoogleQuickLink) { + if (Configuration.shared.showGoogleQuickLink) { EZOpenLinkButton *googleButton = [[EZOpenLinkButton alloc] init]; [self addSubview:googleButton]; self.googleButton = googleButton; @@ -116,7 +117,7 @@ - (void)updateConstraints { } // Apple Dictionary - if (EZConfiguration.shared.showAppleDictionaryQuickLink) { + if (Configuration.shared.showAppleDictionaryQuickLink) { EZOpenLinkButton *appleDictButton = [[EZOpenLinkButton alloc] init]; [self addSubview:appleDictButton]; self.appleDictionaryButton = appleDictButton; @@ -140,7 +141,7 @@ - (void)updateConstraints { } // Eudic - if (EZConfiguration.shared.showEudicQuickLink) { + if (Configuration.shared.showEudicQuickLink) { EZOpenLinkButton *eudicButton = [[EZOpenLinkButton alloc] init]; // !!!: Note that some applications have multiple channel versions. Ref: https://github.com/tisfeng/Raycast-Easydict/issues/16 diff --git a/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m b/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m index aafe9b446..7c32f3634 100644 --- a/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m +++ b/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m @@ -8,6 +8,7 @@ #import "EZWebViewManager.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" static NSString *kObjcHandler = @"objcHandler"; static NSString *kMethod = @"method"; @@ -57,7 +58,7 @@ - (void)userContentController:(WKUserContentController *)userContentController d #pragma mark - WebView evaluateJavaScript - (void)updateAllIframe { - CGFloat fontSize = EZConfiguration.shared.fontSizeRatio; // 1.4 --> 140% + CGFloat fontSize = Configuration.shared.fontSizeRatio; // 1.4 --> 140% NSString *script = [NSString stringWithFormat:@"changeIframeBodyFontSize(%.1f); updateAllIframeStyle();", fontSize]; [self.webView evaluateJavaScript:script completionHandler:^(id _Nullable result, NSError *_Nullable error) { if (!error) { diff --git a/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m b/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m index 10268c06b..0bf1f2b72 100644 --- a/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m +++ b/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m @@ -58,7 +58,7 @@ - (instancetype)initWithFrame:(NSRect)frame { if (self) { self.wantsLayer = YES; self.layer.cornerRadius = EZCornerRadius_8; - self.fontSizeRatio = EZConfiguration.shared.fontSizeRatio; + self.fontSizeRatio = Configuration.shared.fontSizeRatio; [self.layer excuteLight:^(CALayer *layer) { layer.backgroundColor = [NSColor ez_resultViewBgLightColor].CGColor; } dark:^(CALayer *layer) { @@ -71,7 +71,7 @@ - (instancetype)initWithFrame:(NSRect)frame { // TODO: This method is too long, need to refactor. - (void)refreshWithResult:(EZQueryResult *)result { self.result = result; - self.fontSizeRatio = EZConfiguration.shared.fontSizeRatio; + self.fontSizeRatio = Configuration.shared.fontSizeRatio; EZTranslateWordResult *wordResult = result.wordResult; self.webView = result.webViewManager.webView; @@ -826,7 +826,7 @@ - (void)refreshWithResult:(EZQueryResult *)result { language = result.to; } - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; [result.service.audioPlayer playTextAudio:text @@ -1324,7 +1324,7 @@ - (void)getTextWithHref:(NSString *)href completionHandler:(void (^_Nullable)(NS } - (void)updateWebViewAllIframeFontSize { - CGFloat fontSize = EZConfiguration.shared.fontSizeRatio * 100; + CGFloat fontSize = Configuration.shared.fontSizeRatio * 100; NSString *jsCode = [NSString stringWithFormat: @"var iframes = document.querySelectorAll('iframe');" diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h index e407b1360..e7301b5c8 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) NSString *inputText; @property (nonatomic, assign) EZWindowType windowType; -@property (nonatomic, weak) EZBaseQueryWindow *window; +@property (nullable, nonatomic, weak) EZBaseQueryWindow *baseQueryWindow; @property (nonatomic, strong, readonly) NSArray *services; diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index fd28c7928..5c753265a 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -192,7 +192,7 @@ - (void)setupServices { NSMutableArray *services = [NSMutableArray array]; self.youdaoService = nil; - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; NSArray *allServices = [EZLocalStorage.shared allServices:self.windowType]; for (EZQueryService *service in allServices) { @@ -352,7 +352,7 @@ - (NSString *)queryText { } - (EZQueryService *)defaultTTSService { - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; if (![_defaultTTSService.serviceType isEqualToString:defaultTTSServiceType]) { _defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; } @@ -480,7 +480,7 @@ - (void)startOCRImage:(NSImage *)image actionType:(EZActionType)actionType { return; } - if (EZConfiguration.shared.autoCopyOCRText) { + if (Configuration.shared.autoCopyOCRText) { [inputText copyToPasteboardSafely]; } @@ -493,7 +493,7 @@ - (void)startOCRImage:(NSImage *)image actionType:(EZActionType)actionType { return; } - BOOL autoSnipTranslate = EZConfiguration.shared.autoQueryOCRText; + BOOL autoSnipTranslate = Configuration.shared.autoQueryOCRText; if (autoSnipTranslate && queryModel.autoQuery) { [self startQueryText]; } @@ -522,11 +522,14 @@ - (void)retryQuery { - (void)focusInputTextView { // Fix ⚠️: ERROR: Setting as the first responder for window , but it is in a different window ((null))! This would eventually crash when the view is freed. The first responder will be set to nil. - if (self.queryView.window == self.window) { + if (self.queryView.window == self.baseQueryWindow) { // Need to activate the current application first. [NSApp activateIgnoringOtherApps:YES]; - [self.window makeFirstResponder:self.queryView.textView]; + [self.baseQueryWindow makeFirstResponder:self.queryView.textView]; + if (Configuration.shared.selectQueryTextWhenWindowActivate) { + self.queryView.textView.selectedRange = NSMakeRange(0, self.inputText.length); + } } } @@ -1021,34 +1024,12 @@ - (void)resetQueryAndResults { - (NSArray *)resetAllResults { NSMutableArray *allResults = [NSMutableArray array]; for (EZQueryService *service in self.services) { - EZQueryResult *result = [self resetServiceResult:service]; + EZQueryResult *result = [service resetServiceResult]; [allResults addObject:result]; } return allResults; } -- (EZQueryResult *)resetServiceResult:(EZQueryService *)service { - EZQueryResult *result = service.result; - [result reset]; - if (!result) { - result = [[EZQueryResult alloc] init]; - } - - NSArray *enabledReplaceTypes = @[ - EZActionTypeAutoSelectQuery, - EZActionTypeShortcutQuery, - EZActionTypeInvokeQuery, - ]; - if ([enabledReplaceTypes containsObject:self.queryModel.actionType]) { - result.showReplaceButton = EZEventMonitor.shared.isSelectedTextEditable; - } else { - result.showReplaceButton = NO; - } - - service.result = result; - return result; -} - - (nullable EZResultView *)resultCellOfResult:(EZQueryResult *)result { NSInteger index = [self.services indexOfObject:result.service]; NSInteger row = index + [self resultCellOffset]; @@ -1199,7 +1180,7 @@ - (EZQueryView *)createQueryView { [queryView setPasteTextBlock:^(NSString *_Nonnull text) { mm_strongify(self); [self detectQueryText:^(NSString *_Nonnull language) { - if ([EZConfiguration.shared autoQueryPastedText]) { + if ([Configuration.shared autoQueryPastedText]) { [self startQueryWithType:EZActionTypeInputQuery]; } }]; @@ -1292,7 +1273,7 @@ - (void)setupResultCell:(EZResultView *)resultView { // Make enabledQuery = YES before retry, it may be closed manually. service.enabledQuery = YES; - EZQueryResult *newResult = [self resetServiceResult:service]; + EZQueryResult *newResult = [service resetServiceResult]; [self updateCellWithResult:newResult reloadData:YES completionHandler:^{ [self queryWithModel:self.queryModel service:service autoPlay:NO]; }]; @@ -1420,7 +1401,7 @@ - (void)updateWindowViewHeightWithLock:(BOOL)lockFlag // ???: why set window frame will change tableView height? // ???: why this window animation will block cell rendering? // [self.window setFrame:safeFrame display:NO animate:animateFlag]; - [self.window setFrame:safeFrame display:NO]; + [self.baseQueryWindow setFrame:safeFrame display:NO]; // Restore tableView height. self.tableView.height = tableViewHeight; @@ -1484,7 +1465,7 @@ - (CGFloat)miniQueryViewHeight { #pragma mark - Auto play English word - (void)autoPlayEnglishWordAudio { - if (!EZConfiguration.shared.autoPlayAudio) { + if (!Configuration.shared.autoPlayAudio) { return; } @@ -1502,7 +1483,7 @@ - (void)autoPlayEnglishWordAudio { /// Auto copy translated text. - (void)autoCopyTranslatedTextOfService:(EZQueryService *)service { - if (![EZConfiguration.shared autoCopyFirstTranslatedText]) { + if (![Configuration.shared autoCopyFirstTranslatedText]) { service.autoCopyTranslatedTextBlock = nil; return; } diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m index 1e16d79e8..b2b56b8bd 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m @@ -70,7 +70,7 @@ - (void)setWindowType:(EZWindowType)windowType { - (void)setQueryViewController:(EZBaseQueryViewController *)viewController { _queryViewController = viewController; - viewController.window = self; + viewController.baseQueryWindow = self; self.contentViewController = viewController; } diff --git a/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m b/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m index 0fecef8c3..3b8490d02 100644 --- a/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m +++ b/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m @@ -9,6 +9,7 @@ #import "EZLayoutManager.h" #import "EZBaseQueryWindow.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZLayoutManager () @@ -46,7 +47,7 @@ - (void)commonInitialize { self.screen = NSScreen.mainScreen; self.minimumWindowSize = CGSizeMake(300, 70); - EZConfiguration *configuration = [EZConfiguration shared]; + Configuration *configuration = [Configuration shared]; self.miniWindowFrame = [configuration windowFrameWithType:EZWindowTypeMini]; if (CGRectEqualToRect(self.miniWindowFrame, CGRectZero)) { @@ -207,7 +208,7 @@ - (void)updateWindowFrame:(EZBaseQueryWindow *)window { break; } - [EZConfiguration.shared setWindowFrame:window.frame windowType:windowType]; + [Configuration.shared setWindowFrame:window.frame windowType:windowType]; } @end diff --git a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m index 24204c5e4..1b098b8d5 100644 --- a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m +++ b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m @@ -15,6 +15,7 @@ #import "EZPreferencesWindowController.h" #import "EZConfiguration.h" #import "EZLog.h" +#import "Easydict-Swift.h" @interface EZWindowManager () @@ -154,7 +155,7 @@ - (void)updatePopButtonQueryAction { mm_weakify(self); EZButton *popButton = self.popButtonWindow.popButton; - EZConfiguration *config = [EZConfiguration shared]; + Configuration *config = [Configuration shared]; if (config.hideMainWindow) { // FIXME: Click pop button will also show preferences window. @@ -182,7 +183,7 @@ - (void)updatePopButtonQueryAction { } - (void)popButtonWindowClicked { - EZWindowType windowType = EZConfiguration.shared.mouseSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.mouseSelectTranslateWindowType; self.actionType = EZActionTypeAutoSelectQuery; [self showFloatingWindowType:windowType queryText:self.selectedText]; [self->_popButtonWindow close]; @@ -291,7 +292,7 @@ - (void)showFloatingWindowType:(EZWindowType)windowType queryText:(nullable NSSt - (void)showFloatingWindowType:(EZWindowType)windowType queryText:(nullable NSString *)queryText actionType:(EZActionType)actionType { - BOOL autoQuery = [EZConfiguration.shared autoQuerySelectedText]; + BOOL autoQuery = [Configuration.shared autoQuerySelectedText]; [self showFloatingWindowType:windowType queryText:queryText autoQuery:autoQuery actionType:actionType]; } @@ -308,7 +309,7 @@ - (void)showFloatingWindowType:(EZWindowType)windowType actionType:(EZActionType)actionType atPoint:(CGPoint)point completionHandler:(nullable void (^)(void))completionHandler { - BOOL autoQuery = [EZConfiguration.shared autoQuerySelectedText]; + BOOL autoQuery = [Configuration.shared autoQuerySelectedText]; [self showFloatingWindowType:windowType queryText:queryText autoQuery:autoQuery actionType:actionType atPoint:point completionHandler:completionHandler]; } @@ -361,7 +362,7 @@ - (void)showFloatingWindowType:(EZWindowType)windowType } // TODO: Maybe we should remove this option, it seems useless. - if ([EZConfiguration.shared autoCopySelectedText]) { + if ([Configuration.shared autoCopySelectedText]) { [queryText copyToPasteboard]; } @@ -434,11 +435,6 @@ - (void)showFloatingWindow:(EZBaseQueryWindow *)window atPoint:(CGPoint)point { [window.queryViewController focusInputTextView]; [self updateFloatingWindowType:window.windowType]; - - // mainWindow has been ordered out before, so we need to order back. - if ([EZMainQueryWindow isAlive]) { - [self.mainWindow orderBack:nil]; - } } - (void)updateFloatingWindowType:(EZWindowType)floatingWindowType { @@ -517,7 +513,7 @@ - (CGPoint)getPopButtonWindowLocation { // NSLog(@"start point: %@", NSStringFromPoint(startLocation)); // NSLog(@"end point: %@", NSStringFromPoint(endLocation)); - if (EZConfiguration.shared.adjustPopButtomOrigin) { + if (Configuration.shared.adjustPopButtomOrigin) { // Since the pop button may cover selected text, we need to move it to the left. CGFloat horizontalOffset = 20; @@ -539,7 +535,7 @@ - (CGPoint)getPopButtonWindowLocation { - (CGPoint)getMiniWindowLocation { CGPoint position = [self getShowingMouseLocation]; - if (EZConfiguration.shared.adjustPopButtomOrigin) { + if (Configuration.shared.adjustPopButtomOrigin) { position.y = position.y - 8; } @@ -587,7 +583,7 @@ - (CGPoint)getMouseLocation:(BOOL)offsetFlag { /// !!!: This return value is top-left point. - (CGPoint)getFixedWindowLocation { CGPoint position = CGPointZero; - EZShowWindowPosition windowPosition = EZConfiguration.shared.fixedWindowPosition; + EZShowWindowPosition windowPosition = Configuration.shared.fixedWindowPosition; switch (windowPosition) { case EZShowWindowPositionRight: { position = [self getFloatingWindowInRightSideOfScreenPoint:self.fixedWindow]; @@ -651,7 +647,7 @@ - (void)saveFrontmostApplication { } - (void)showMainWindowIfNedded { - BOOL showFlag = !EZConfiguration.shared.hideMainWindow; + BOOL showFlag = !Configuration.shared.hideMainWindow; NSApplicationActivationPolicy activationPolicy = showFlag ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; [NSApp setActivationPolicy:activationPolicy]; @@ -689,13 +685,18 @@ - (void)selectTextTranslate { return; } - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; NSLog(@"selectTextTranslate windowType: %@", @(windowType)); self.eventMonitor.actionType = EZActionTypeShortcutQuery; [self.eventMonitor getSelectedText:^(NSString *_Nullable text) { - // If text is nil, currently, we choose to clear input. - self.selectedText = [text trim] ?: @""; self.actionType = self.eventMonitor.actionType; + + // Clear query if text is nil and user don't want to keep the last result. + if (!text && !Configuration.shared.keepPrevResultWhenEmpty) { + text = @""; + } + self.selectedText = [text trim]; + [self showFloatingWindowType:windowType queryText:self.selectedText]; }]; } @@ -715,7 +716,7 @@ - (void)snipTranslate { } // Since ocr detect may be inaccurate, sometimes need to set sourceLanguage manually, so show Fixed window. - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; EZBaseQueryWindow *window = [self windowWithType:windowType]; // Wait to close floating window if need. @@ -755,7 +756,7 @@ - (void)inputTranslate { return; } - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; if (self.floatingWindowType == windowType) { [self closeFloatingWindow]; @@ -763,7 +764,7 @@ - (void)inputTranslate { } NSString *queryText = nil; - if ([EZConfiguration.shared clearInput]) { + if ([Configuration.shared clearInput]) { queryText = @""; } @@ -775,7 +776,7 @@ - (void)inputTranslate { - (void)showMiniFloatingWindow { MMLogInfo(@"showMiniFloatingWindow"); - EZWindowType windowType = EZConfiguration.shared.mouseSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.mouseSelectTranslateWindowType; if (self.floatingWindowType == windowType) { [self closeFloatingWindow]; diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift new file mode 100644 index 000000000..240aed001 --- /dev/null +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -0,0 +1,203 @@ +// +// Configuration+Defaults.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/12. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation +import Magnet + +/// Utils +extension Defaults.Keys { + /// is first launch + static let firstLaunch = Key("EZConfiguration_kFirstLaunch", default: true) +} + +// Setting +extension Defaults.Keys { + // rename `from` + static let queryFromLanguage = Key("EZConfiguration_kFromKey", default: .auto) + // rename `to` + static let queryToLanguage = Key("EZConfiguration_kToKey", default: .auto) + + static let firstLanguage = Key("EZConfiguration_kFirstLanguageKey", default: EZLanguageManager.shared().systemPreferredTwoLanguages[0]) + static let secondLanguage = Key("EZConfiguration_kSecondLanguageKey", default: EZLanguageManager.shared().systemPreferredTwoLanguages[1]) + + static let autoSelectText = Key("EZConfiguration_kAutoSelectTextKey", default: true) + static let forceAutoGetSelectedText = Key("EZConfiguration_kForceAutoGetSelectedText", default: false) + + static let disableEmptyCopyBeep = Key("EZConfiguration_kDisableEmptyCopyBeepKey", default: true) + static let clickQuery = Key("EZConfiguration_kClickQueryKey", default: false) + static let autoPlayAudio = Key("EZConfiguration_kAutoPlayAudioKey", default: true) + static let launchAtStartup = Key("EZConfiguration_kLaunchAtStartupKey", default: false) + static let hideMainWindow = Key("EZConfiguration_kHideMainWindowKey", default: true) + static let autoQueryOCRText = Key("EZConfiguration_kAutoQueryOCTTextKey", default: true) + static let autoQuerySelectedText = Key("EZConfiguration_kAutoQuerySelectedTextKey", default: true) + static let autoQueryPastedText = Key("EZConfiguration_kAutoQueryPastedTextKey", default: false) + static let autoCopyOCRText = Key("EZConfiguration_kAutoCopyOCRTextKey", default: false) + static let autoCopySelectedText = Key("EZConfiguration_kAutoCopySelectedTextKey", default: false) + static let autoCopyFirstTranslatedText = Key("EZConfiguration_kAutoCopyFirstTranslatedTextKey", default: false) + static let languageDetectOptimize = Key("EZConfiguration_kLanguageDetectOptimizeTypeKey", default: LanguageDetectOptimize.none) + static let defaultTTSServiceType = Key("EZConfiguration_kDefaultTTSServiceTypeKey", default: TTSServiceType.youdao) + static let showGoogleQuickLink = Key("EZConfiguration_kShowGoogleLinkKey", default: true) + static let showEudicQuickLink = Key("EZConfiguration_kShowEudicLinkKey", default: true) + static let showAppleDictionaryQuickLink = Key("EZConfiguration_kShowAppleDictionaryLinkKey", default: true) + static let hideMenuBarIcon = Key("EZConfiguration_kHideMenuBarIconKey", default: false) + static let fixedWindowPosition = Key("EZConfiguration_kShowFixedWindowPositionKey", default: .right) + static let mouseSelectTranslateWindowType = Key("EZConfiguration_kMouseSelectTranslateWindowTypeKey", default: .mini) + static let shortcutSelectTranslateWindowType = Key("EZConfiguration_kShortcutSelectTranslateWindowTypeKey", default: .fixed) + static let adjustPopButtonOrigin = Key("EZConfiguration_kAdjustPopButtomOriginKey", default: false) + static let allowCrashLog = Key("EZConfiguration_kAllowCrashLogKey", default: true) + static let allowAnalytics = Key("EZConfiguration_kAllowAnalyticsKey", default: true) + static let clearInput = Key("EZConfiguration_kClearInputKey", default: true) + static let keepPrevResultWhenEmpty = Key("EZConfiguration_kKeepPrevResultKey", default: true) + static let selectQueryTextWhenWindowActivate = Key("EZConfiguration_kSelectQueryTextWhenWindowActivate", default: false) + static let enableBetaNewApp = Key("EZConfiguration_kEnableBetaNewAppKey", default: false) + + static let enableBetaFeature = Key("EZBetaFeatureKey", default: false) + + static let appearanceType = Key("EZConfiguration_kApperanceKey", default: .followSystem) + static let fontSizeOptionIndex = Key("EZConfiguration_kTranslationControllerFontKey", default: 0) + static let selectedMenuBarIcon = Key("EZConfiguration_kSelectedMenuBarIconKey", default: .square) +} + +extension Defaults.Keys { + static func intelligentQueryTextType(for serviceType: ServiceType) -> Key { + let key = EZConstKey.constkey("IntelligentQueryTextType", serviceType: serviceType) + return .init(key, default: EZQueryTextType(rawValue: 7)) + } + + static func queryTextType(for serviceType: ServiceType) -> Key { + let key = EZConstKey.constkey("QueryTextType", serviceType: serviceType) + return .init(key, default: EZQueryTextType(rawValue: 0)) + } + + static func windorFrame(for windowType: EZWindowType) -> Key { + let key = "EZConfiguration_kWindowFrameKey_\(windowType)" + return .init(key, default: .zero) + } +} + +extension EZQueryTextType: Defaults.Serializable { + public static var bridge: Bridge = .init() + + public struct Bridge: Defaults.Bridge { + public func serialize(_ value: EZQueryTextType?) -> String? { + guard let value else { return "7" } + return "\(value.rawValue)" + } + + public func deserialize(_ object: String?) -> EZQueryTextType? { + guard let object else { return nil } + return EZQueryTextType(rawValue: UInt(object) ?? 7) + } + + public typealias Value = EZQueryTextType + + public typealias Serializable = String + } +} + +extension CGRect: Defaults.Serializable { + public static var bridge: Bridge = .init() + + public struct Bridge: Defaults.Bridge { + public func serialize(_ value: CGRect?) -> String? { + let value = value ?? .zero + return NSStringFromRect(value) + } + + public func deserialize(_ object: String?) -> CGRect? { + guard let object else { return nil } + return NSRectFromString(object) + } + + public typealias Value = CGRect + + public typealias Serializable = String + } +} + +@propertyWrapper +class DefaultsWrapper { + var wrappedValue: T { + get { + Defaults[key] + } set { + Defaults[key] = newValue + } + } + + init(_ key: Defaults.Key) { + self.key = key + } + + let key: Defaults.Key +} + +// Service Configuration +extension Defaults.Keys { + // OpenAI + static let openAIAPIKey = Key("EZOpenAIAPIKey") + static let openAITranslation = Key("EZOpenAITranslationKey", default: "1") + static let openAIDictionary = Key("EZOpenAIDictionaryKey", default: "1") + static let openAISentence = Key("EZOpenAISentenceKey", default: "1") + static let openAIServiceUsageStatus = Key("EZOpenAIServiceUsageStatusKey", default: OpenAIUsageStats.default) + static let openAIEndPoint = Key("EZOpenAIEndPointKey") + static let openAIModel = Key("EZOpenAIModelKey", default: OpenAIModels.gpt3_5_turbo_0125) + + // DeepL + static let deepLAuth = Key("EZDeepLAuthKey") + static let deepLTranslation = Key("EZDeepLTranslationAPIKey", default: DeepLAPIUsagePriority.webFirst) + static let deepLTranslateEndPointKey = Key("EZDeepLTranslateEndPointKey") + + // Bing + static let bingCookieKey = Key("EZBingCookieKey") + + // niu + static let niuTransAPIKey = Key("EZNiuTransAPIKey") + + // Caiyun + static let caiyunToken = Key("EZCaiyunToken") + + // tencent + static let tencentSecretId = Key("EZTencentSecretId") + static let tencentSecretKey = Key("EZTencentSecretKey") + + // Ali + static let aliAccessKeyId = Key("EZAliAccessKeyId") + static let aliAccessKeySecret = Key("EZAliAccessKeySecret") + + // Gemni + static let geminiAPIKey = Key("EZGeminiAPIKey") +} + +/// shortcut +extension Defaults.Keys { + // Global + static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder") + static let snipShortcut = Key("EZSnipShortcutKey_keyHolder") + static let inputShortcut = Key("EZInputShortcutKey_keyHolder") + static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder") + static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder") + + // App + static let clearInputShortcut = Key("EZClearInputShortcutKey_keyHolder") + static let clearAllShortcut = Key("EZClearAllShortcutKey_keyHolder") + static let copyShortcut = Key("EZCopyShortcutKey_keyHolder") + static let copyFirstResultShortcut = Key("EZCopyFirstResultShortcutKey_keyHolder") + static let focusShortcut = Key("EZFocusShortcutKey_keyHolder") + static let playShortcut = Key("EZPlayShortcutKey_keyHolder") + static let retryShortcut = Key("EZRetryShortcutKey_keyHolder") + static let toggleShortcut = Key("EZToggleShortcutKey_keyHolder") + static let pinShortcut = Key("EZPinShortcutKey_keyHolder") + static let hideShortcut = Key("EZHideShortcutKey_keyHolder") + static let increaseFontSize = Key("EZIncreaseFontSizeShortcutKey_keyHolder") + static let decreaseFontSize = Key("EZDecreaseFontSizeShortcutKey_keyHolder") + static let googleShortcut = Key("EZGoogleShortcutKey_keyHolder") + static let eudicShortcut = Key("EZEudicShortcutKey_keyHolder") + static let appleDictionaryShortcut = Key("EZAppleDictionaryShortcutKey_keyHolder") +} diff --git a/Easydict/NewApp/EasydictApp.swift b/Easydict/NewApp/EasydictApp.swift index ae385f4e7..9743e4dc4 100644 --- a/Easydict/NewApp/EasydictApp.swift +++ b/Easydict/NewApp/EasydictApp.swift @@ -6,6 +6,8 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults +import Sparkle import SwiftUI @main @@ -22,18 +24,15 @@ enum EasydictCmpatibilityEntry { struct EasydictApp: App { @NSApplicationDelegateAdaptor - var delegate: AppDelegate + private var delegate: AppDelegate - @AppStorage(kHideMenuBarIconKey) - private var hideMenuBar = false + // Use `@Default` will cause a purple warning and continuously call `set` of it. + // I'm not sure why. Just leave `AppStorage` here. + @AppStorage(Defaults.Key.hideMenuBarIcon.name) + private var hideMenuBar = Defaults.Key.hideMenuBarIcon.defaultValue - private var menuBarImage: String { - #if DEBUG - "status_icon_debug" - #else - "status_icon" - #endif - } + @Default(.selectedMenuBarIcon) + private var menuBarIcon var body: some Scene { if #available(macOS 13, *) { @@ -43,13 +42,21 @@ struct EasydictApp: App { Label { Text("Easydict") } icon: { - Image(menuBarImage) + Image(menuBarIcon.rawValue) .resizable() + #if DEBUG + .renderingMode(.original) + #else .renderingMode(.template) + #endif .scaledToFit() } .help("Easydict 🍃") } + .menuBarExtraStyle(.menu) + .commands { + EasyDictMainMenu() // main menu + } Settings { SettingView() } @@ -63,3 +70,10 @@ extension Bool { mutating set { self = newValue.toggledValue } } } + +enum MenuBarIconType: String, CaseIterable, Defaults.Serializable, Identifiable { + var id: Self { self } + + case square = "square_menu_bar_icon" + case rounded = "rounded_menu_bar_icon" +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Bind.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Bind.swift new file mode 100644 index 000000000..37008348c --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Bind.swift @@ -0,0 +1,96 @@ +// +// Shortcut+Bind.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +// App shortcut binding func +extension Shortcut { + @objc func clearInput() { + EZWindowManager.shared().clearInput() + } + + @objc func clearAll() { + EZWindowManager.shared().clearAll() + } + + @objc func shortcutCopy() { + EZWindowManager.shared().copyQueryText() + } + + @objc func shortcutCopyFirstResult() { + EZWindowManager.shared().copyFirstTranslatedText() + } + + @objc func shortcutFocus() { + EZWindowManager.shared().focusInputTextView() + } + + @objc func shortcutPlay() { + EZWindowManager.shared().playOrStopQueryTextAudio() + } + + @objc func shortcutRetry() { + EZWindowManager.shared().rerty() + } + + @objc func shortcutToggle() { + EZWindowManager.shared().toggleTranslationLanguages() + } + + @objc func shortcutPin() { + EZWindowManager.shared().pin() + } + + @objc func shortcutHide() { + EZWindowManager.shared().closeWindowOrExitSreenshot() + } + + @objc func increaseFontSize() { + Configuration.shared.fontSizeIndex += 1 + } + + @objc func decreaseFontSize() { + Configuration.shared.fontSizeIndex -= 1 + } + + @objc func shortcutGoogle() { + let window = EZWindowManager.shared().floatingWindow + window?.titleBar.googleButton.openLink() + } + + @objc func shortcutEudic() { + let window = EZWindowManager.shared().floatingWindow + window?.titleBar.eudicButton.openLink() + } + + @objc func shortcutAppleDic() { + let window = EZWindowManager.shared().floatingWindow + window?.titleBar.appleDictionaryButton.openLink() + } +} + +// global shortcut binding func +extension Shortcut { + @objc func selectTextTranslate() { + EZWindowManager.shared().selectTextTranslate() + } + + @objc func snipTranslate() { + EZWindowManager.shared().snipTranslate() + } + + @objc func inputTranslate() { + EZWindowManager.shared().inputTranslate() + } + + @objc func showMiniFloatingWindow() { + EZWindowManager.shared().showMiniFloatingWindow() + } + + @objc func screenshotOCR() { + EZWindowManager.shared().screenshotOCR() + } +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift new file mode 100644 index 000000000..d48d74d1d --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift @@ -0,0 +1,31 @@ +// +// Shortcut+Default.swift +// Easydict +// +// Created by Sharker on 2024/2/5. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Magnet + +extension Shortcut { + // set defalut for app shortcut + func setDefaultForAppShortcut() { + Defaults[.clearInputShortcut] = KeyCombo(key: .k, cocoaModifiers: .command) + Defaults[.clearAllShortcut] = KeyCombo(key: .k, cocoaModifiers: [.command, .shift]) + Defaults[.copyShortcut] = KeyCombo(key: .c, cocoaModifiers: [.command, .shift]) + Defaults[.copyFirstResultShortcut] = KeyCombo(key: .j, cocoaModifiers: [.command, .shift]) + Defaults[.focusShortcut] = KeyCombo(key: .i, cocoaModifiers: .command) + Defaults[.playShortcut] = KeyCombo(key: .s, cocoaModifiers: .command) + Defaults[.retryShortcut] = KeyCombo(key: .r, cocoaModifiers: .command) + Defaults[.toggleShortcut] = KeyCombo(key: .t, cocoaModifiers: .command) + Defaults[.pinShortcut] = KeyCombo(key: .p, cocoaModifiers: .command) + Defaults[.hideShortcut] = KeyCombo(key: .y, cocoaModifiers: .command) + Defaults[.increaseFontSize] = KeyCombo(key: .keypadPlus, cocoaModifiers: .command) + Defaults[.decreaseFontSize] = KeyCombo(key: .keypadMinus, cocoaModifiers: .command) + Defaults[.googleShortcut] = KeyCombo(key: .return, cocoaModifiers: .command) + Defaults[.eudicShortcut] = KeyCombo(key: .return, cocoaModifiers: [.command, .shift]) + Defaults[.appleDictionaryShortcut] = KeyCombo(key: .d, cocoaModifiers: [.command, .shift]) + } +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Menu.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Menu.swift new file mode 100644 index 000000000..04f109aa6 --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Menu.swift @@ -0,0 +1,20 @@ +// +// Shortcut+Menu.swift +// Easydict +// +// Created by Sharker on 2024/2/7. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +extension Shortcut { + @available(macOS 13, *) + func updateMenu(_ type: ShortcutType) { // update shortcut menu + let shortcutTitle = String(localized: LocalizedStringResource(stringLiteral: type.localizedStringKey())) + let menuTitle = String(localized: LocalizedStringResource(stringLiteral: "shortcut")) + let shortcutMenu = NSApp.mainMenu?.items.first(where: { $0.title == menuTitle }) + let clearInput = shortcutMenu?.submenu?.items.first(where: { $0.title == shortcutTitle }) + clearInput?.keyEquivalent = "" + } +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Validator.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Validator.swift new file mode 100644 index 000000000..f7590400e --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Validator.swift @@ -0,0 +1,101 @@ +// +// Shortcut+Validator.swift +// Easydict +// +// Created by Sharker on 2024/1/29. +// Copyright © 2024 izual. All rights reserved. +// + +import Carbon +import Carbon.HIToolbox +import Foundation +import KeyHolder +import Magnet +import Sauce + +extension Shortcut { + static func validateShortcut(_ keyCombo: KeyCombo) -> Bool { + validateShortcutConfictBySystem(keyCombo) || + validateShortcutConfictByMenuItem(keyCombo) || + validateShortcutConfictByCustom(keyCombo) + } +} + +// validate shortcut used by system +// ref: https://github.com/cocoabits/MASShortcut/blob/6f2603c6b6cc18f64a799e5d2c9d3bbc467c413a/Framework/Model/MASShortcutValidator.m#L94 +extension Shortcut { + static func validateShortcutConfictBySystem(_ keyCombo: KeyCombo) -> Bool { + systemUsedShortcut().contains(keyCombo) + } + + static func systemUsedShortcut() -> [KeyCombo] { + var shortcutsUnmanaged: Unmanaged? + guard + CopySymbolicHotKeys(&shortcutsUnmanaged) == noErr, + let shortcuts = shortcutsUnmanaged?.takeRetainedValue() as? [[String: Any]] + else { + assertionFailure("Could not get system keyboard shortcuts") + return [] + } + return shortcuts.compactMap { + guard + ($0[kHISymbolicHotKeyEnabled] as? Bool) == true, + let carbonKeyCode = $0[kHISymbolicHotKeyCode] as? Int, + let carbonModifiers = $0[kHISymbolicHotKeyModifiers] as? Int + else { + return nil + } + guard let key = Sauce.shared.key(for: Int(carbonKeyCode)) else { return nil } + guard let keyCombo = KeyCombo(key: key, carbonModifiers: carbonModifiers) else { return nil } + return keyCombo + } + } +} + +// validate shortcut used by menuItem +extension Shortcut { + static func validateShortcutConfictByMenuItem(_ keyCombo: KeyCombo) -> Bool { + if let item = menuItemUsedShortcut(keyCombo) { + Shortcut.shared.confictMenuItem = item + return true + } else { + return false + } + } + + static func menuItemUsedShortcut(_ keyCombo: KeyCombo) -> NSMenuItem? { + guard let mainMenu = NSApp.mainMenu else { + return nil + } + return menuItemWithMatchingShortcut(in: mainMenu, keyCombo: keyCombo) + } + + static func menuItemWithMatchingShortcut(in menu: NSMenu, keyCombo: KeyCombo) -> NSMenuItem? { + for item in menu.items { + let keyEquivalent = item.keyEquivalent + let keyEquivalentModifierMask = item.keyEquivalentModifierMask + if keyCombo.keyEquivalent == keyEquivalent, + keyCombo.keyEquivalentModifierMask == keyEquivalentModifierMask, + keyCombo.keyEquivalent != "" + { + return item + } + if let submenu = item.submenu, + let menuItem = menuItemWithMatchingShortcut(in: submenu, keyCombo: keyCombo) + { + return menuItem + } + } + return nil + } +} + +// validate shortcut used by custom +// ref: https://support.apple.com/zh-cn/HT201236 +extension Shortcut { + static func validateShortcutConfictByCustom(_: KeyCombo) -> Bool { + false + } + + static func customUsedShortcut(_: KeyCombo) {} +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift new file mode 100644 index 000000000..c08c2dc2a --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift @@ -0,0 +1,287 @@ +// +// Shortcut.swift +// Easydict +// +// Created by Sharker on 2024/1/20. +// Copyright © 2024 izual. All rights reserved. + +import Defaults +import Foundation +import KeyHolder +import Magnet +import SwiftUI + +/// Shortcut Service +public enum ShortcutType: String { + // Global + case inputTranslate = "EZInputShortcutKey" + case snipTranslate = "EZSnipShortcutKey" + case selectTranslate = "EZSelectionShortcutKey" + case silentScreenshotOcr = "EZScreenshotOCRShortcutKey" + case showMiniWindow = "EZShowMiniShortcutKey" + // In App + case clearInput = "EZClearInputShortcutKey" + case clearAll = "EZClearAllShortcutKey" + case copy = "EZCopyShortcutKey" + case copyFirstResult = "EZCopyFirstResultShortcutKey" + case focus = "EZFocusShortcutKey" + case play = "EZPlayShortcutKey" + case retry = "EZRetryShortcutKey" + case toggle = "EZToggleShortcutKey" + case pin = "EZPinShortcutKey" + case hide = "EZHideShortcutKey" + case increaseFontSize = "EZIncreaseFontSizeShortcutKey" + case decreaseFontSize = "EZDecreaseFontSizeShortcutKey" + case google = "EZGoogleShortcutKey" + case eudic = "EZEudicShortcutKey" + case appleDic = "EZAppleDicShortcutKey" +} + +extension ShortcutType { + func localizedStringKey() -> String { + switch self { + case .inputTranslate: + "input_translate" + case .snipTranslate: + "snip_translate" + case .selectTranslate: + "select_translate" + case .silentScreenshotOcr: + "silent_screenshot_ocr" + case .showMiniWindow: + "show_mini_window" + case .clearInput: + "shortcut_clear_input" + case .clearAll: + "shortcut_clear_all" + case .copy: + "shortcut_copy" + case .copyFirstResult: + "shortcut_copy_first_translated_text" + case .focus: + "shortcut_focus" + case .play: + "shortcut_play" + case .retry: + "retry" + case .toggle: + "toggle_languages" + case .pin: + "pin" + case .hide: + "hide" + case .increaseFontSize: + "shortcut_increase_font" + case .decreaseFontSize: + "shortcut_decrease_font" + case .google: + "open_in_google" + case .eudic: + "open_in_eudic" + case .appleDic: + "open_in_apple_dictionary" + } + } +} + +// Confict Message +public struct ShortcutConfictAlertMessage: Identifiable { + public var id: String { message } + var title: String + var message: String +} + +class Shortcut: NSObject { + var confictMenuItem: NSMenuItem? + + static let shared = Shortcut() + + @objc static func setupShortcut() { + let shortcut = Shortcut.shared + shortcut.restoreShortcut() + + if Defaults[.firstLaunch] { + Defaults[.firstLaunch] = false + // set defalut for app shortcut + shortcut.setDefaultForAppShortcut() + } else { + // do nothing + } + } + + // Make sure the class has only one instance + // Should not init or copy outside + override private init() {} + + override func copy() -> Any { + self // SingletonClass.shared + } + + override func mutableCopy() -> Any { + self // SingletonClass.shared + } + + // Optional + func reset() { + // Reset all properties to default value + } +} + +// restore shortcut +extension Shortcut { + func restoreShortcut() { + // inputTranslate + bindingShortcut(keyCombo: Defaults[.inputShortcut], type: .inputTranslate) + // snipTranslate + bindingShortcut(keyCombo: Defaults[.snipShortcut], type: .snipTranslate) + // selectTranslate + bindingShortcut(keyCombo: Defaults[.selectionShortcut], type: .selectTranslate) + // silentScreenshotOcr + bindingShortcut(keyCombo: Defaults[.screenshotOCRShortcut], type: .silentScreenshotOcr) + // showMiniWindow + bindingShortcut(keyCombo: Defaults[.showMiniWindowShortcut], type: .showMiniWindow) + } +} + +// binding shortcut +extension Shortcut { + func bindingShortcut(keyCombo: KeyCombo?, type: ShortcutType) { + guard let keyCombo else { + HotKeyCenter.shared.unregisterHotKey(with: type.rawValue) + return + } + var hotKey: HotKey? + switch type { + case .inputTranslate: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.inputTranslate)) + case .snipTranslate: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.snipTranslate)) + case .selectTranslate: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.selectTextTranslate)) + case .silentScreenshotOcr: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.screenshotOCR)) + case .showMiniWindow: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.showMiniFloatingWindow)) + default: () + } + + hotKey?.register() + } +} + +struct KeyboardShortcut: ViewModifier { + init(type: ShortcutType) { + let key: Defaults.Key = switch type { + case .inputTranslate: + .inputShortcut + case .snipTranslate: + .snipShortcut + case .selectTranslate: + .selectionShortcut + case .silentScreenshotOcr: + .screenshotOCRShortcut + case .showMiniWindow: + .showMiniWindowShortcut + case .clearInput: + .clearInputShortcut + case .clearAll: + .clearAllShortcut + case .copy: + .copyShortcut + case .copyFirstResult: + .copyFirstResultShortcut + case .focus: + .focusShortcut + case .play: + .playShortcut + case .retry: + .retryShortcut + case .toggle: + .toggleShortcut + case .pin: + .pinShortcut + case .hide: + .hideShortcut + case .increaseFontSize: + .increaseFontSize + case .decreaseFontSize: + .decreaseFontSize + case .google: + .googleShortcut + case .eudic: + .eudicShortcut + case .appleDic: + .appleDictionaryShortcut + } + + _shortcut = .init(key) + } + + @Default var shortcut: KeyCombo? + + func body(content: Content) -> some View { + if let shortcut { + content + .keyboardShortcut( + fetchShortcutKeyEquivalent(shortcut), + modifiers: fetchShortcutKeyEventModifiers(shortcut) + ) + } else { + content + } + } + + private func fetchShortcutKeyEquivalent(_ keyCombo: KeyCombo) -> KeyEquivalent { + if keyCombo.doubledModifiers { + return KeyEquivalent(Character(keyCombo.keyEquivalentModifierMaskString)) + } else { + return KeyEquivalent(Character(keyCombo.keyEquivalent)) + } + } + + private func fetchShortcutKeyEventModifiers(_ keyCombo: KeyCombo) -> EventModifiers { + var modifiers: EventModifiers = [] + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.command) { + modifiers.update(with: EventModifiers.command) + } + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.control) { + modifiers.update(with: EventModifiers.control) + } + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.option) { + modifiers.update(with: EventModifiers.option) + } + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.shift) { + modifiers.update(with: EventModifiers.shift) + } + + return modifiers + } +} + +/// can't using keyEquivalent and EventModifiers in SwiftUI MenuItemView direct, because item +/// keyboardShortcut not support double modifier key but can use ⌥ as character +public extension View { + @ViewBuilder + func keyboardShortcut(_ type: ShortcutType) -> some View { + modifier(KeyboardShortcut(type: type)) + } +} diff --git a/Easydict/NewApp/Model/TTSServiceType.swift b/Easydict/NewApp/Model/TTSServiceType.swift new file mode 100644 index 000000000..a32efbe17 --- /dev/null +++ b/Easydict/NewApp/Model/TTSServiceType.swift @@ -0,0 +1,81 @@ +// +// TTSServiceType.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +enum TTSServiceType: String, CaseIterable { + case youdao = "Youdao" + case bing = "Bing" + case google = "Google" + case baidu = "Baidu" + case apple = "Apple" +} + +@available(macOS 13, *) +extension TTSServiceType: CustomLocalizedStringResourceConvertible { + var localizedStringResource: LocalizedStringResource { + switch self { + case .youdao: + "setting.tts_service.options.youdao" + case .bing: + "setting.tts_service.options.bing" + case .google: + "setting.tts_service.options.google" + case .baidu: + "setting.tts_service.options.baidu" + case .apple: + "setting.tts_service.options.apple" + } + } +} + +extension TTSServiceType: Defaults.Serializable { + // while in the future, ServiceType was deleted, then you can safely delete this struct and `bridge` + struct TTSServiceTypeBridge: Defaults.Bridge { + func serialize(_ value: TTSServiceType?) -> String? { + guard let value else { return nil } + switch value { + case .youdao: + return ServiceType.youdao.rawValue + case .bing: + return ServiceType.bing.rawValue + case .google: + return ServiceType.google.rawValue + case .baidu: + return ServiceType.baidu.rawValue + case .apple: + return ServiceType.apple.rawValue + } + } + + func deserialize(_ object: String?) -> TTSServiceType? { + guard let object else { return nil } + switch object { + case "Youdao": + return .youdao + case "Bing": + return .bing + case "Google": + return .google + case "Baidu": + return .baidu + case "Apple": + return .apple + default: + return nil + } + } + + typealias Value = TTSServiceType + + typealias Serializable = String + } + + static let bridge = TTSServiceTypeBridge() +} diff --git a/Easydict/NewApp/NewAppManager.swift b/Easydict/NewApp/NewAppManager.swift index 5b54187f0..8b3d9a8dc 100644 --- a/Easydict/NewApp/NewAppManager.swift +++ b/Easydict/NewApp/NewAppManager.swift @@ -22,13 +22,4 @@ public final class NewAppManager: NSObject { public var enable: Bool { UserDefaults.standard.bool(forKey: Self.enableKey) } - - @objc - public var showEnableToggleUI: Bool { - #if DEBUG - true - #else - false - #endif - } } diff --git a/Easydict/NewApp/Utility/Extensions/Defaults/KeyCombo+Defaults.Serializable.swift b/Easydict/NewApp/Utility/Extensions/Defaults/KeyCombo+Defaults.Serializable.swift new file mode 100644 index 000000000..6858aa387 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/Defaults/KeyCombo+Defaults.Serializable.swift @@ -0,0 +1,31 @@ +// +// KeyCombo+Defaults.Serializable.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/21. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation +import Magnet + +extension KeyCombo: Defaults.Serializable { + public static var bridge = ShortcutBridge() + + public struct ShortcutBridge: Defaults.Bridge { + public func serialize(_ value: Magnet.KeyCombo??) -> Data? { + guard let value else { return nil } + return try? JSONEncoder().encode(value) + } + + public func deserialize(_ object: Data?) -> Magnet.KeyCombo?? { + guard let data = object else { return nil } + return try? JSONDecoder().decode(KeyCombo.self, from: data) as Magnet.KeyCombo? + } + + public typealias Value = KeyCombo? + + public typealias Serializable = Data + } +} diff --git a/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift b/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift new file mode 100644 index 000000000..2da99bad1 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift @@ -0,0 +1,32 @@ +// +// LanguageDetectOptimizeExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension LanguageDetectOptimize: Defaults.Serializable {} + +extension LanguageDetectOptimize: CaseIterable { + public static let allCases: [LanguageDetectOptimize] = [.none, .baidu, .google] +} + +@available(macOS 13, *) +extension LanguageDetectOptimize: CustomLocalizedStringResourceConvertible { + public var localizedStringResource: LocalizedStringResource { + switch self { + case .none: + "language_detect_optimize_none" + case .google: + "language_detect_optimize_google" + case .baidu: + "language_detect_optimize_baidu" + @unknown default: + "unknown_option" + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/LanguageExtensions.swift b/Easydict/NewApp/Utility/Extensions/LanguageExtensions.swift new file mode 100644 index 000000000..b7cc84eae --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/LanguageExtensions.swift @@ -0,0 +1,78 @@ +// +// LanguageExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension Language: Defaults.Serializable {} + +extension Language: CaseIterable { + public static let allCases: [Language] = EZLanguageModel.allLanguagesDict().sortedKeys().map { rawValue in + Language(rawValue: rawValue as String) + } + + public static let allAvailableOptions: [Language] = allCases.filter { language in + language != .auto && language != .classicalChinese + } +} + +public extension Language { + var model: EZLanguageModel { + EZLanguageModel.allLanguagesDict().object(forKey: rawValue as NSString) + } + + var chineseName: String { + model.chineseName + } + + var englishName: String { + model.englishName.rawValue + } + + var localName: String { + model.localName + } + + var flagEmoji: String { + model.flagEmoji + } + + var voiceName: String { + model.voiceName + } + + var localeIdentifier: String { + model.localeIdentifier + } + + var localizedName: String { + if EZLanguageManager.shared().isSystemChineseFirstLanguage() { + chineseName + } else { + if self == .auto { + "Auto" + } else { + englishName + } + } + } +} + +extension [Language] { + /// Contains Chinese language, + func containsChinese() -> Bool { + contains { $0.isKindOfChinese() } + } +} + +extension Language { + /// Is kind of Chinese language, means it is simplifiedChinese or traditionalChinese. + func isKindOfChinese() -> Bool { + self == .simplifiedChinese || self == .traditionalChinese + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/AliService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/AliService+ConfigurableService.swift new file mode 100644 index 000000000..109501d18 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/AliService+ConfigurableService.swift @@ -0,0 +1,26 @@ +// +// AliService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension AliService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.aliAccessKeyId, .aliAccessKeySecret]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.ali.access_key_id.title", + key: .aliAccessKeyId + ) + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.ali.access_key_secret.title", + key: .aliAccessKeySecret + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/BingService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/BingService+ConfigurableService.swift new file mode 100644 index 000000000..be7d26361 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/BingService+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// BingService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension EZBingService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.bingCookieKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.bing.cookie.title", + key: .bingCookieKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift new file mode 100644 index 000000000..300fbf87b --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// CaiyunService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension CaiyunService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.caiyunToken]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.caiyun.token.title", + key: .caiyunToken + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift new file mode 100644 index 000000000..3c6a7d17b --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift @@ -0,0 +1,65 @@ +// +// DeepLTranslate+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/30. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension EZDeepLTranslate: ConfigurableService { + func configurationListItems() -> some View { + EZDeepLTranslateConfigurationView(service: self) + } +} + +@available(macOS 13.0, *) +private struct EZDeepLTranslateConfigurationView: View { + let service: EZDeepLTranslate + + var body: some View { + ServiceConfigurationSecretSectionView(service: service, observeKeys: [.deepLAuth]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.deepl.auth_key.title", + key: .deepLAuth + ) + + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.deepl.endpoint.title", + key: .deepLTranslateEndPointKey, + placeholder: "service.configuration.deepl.endpoint.placeholder" + ) + + ServiceConfigurationPickerCell( + titleKey: "service.configuration.deepl.translation.title", + key: .deepLTranslation, + values: DeepLAPIUsagePriority.allCases + ) + } + } +} + +enum DeepLAPIUsagePriority: String, CaseIterable { + case webFirst = "0" + case authKeyFirst = "1" + case authKeyOnly = "2" +} + +extension DeepLAPIUsagePriority: Defaults.Serializable {} + +extension DeepLAPIUsagePriority: EnumLocalizedStringConvertible { + var title: String { + switch self { + case .webFirst: + return NSLocalizedString("service.configuration.deepl.web_first.title", bundle: .main, comment: "") + case .authKeyFirst: + return NSLocalizedString("service.configuration.deepl.authkey_first.title", bundle: .main, comment: "") + case .authKeyOnly: + return NSLocalizedString("service.configuration.deepl.authkey_only.title", bundle: .main, comment: "") + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift new file mode 100644 index 000000000..3d0cdc9df --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// GeminiService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension GeminiService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.geminiAPIKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.gemini.api_key.title", + key: .geminiAPIKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift new file mode 100644 index 000000000..db033ff54 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// NiuTransTranslate+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension EZNiuTransTranslate: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.niuTransAPIKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.niutrans.api_key.title", + key: .niuTransAPIKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift new file mode 100644 index 000000000..091631ed8 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift @@ -0,0 +1,104 @@ +// +// OpenAIService+ConfigurableService.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/14. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension EZOpenAIService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.openAIAPIKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.openai.api_key.title", + key: .openAIAPIKey, + placeholder: "service.configuration.openai.api_key.placeholder" + ) + // endpoint + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.openai.endpoint.title", + key: .openAIEndPoint, + placeholder: "service.configuration.openai.endpoint.placeholder" + ) + // model + ServiceConfigurationPickerCell( + titleKey: "service.configuration.openai.model.title", + key: .openAIModel, + values: OpenAIModels.allCases + ) + + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.translation.title", + key: .openAITranslation + ) + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.sentence.title", + key: .openAISentence + ) + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.dictionary.title", + key: .openAIDictionary + ) + ServiceConfigurationPickerCell( + titleKey: "service.configuration.openai.usage_status.title", + key: .openAIServiceUsageStatus, + values: OpenAIUsageStats.allCases + ) + } + } +} + +protocol EnumLocalizedStringConvertible { + var title: String { get } +} + +enum OpenAIModels: String, CaseIterable { + case gpt3_5_turbo_0125 = "gpt-3.5-turbo-0125" + case gpt4_0125_preview = "gpt-4-0125-preview" +} + +extension OpenAIModels: EnumLocalizedStringConvertible { + var title: String { + rawValue + } +} + +extension OpenAIModels: Defaults.Serializable {} + +enum OpenAIUsageStats: String, CaseIterable { + case `default` = "0" + case alwaysOff = "1" + case alwaysOn = "2" +} + +extension OpenAIUsageStats: EnumLocalizedStringConvertible { + var title: String { + switch self { + case .default: + return NSLocalizedString( + "service.configuration.openai.usage_status_default.title", + bundle: .main, + comment: "" + ) + case .alwaysOff: + return NSLocalizedString( + "service.configuration.openai.usage_status_always_off.title", + bundle: .main, + comment: "" + ) + case .alwaysOn: + return NSLocalizedString( + "service.configuration.openai.usage_status_always_on.title", + bundle: .main, + comment: "" + ) + } + } +} + +extension OpenAIUsageStats: Defaults.Serializable {} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/TencentService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/TencentService+ConfigurableService.swift new file mode 100644 index 000000000..032067baa --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/TencentService+ConfigurableService.swift @@ -0,0 +1,27 @@ +// +// TencentService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension TencentService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.tencentSecretId, .tencentSecretKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.tencent.secret_id.title", + key: .tencentSecretId + ) + + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.tencent.secret_key.title", + key: .tencentSecretKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/ShowWindowPositionExtensions.swift b/Easydict/NewApp/Utility/Extensions/ShowWindowPositionExtensions.swift new file mode 100644 index 000000000..e531d64c1 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/ShowWindowPositionExtensions.swift @@ -0,0 +1,34 @@ +// +// ShowWindowPositionExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension EZShowWindowPosition: Defaults.Serializable {} + +extension EZShowWindowPosition: CaseIterable { + public static let allCases: [EZShowWindowPosition] = [.right, .mouse, .former, .center] +} + +@available(macOS 13, *) +extension EZShowWindowPosition: CustomLocalizedStringResourceConvertible { + public var localizedStringResource: LocalizedStringResource { + switch self { + case .right: + "fixed_window_position_right" + case .mouse: + "fixed_window_position_mouse" + case .former: + "fixed_window_position_former" + case .center: + "fixed_window_position_center" + @unknown default: + "unknown_option" + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/String/String+Regex.swift b/Easydict/NewApp/Utility/Extensions/String/String+Regex.swift new file mode 100644 index 000000000..0acccba89 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/String/String+Regex.swift @@ -0,0 +1,26 @@ +// +// String+Regex.swift +// Easydict +// +// Created by tisfeng on 2024/1/26. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension String { + func extract(withPattern pattern: String) -> String? { + do { + let regex = try NSRegularExpression(pattern: pattern) + let range = NSRange(location: 0, length: utf16.count) + if let match = regex.firstMatch(in: self, options: [], range: range) { + if let range = Range(match.range(at: 1), in: self) { + return String(self[range]) + } + } + } catch { + print("Invalid regex: \(error.localizedDescription)") + } + return nil + } +} diff --git a/Easydict/NewApp/Utility/Extensions/WindowTypeExtensions.swift b/Easydict/NewApp/Utility/Extensions/WindowTypeExtensions.swift new file mode 100644 index 000000000..34b60c049 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/WindowTypeExtensions.swift @@ -0,0 +1,34 @@ +// +// WindowTypeExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension EZWindowType: Defaults.Serializable {} + +public extension EZWindowType { + static let availableOptions: [EZWindowType] = [.mini, .fixed] +} + +@available(macOS 13, *) +extension EZWindowType: CustomLocalizedStringResourceConvertible { + public var localizedStringResource: LocalizedStringResource { + switch self { + case .fixed: + "fixed_window" + case .main: + "main_window" + case .mini: + "mini_window" + case .none: + "none_window" + @unknown default: + "unknown_option" + } + } +} diff --git a/Easydict/NewApp/Utility/GlobalContext.swift b/Easydict/NewApp/Utility/GlobalContext.swift new file mode 100644 index 000000000..557c22ae1 --- /dev/null +++ b/Easydict/NewApp/Utility/GlobalContext.swift @@ -0,0 +1,43 @@ +// +// GlobalContext.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/25. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import Sparkle + +@objcMembers +class GlobalContext: NSObject { + static let shared = GlobalContext() + + let updaterController: SPUStandardUpdaterController + + private let updaterHelper: SPUUpdaterHelper + private let userDriverHelper: SPUUserDriverHelper + + override init() { + updaterHelper = SPUUpdaterHelper() + userDriverHelper = SPUUserDriverHelper() + + updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterHelper, userDriverDelegate: userDriverHelper) + } + + class SPUUpdaterHelper: NSObject, SPUUpdaterDelegate { + func feedURLString(for _: SPUUpdater) -> String? { + var feedURLString = "https://raw.githubusercontent.com/tisfeng/Easydict/main/appcast.xml" + #if DEBUG + feedURLString = "http://localhost:8000/appcast.xml" + #endif + return feedURLString + } + } + + class SPUUserDriverHelper: NSObject, SPUStandardUserDriverDelegate { + var supportsGentleScheduledUpdateReminders: Bool { + true + } + } +} diff --git a/Easydict/NewApp/Utility/Protocol/ConfigurableService.swift b/Easydict/NewApp/Utility/Protocol/ConfigurableService.swift new file mode 100644 index 000000000..058151a5c --- /dev/null +++ b/Easydict/NewApp/Utility/Protocol/ConfigurableService.swift @@ -0,0 +1,29 @@ +// +// ConfigurableService.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/14. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +/// A service can provide configuration view in setting +protocol ConfigurableService { + associatedtype T: View + + /// Items in Configuration Form. Use ServiceStringConfigurationSection or other customize view. + @ViewBuilder + func configurationListItems() -> T +} + +@available(macOS 13.0, *) +extension ConfigurableService { + func configurationView() -> some View { + Form { + configurationListItems() + } + .formStyle(.grouped) + } +} diff --git a/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift new file mode 100644 index 000000000..1a8f94f7e --- /dev/null +++ b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift @@ -0,0 +1,30 @@ +// +// ServiceSecretConfigreValidatable.swift +// Easydict +// +// Created by phlpsong on 2024/1/30. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +protocol ServiceSecretConfigreValidatable { + func validate(completion: @escaping (EZQueryResult, Error?) -> Void) +} + +extension ServiceSecretConfigreValidatable { + func validate(completion _: @escaping (EZQueryResult, Error?) -> Void) {} +} + +extension QueryService: ServiceSecretConfigreValidatable { + func validate(completion: @escaping (EZQueryResult, Error?) -> Void) { + resetServiceResult() + /** + To reduce output text, save cost, a simple translation example is enough. + + 1. use zh -> en to avoid analyze English sentence. + 2. if Chinese text length > 5, it won't query dict. + */ + translate("曾经沧海难为水", from: .simplifiedChinese, to: .english, completion: completion) + } +} diff --git a/Easydict/NewApp/View/MenuItemView.swift b/Easydict/NewApp/View/MenuItemView.swift index 806abc807..404b691bb 100644 --- a/Easydict/NewApp/View/MenuItemView.swift +++ b/Easydict/NewApp/View/MenuItemView.swift @@ -6,18 +6,48 @@ // Copyright © 2023 izual. All rights reserved. // +import SettingsAccess import Sparkle import SwiftUI +import ZipArchive + +@available(macOS 13, *) +final class MenuItemStore: ObservableObject { + @Published var canCheckForUpdates = false + + init() { + Configuration.shared.updater + .publisher(for: \.canCheckForUpdates) + .assign(to: &$canCheckForUpdates) + } +} @available(macOS 13, *) struct MenuItemView: View { + @ObservedObject private var store = MenuItemStore() + var body: some View { + // ️.menuBarExtraStyle为 .menu 时某些控件可能会失效 ,只能显示内容(按照菜单项高度、图像以 template 方式渲染)无法交互 ,比如 Stepper、Slider 等,像基本的 Button、Text、Divider、Image 等还是能正常显示的。 + // Button 和Label的systemImage是不会渲染的 Group { versionItem Divider() + inputItem + .keyboardShortcut(.inputTranslate) + screenshotItem + .keyboardShortcut(.snipTranslate) + selectWordItem + .keyboardShortcut(.selectTranslate) + miniWindowItem + .keyboardShortcut(.showMiniWindow) + Divider() + ocrItem + .keyboardShortcut(.silentScreenshotOcr) + Divider() settingItem .keyboardShortcut(.init(",")) checkUpdateItem + helpItem Divider() quitItem .keyboardShortcut(.init("q")) @@ -50,7 +80,7 @@ struct MenuItemView: View { } private var versionString: String { - let defaultLabel = "Easydict \(currentVersion)" + let defaultLabel = "Easydict \(currentVersion)" if let latestVersion, currentVersion.compare(latestVersion, options: .numeric) == .orderedAscending { @@ -63,21 +93,98 @@ struct MenuItemView: View { @ViewBuilder private var settingItem: some View { if #available(macOS 14.0, *) { - SettingsLink() + SettingsLink { + Text("Settings...") + } preAction: { + NSLog("打开设置") + NSApp.activate(ignoringOtherApps: true) + } postAction: { + // nothing to do + } } else { Button("Settings...") { NSLog("打开设置") + NSApp.activate(ignoringOtherApps: true) NSApplication.shared.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) } } } + // MARK: - List of functions + + @ViewBuilder + private var inputItem: some View { + Button { + NSLog("输入翻译") + EZWindowManager.shared().inputTranslate() + } label: { + HStack { + Image(systemName: "keyboard") + Text("menu_input_translate") + } + } + } + + @ViewBuilder + private var screenshotItem: some View { + Button { + NSLog("截图翻译") + EZWindowManager.shared().snipTranslate() + } label: { + HStack { + Image(systemName: "camera.viewfinder") + Text("menu_screenshot_Translate") + } + } + } + + @ViewBuilder + private var selectWordItem: some View { + Button { + NSLog("划词翻译") + EZWindowManager.shared().selectTextTranslate() + } label: { + HStack { + Image(systemName: "highlighter") + Text("menu_selectWord_Translate") + } + } + } + + @ViewBuilder + private var miniWindowItem: some View { + Button { + NSLog("显示迷你窗口") + EZWindowManager.shared().showMiniFloatingWindow() + } label: { + HStack { + Image(systemName: "dock.rectangle") + Text("menu_show_mini_window") + } + } + } + + @ViewBuilder + private var ocrItem: some View { + Button { + NSLog("静默截图OCR") + EZWindowManager.shared().screenshotOCR() + } label: { + HStack { + Image(systemName: "camera.metering.spot") + Text("menu_silent_screenshot_OCR") + } + } + } + + // MARK: - Setting + @ViewBuilder private var checkUpdateItem: some View { Button("check_updates") { NSLog("检查更新") - SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).checkForUpdates(nil) - } + Configuration.shared.updater.checkForUpdates() + }.disabled(!store.canCheckForUpdates) } @ViewBuilder @@ -87,6 +194,43 @@ struct MenuItemView: View { NSApplication.shared.terminate(nil) } } + + @ViewBuilder + private var helpItem: some View { + Menu("Help") { + Button("Feedback") { + guard let versionURL = URL(string: "\(EZGithubRepoEasydictURL)/issues") else { + return + } + openURL(versionURL) + } + Button("Export Log") { + exportLogAction() + } + Button("Log Directory") { + NSLog("日志目录") + let logPath = MMManagerForLog.rootLogDirectory() ?? "" + let directoryURL = URL(fileURLWithPath: logPath) + NSWorkspace.shared.open(directoryURL) + } + } + } + + private func exportLogAction() { + NSLog("导出日志") + let logPath = MMManagerForLog.rootLogDirectory() ?? "" + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH-mm-ss-SSS" + let dataString = dateFormatter.string(from: Date()) + let downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] + let zipPath = downloadDirectory.appendingPathComponent("Easydict log \(dataString).zip").path(percentEncoded: false) + let success = SSZipArchive.createZipFile(atPath: zipPath, withContentsOfDirectory: logPath, keepParentDirectory: false) + if success { + NSWorkspace.shared.selectFile(zipPath, inFileViewerRootedAtPath: "") + } else { + MMLogInfo("导出日志失败") + } + } } @available(macOS 13, *) diff --git a/Easydict/NewApp/View/MenuView/MainMenuCommand.swift b/Easydict/NewApp/View/MenuView/MainMenuCommand.swift new file mode 100644 index 000000000..388caacd3 --- /dev/null +++ b/Easydict/NewApp/View/MenuView/MainMenuCommand.swift @@ -0,0 +1,16 @@ +// +// MainMenuCommand.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +struct EasyDictMainMenu: Commands { + var body: some Commands { + // shortcut + MainMenuShortcutCommand() + } +} diff --git a/Easydict/NewApp/View/MenuView/MainMenuShortcutCommand.swift b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommand.swift new file mode 100644 index 000000000..82ff53423 --- /dev/null +++ b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommand.swift @@ -0,0 +1,40 @@ +// +// MainMenuShortcutCommand.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +extension EasyDictMainMenu { + struct MainMenuShortcutCommand: Commands { + @State private var appShortcutCommandList = [ + MainMenuShortcutCommandDataItem(type: .clearInput), + MainMenuShortcutCommandDataItem(type: .clearAll), + MainMenuShortcutCommandDataItem(type: .copy), + MainMenuShortcutCommandDataItem(type: .copyFirstResult), + MainMenuShortcutCommandDataItem(type: .focus), + MainMenuShortcutCommandDataItem(type: .play), + MainMenuShortcutCommandDataItem(type: .retry), + MainMenuShortcutCommandDataItem(type: .toggle), + MainMenuShortcutCommandDataItem(type: .pin), + MainMenuShortcutCommandDataItem(type: .hide), + MainMenuShortcutCommandDataItem(type: .increaseFontSize), + MainMenuShortcutCommandDataItem(type: .decreaseFontSize), + MainMenuShortcutCommandDataItem(type: .google), + MainMenuShortcutCommandDataItem(type: .eudic), + MainMenuShortcutCommandDataItem(type: .appleDic), + ] + + var body: some Commands { + // shortcut Commands + CommandMenu("shortcut") { + ForEach(appShortcutCommandList) { item in + MainMenuShortcutCommandItem(dataItem: item) + } + } + } + } +} diff --git a/Easydict/NewApp/View/MenuView/MainMenuShortcutCommandItem.swift b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommandItem.swift new file mode 100644 index 000000000..669057c9f --- /dev/null +++ b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommandItem.swift @@ -0,0 +1,61 @@ +// +// MainMenuShortcutCommandItem.swift +// Easydict +// +// Created by Sharker on 2024/2/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +struct MainMenuShortcutCommandDataItem: Identifiable { + public var id: String { type.localizedStringKey() } + var type: ShortcutType +} + +struct MainMenuShortcutCommandItem: View { + public var dataItem: MainMenuShortcutCommandDataItem + + var body: some View { + Button(LocalizedStringKey(dataItem.type.localizedStringKey())) { + switch dataItem.type { + case .clearInput: + Shortcut.shared.clearInput() + case .clearAll: + Shortcut.shared.clearAll() + case .copy: + Shortcut.shared.shortcutCopy() + case .copyFirstResult: + Shortcut.shared.shortcutCopyFirstResult() + case .focus: + Shortcut.shared.shortcutFocus() + case .play: + Shortcut.shared.shortcutPlay() + case .retry: + Shortcut.shared.shortcutRetry() + case .toggle: + Shortcut.shared.shortcutToggle() + case .pin: + Shortcut.shared.shortcutPin() + case .hide: + Shortcut.shared.shortcutHide() + case .increaseFontSize: + Shortcut.shared.increaseFontSize() + case .decreaseFontSize: + Shortcut.shared.decreaseFontSize() + case .google: + Shortcut.shared.shortcutGoogle() + case .eudic: + Shortcut.shared.shortcutEudic() + case .appleDic: + Shortcut.shared.shortcutAppleDic() + default: () + } + } + .keyboardShortcut(dataItem.type) + + if dataItem.type == .toggle || dataItem.type == .decreaseFontSize { + Divider() + } + } +} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index 2e5da7bf3..2d73db6f0 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -8,21 +8,90 @@ import SwiftUI +enum SettingTab: Int { + case general + case service + case disabled + case advanced + case shortcut + case privacy + case about +} + @available(macOS 13, *) struct SettingView: View { + @State private var selection = SettingTab.general + @State private var window: NSWindow? + var body: some View { - TabView { + TabView(selection: $selection) { GeneralTab() .tabItem { Label("setting_general", systemImage: "gear") } - .frame(width: 500, height: 400) + .tag(SettingTab.general) + + ServiceTab() + .tabItem { Label("service", systemImage: "briefcase") } + .tag(SettingTab.service) + + DisabledAppTab() + .tabItem { Label("disabled_app_list", systemImage: "nosign") } + .tag(SettingTab.disabled) + + ShortcutTab() + .tabItem { Label("shortcut", systemImage: "command.square") } + .tag(SettingTab.shortcut) + + AdvancedTab() + .tabItem { Label("advanced", systemImage: "gearshape.2") } + .tag(SettingTab.advanced) PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } - .frame(width: 500, height: 400) + .tag(SettingTab.privacy) + AboutTab() .tabItem { Label("about", systemImage: "info.bubble") } - .frame(width: 500, height: 400) + .tag(SettingTab.about) + } + .background( + WindowAccessor(window: $window.didSet(execute: { _ in + // reset frame when first launch + resizeWindowFrame() + })) + ) + .onChange(of: selection) { _ in + resizeWindowFrame() } } + + func resizeWindowFrame() { + guard let window else { return } + + // Disable zoom button, refer: https://stackoverflow.com/a/66039864/8378840 + window.standardWindowButton(.zoomButton)?.isEnabled = false + + // Keep the settings page windows all the same width to avoid strange animations. + let maxWidth = 780 + let height = switch selection { + case .disabled: + 500 + case .advanced: + 400 + case .privacy: + 320 + case .about: + 450 + default: + maxWidth - 110 + } + + let newSize = CGSize(width: maxWidth, height: height) + + let originalFrame = window.frame + let newY = originalFrame.origin.y + originalFrame.size.height - newSize.height + let newRect = NSRect(origin: CGPoint(x: originalFrame.origin.x, y: newY), size: newSize) + + window.setFrame(newRect, display: true, animate: true) + } } @available(macOS 13, *) diff --git a/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift b/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift index 92de82c24..28e79d37f 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import SwiftUI @available(macOS 13, *) @@ -20,7 +21,7 @@ struct AboutTab: View { .font(.system(size: 26, weight: .semibold)) Text("current_version") + Text(verbatim: " \(version)") .font(.system(size: 14)) - Toggle("auto_check_update", isOn: $autoChecksForUpdates) + Toggle("auto_check_update", isOn: $checkUpdaterViewModel.autoChecksForUpdates) Text(verbatim: "(") + Text("lastest_version") + Text(verbatim: " \(lastestVersion ?? version))") HStack { @@ -34,7 +35,9 @@ struct AboutTab: View { } .padding(.horizontal, 50) .padding(.vertical, 30) + .frame(maxWidth: .infinity) } + .scrollIndicators(.hidden) .task { let version = await EZMenuItemManager.shared().fetchRepoLatestVersion(EZGithubRepoEasydict) await MainActor.run { @@ -43,8 +46,8 @@ struct AboutTab: View { } } - @AppStorage("EZConfiguration_kAutomaticallyChecksForUpdatesKey") - private var autoChecksForUpdates = false + @StateObject private var checkUpdaterViewModel = CheckUpdaterViewModel() + @State private var lastestVersion: String? private var appName: String { @@ -54,6 +57,22 @@ struct AboutTab: View { private var version: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" } + + class CheckUpdaterViewModel: ObservableObject { + private let updater = Configuration.shared.updater + + @Published var autoChecksForUpdates = true { + didSet { + updater.automaticallyChecksForUpdates = autoChecksForUpdates + } + } + + init() { + updater + .publisher(for: \.automaticallyChecksForUpdates) + .assign(to: &$autoChecksForUpdates) + } + } } @available(macOS 13, *) diff --git a/Easydict/NewApp/View/SettingView/Tabs/AdvancedTab.swift b/Easydict/NewApp/View/SettingView/Tabs/AdvancedTab.swift new file mode 100644 index 000000000..55b9da5c3 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/AdvancedTab.swift @@ -0,0 +1,42 @@ +// +// AdvancedTab.swift +// Easydict +// +// Created by tisfeng on 2024/1/23. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import SwiftUI + +@available(macOS 13, *) +struct AdvancedTab: View { + var body: some View { + Form { + Section { + Picker("setting.general.advance.default_tts_service", selection: $defaultTTSServiceType) { + ForEach(TTSServiceType.allCases, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) + } + } + Toggle("setting.general.advance.enable_beta_feature", isOn: $enableBetaFeature) + Toggle(isOn: $enableBetaNewApp) { + Text("enable_beta_new_app") + } + } header: { + Text("setting.general.advance.header") + } + } + .formStyle(.grouped) + } + + @Default(.defaultTTSServiceType) private var defaultTTSServiceType + @Default(.enableBetaFeature) private var enableBetaFeature + @Default(.enableBetaNewApp) private var enableBetaNewApp +} + +@available(macOS 13, *) +#Preview { + AdvancedTab() +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift new file mode 100644 index 000000000..ece9cc335 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift @@ -0,0 +1,247 @@ +// +// DisabledAppTab.swift +// Easydict +// +// Created by phlpsong on 2024/1/15. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import SwiftUI + +private class DisabledAppViewModel: ObservableObject { + @Published var appModelList: [EZAppModel] = [] + @Published var selectedAppModels: Set = [] + @Published var isShowImportErrorAlert = false + @Published var isImporting = false { + didSet { + // https://github.com/tisfeng/Easydict/issues/346 + Configuration.shared.disabledAutoSelect = isImporting + } + } + + init() { + fetchDisabledApps() + } + + func fetchDisabledApps() { + let allAppModelList = EZLocalStorage.shared().selectTextTypeAppModelList + + appModelList = allAppModelList.compactMap { appModel in + let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appModel.appBundleID) + return url == nil ? nil : appModel + } + } + + func saveDisabledApps() { + EZLocalStorage.shared().selectTextTypeAppModelList = appModelList + } + + func removeDisabledApp() { + appModelList = appModelList.filter { !selectedAppModels.contains($0) } + saveDisabledApps() + selectedAppModels = [] + } + + func newAppURLsSelected(from urls: [URL]) { + urls.forEach { url in + let gotAccess = url.startAccessingSecurityScopedResource() + if !gotAccess { return } + appendNewDisabledApp(for: url) + url.stopAccessingSecurityScopedResource() + } + } + + func appendNewDisabledApp(for url: URL) { + guard let selectAppModel = disabledAppModel(from: url) else { return } + guard !appModelList.contains(selectAppModel) else { return } + appModelList.append(selectAppModel) + saveDisabledApps() + } + + func disabledAppModel(from url: URL) -> EZAppModel? { + let appModel = EZAppModel() + guard let bundle = Bundle(url: url) else { return nil } + appModel.appBundleID = bundle.bundleIdentifier ?? "" + appModel.triggerType = [] + return appModel + } +} + +@available(macOS 13.0, *) +struct DisabledAppTab: View { + @StateObject private var disabledAppViewModel = DisabledAppViewModel() + + var listToolbar: some View { + ListToolbar() + .fileImporter( + isPresented: $disabledAppViewModel.isImporting, + allowedContentTypes: [.application], + allowsMultipleSelection: true + ) { result in + switch result { + case let .success(urls): + disabledAppViewModel.newAppURLsSelected(from: urls) + case let .failure(error): + print("fileImporter error: \(error)") + disabledAppViewModel.isShowImportErrorAlert.toggle() + } + } + .alert(isPresented: $disabledAppViewModel.isShowImportErrorAlert) { + Alert(title: Text(""), message: Text("setting.disabled.import_app_error.message"), dismissButton: .default(Text("ok"))) + } + } + + var appListView: some View { + List(selection: $disabledAppViewModel.selectedAppModels) { + ForEach(disabledAppViewModel.appModelList, id: \.self) { app in + BlockAppItemView(with: app) + .tag(app) + } + .listRowSeparator(.hidden) + } + .listStyle(.plain) + .scrollIndicators(.never) + } + + var appListViewWithToolbar: some View { + VStack(spacing: 0) { + appListView + + listToolbar + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay(content: { + RoundedRectangle(cornerRadius: 8) + .stroke(Color("list_border_color"), lineWidth: 0.5) + }) + .padding(.horizontal, 25) + .padding(.bottom, 25) + .onTapGesture { + disabledAppViewModel.selectedAppModels = [] + } + } + + var body: some View { + VStack { + Text("disabled_title") + .padding(.horizontal) + .padding(.top, 18) + .padding(.bottom, 8) + + appListViewWithToolbar + } + .environmentObject(disabledAppViewModel) + } +} + +@available(macOS 13.0, *) +private struct ListToolbar: View { + @EnvironmentObject private var disabledAppViewModel: DisabledAppViewModel + + var body: some View { + VStack(spacing: 0) { + Divider() + HStack(spacing: 0) { + ListButton(systemName: "plus") { + disabledAppViewModel.isImporting.toggle() + } + .disabled(false) + Divider() + .padding(.vertical, 1) + ListButton(systemName: "minus") { + disabledAppViewModel.removeDisabledApp() + } + .disabled(disabledAppViewModel.selectedAppModels.isEmpty) + Spacer() + } + .padding(2) + } + .frame(height: 28) + .background(Color("add_minus_bg_color")) + } +} + +@available(macOS 13.0, *) +private struct ListButton: View { + @Environment(\.isEnabled) private var isEnabled: Bool + var systemName: String + var action: () -> Void + + var body: some View { + Button(action: { + action() + }) { + Image(systemName: systemName) + .resizable() + .scaledToFit() + .frame(width: 10, height: 10) + .padding(.horizontal, 8) + .contentShape(Rectangle()) + .foregroundStyle(isEnabled ? Color(.secondaryLabelColor) : Color(.tertiaryLabelColor)) + .font(.system(size: 14, weight: .semibold)) + } + .buttonStyle(BorderlessButtonStyle()) + } +} + +@available(macOS 13.0, *) +private struct BlockAppItemView: View { + @EnvironmentObject var disabledAppViewModel: DisabledAppViewModel + + @StateObject private var appItemViewModel: AppItemViewModel + + init(with appModel: EZAppModel) { + _appItemViewModel = StateObject(wrappedValue: AppItemViewModel(appModel: appModel)) + } + + var body: some View { + HStack(alignment: .center) { + Image(nsImage: appItemViewModel.appIcon) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + + Text(appItemViewModel.appName) + + Spacer() + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .padding(.vertical, 4) + .padding(.leading, 6) + } +} + +@available(macOS 13.0, *) +private class AppItemViewModel: ObservableObject { + @Published var appIcon = NSImage() + + @Published var appName = "" + + var appModel: EZAppModel + + init(appModel: EZAppModel) { + self.appModel = appModel + getAppBundleInfo() + } + + func getAppBundleInfo() { + let appBundleId = appModel.appBundleID + let workspace = NSWorkspace.shared + let appURL = workspace.urlForApplication(withBundleIdentifier: appBundleId) + guard let appURL else { return } + + let appPath = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appBundleId) + guard let appPath else { return } + appIcon = workspace.icon(forFile: appPath.path(percentEncoded: false)) + + guard let appBundle = Bundle(url: appURL) else { return } + appName = appBundle.applicationName + } +} + +@available(macOS 13.0, *) +#Preview { + DisabledAppTab() +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift index e9368eaea..cfbf2f2dd 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift @@ -6,51 +6,281 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import SwiftUI @available(macOS 13, *) struct GeneralTab: View { + @Environment(\.colorScheme) var colorScheme + var body: some View { Form { Section { - HStack { - Text("show_main_window") - Toggle(isOn: $hideMainWindow) { - Text("hide_main_window") + Picker("setting.general.appearance.light_dark_appearance", selection: $appearanceType) { + ForEach(AppearenceType.allCases, id: \.rawValue) { option in + Text(option.title) + .tag(option) } } - HStack { - Text("launch") - Toggle(isOn: $launchAtStartup) { - Text("launch_at_startup") + } header: { + Text("setting.general.appearance.header") + } + Section { + FirstAndSecondLanguageSettingView() + Picker("setting.general.language.language_detect_optimize", selection: $languageDetectOptimize) { + ForEach(LanguageDetectOptimize.allCases, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) } } - HStack { - Text("menu_bar_icon") - Toggle(isOn: $hideMenuBarIcon) { - Text("hide_menu_bar_icon") + } header: { + Text("setting.general.language.header") + } + + Section { + Toggle("auto_show_query_icon", isOn: $autoSelectText) + Toggle("force_auto_get_selected_text", isOn: $forceAutoGetSelectedText) + Toggle("click_icon_query_info", isOn: $clickQuery) + Toggle("setting.general.mouse_query.adjust_pop_button_origin", isOn: $adjustPopButtonOrigin) // 调整查询图标位置: + } header: { + Text("setting.general.mouse_query.header") + } + + Section { + Toggle("setting.general.voice.disable_empty_copy_beep_msg", isOn: $disableEmptyCopyBeep) // 禁用提示音:划词内容为空时生效 + Toggle("setting.general.voice.auto_play_word_audio", isOn: $autoPlayAudio) // 查询英语单词后自动播放发音 + } header: { + Text("setting.general.voice.header") + } + + Section { + Picker("setting.general.window.mouse_select_translate_window_type", selection: $mouseSelectTranslateWindowType) { + ForEach(EZWindowType.availableOptions, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) + } + } + Picker("setting.general.window.shortcut_select_translate_window_type", selection: $shortcutSelectTranslateWindowType) { + ForEach(EZWindowType.availableOptions, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) } } - HStack { - Text("beta_new_app") - Toggle(isOn: $enableBetaNewApp) { - Text("enable_beta_new_app") + Picker("setting.general.window.fixed_window_position", selection: $fixedWindowPosition) { + ForEach(EZShowWindowPosition.allCases, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) } } } header: { - Text("other") + Text("setting.general.windows.header") + } + + Section { + Toggle("clear_input_when_translating", isOn: $clearInput) + Toggle("keep_prev_result_when_selected_text_is_empty", isOn: $keepPrevResultWhenEmpty) + Toggle("select_query_text_when_window_activate", isOn: $selectQueryTextWhenWindowActivate) + } header: { + Text("setting.general.input.header") + } + + Section { + Toggle("auto_query_ocr_text", isOn: $autoQueryOCRText) + Toggle("auto_query_selected_text", isOn: $autoQuerySelectedText) + Toggle("auto_query_pasted_text", isOn: $autoQueryPastedText) + } header: { + Text("setting.general.auto_query.header") + } + + Section { + Toggle("auto_copy_ocr_text", isOn: $autoCopyOCRText) + Toggle("auto_copy_selected_text", isOn: $autoCopySelectedText) + Toggle("auto_copy_first_translated_text", isOn: $autoCopyFirstTranslatedText) + } header: { + Text("setting.general.auto_copy.header") + } + + Section { + Toggle("show_google_quick_link", isOn: $showGoogleQuickLink) + Toggle("show_eudic_quick_link", isOn: $showEudicQuickLink) + Toggle("show_apple_dictionary_quick_link", isOn: $showAppleDictionaryQuickLink) + } header: { + Text("setting.general.quick_link.header") + } + + Section { + let bindingFontSize = Binding(get: { + Double(fontSizeOptionIndex) + }, set: { newValue in + fontSizeOptionIndex = UInt(newValue) + }) + Slider(value: bindingFontSize, in: 0.0 ... 4.0, step: 1) { + Text("setting.general.font.font_size.label") + } minimumValueLabel: { + Text("small") + .font(.system(size: 10)) + } maximumValueLabel: { + Text("large") + .font(.system(size: 14)) + } + } header: { + Text("setting.general.font.header") + } footer: { + Text("hints_keyboard_shortcuts_font_size") + .font(.footnote) + } + + Section { + Toggle(isOn: $launchAtStartup) { + Text("launch_at_startup") + } + Toggle(isOn: $hideMainWindow) { + Text("hide_main_window") + } + Toggle(isOn: $hideMenuBarIcon) { + Text("hide_menu_bar_icon") + } + Picker( + "modify_menubar_icon", + selection: $selectedMenuBarIcon + ) { + ForEach(MenuBarIconType.allCases) { option in + Image(option.rawValue) + .renderingMode(.template) + .foregroundStyle(.primary) + } + } + } header: { + Text("setting.general.other.header") } } .formStyle(.grouped) } - @AppStorage(kHideMainWindowKey) private var hideMainWindow = false - @AppStorage(kLaunchAtStartupKey) private var launchAtStartup = false - @AppStorage(kHideMenuBarIconKey) private var hideMenuBarIcon = false - @AppStorage(kEnableBetaNewAppKey) private var enableBetaNewApp = false + @Default(.autoSelectText) private var autoSelectText + @Default(.forceAutoGetSelectedText) private var forceAutoGetSelectedText + @Default(.clickQuery) private var clickQuery + @Default(.adjustPopButtonOrigin) private var adjustPopButtonOrigin + + @Default(.clearInput) private var clearInput + @Default(.keepPrevResultWhenEmpty) private var keepPrevResultWhenEmpty + @Default(.selectQueryTextWhenWindowActivate) private var selectQueryTextWhenWindowActivate + + @Default(.disableEmptyCopyBeep) private var disableEmptyCopyBeep + @Default(.autoPlayAudio) private var autoPlayAudio + + @Default(.autoQueryOCRText) private var autoQueryOCRText + @Default(.autoQuerySelectedText) private var autoQuerySelectedText + @Default(.autoQueryPastedText) private var autoQueryPastedText + + @Default(.autoCopyOCRText) private var autoCopyOCRText + @Default(.autoCopySelectedText) private var autoCopySelectedText + @Default(.autoCopyFirstTranslatedText) private var autoCopyFirstTranslatedText + + @Default(.showGoogleQuickLink) private var showGoogleQuickLink + @Default(.showEudicQuickLink) private var showEudicQuickLink + @Default(.showAppleDictionaryQuickLink) private var showAppleDictionaryQuickLink + + @Default(.hideMainWindow) private var hideMainWindow + @Default(.launchAtStartup) private var launchAtStartup + @Default(.hideMenuBarIcon) private var hideMenuBarIcon + @Default(.enableBetaNewApp) private var enableBetaNewApp + + @Default(.languageDetectOptimize) private var languageDetectOptimize + @Default(.defaultTTSServiceType) private var defaultTTSServiceType + + @Default(.fixedWindowPosition) private var fixedWindowPosition + @Default(.mouseSelectTranslateWindowType) private var mouseSelectTranslateWindowType + @Default(.shortcutSelectTranslateWindowType) private var shortcutSelectTranslateWindowType + @Default(.enableBetaFeature) private var enableBetaFeature + @Default(.appearanceType) private var appearanceType + + @Default(.fontSizeOptionIndex) private var fontSizeOptionIndex + @Default(.selectedMenuBarIcon) private var selectedMenuBarIcon } @available(macOS 13, *) #Preview { GeneralTab() } + +@available(macOS 13, *) +private struct FirstAndSecondLanguageSettingView: View { + var body: some View { + Group { + Picker("setting.general.language.first_language", selection: $firstLanguage) { + ForEach(Language.allAvailableOptions, id: \.rawValue) { option in + Text(verbatim: "\(option.flagEmoji) \(option.localizedName)") + .tag(option) + } + } + Picker("setting.general.language.second_language", selection: $secondLanguage) { + ForEach(Language.allAvailableOptions, id: \.rawValue) { option in + Text(verbatim: "\(option.flagEmoji) \(option.localizedName)") + .tag(option) + } + } + } + .onChange(of: firstLanguage) { [firstLanguage] newValue in + let oldValue = firstLanguage + if newValue == secondLanguage { + secondLanguage = oldValue + languageDuplicatedAlert = .init(duplicatedLanguage: newValue, setField: .second, setLanguage: oldValue) + } + } + .onChange(of: secondLanguage) { [secondLanguage] newValue in + let oldValue = secondLanguage + if newValue == firstLanguage { + firstLanguage = oldValue + languageDuplicatedAlert = .init(duplicatedLanguage: newValue, setField: .first, setLanguage: oldValue) + } + } + .alert("setting.general.language.duplicated_alert.title", isPresented: showLanguageDuplicatedAlert, presenting: languageDuplicatedAlert) { _ in + + } message: { alert in + Text(alert.description) + } + } + + @Default(.firstLanguage) private var firstLanguage + @Default(.secondLanguage) private var secondLanguage + + private struct LanguageDuplicateAlert: CustomStringConvertible { + var description: String { + // First language should not be same as second language. (\(duplicatedLanguage)) + // \(setField) is replaced with \(setLanguage). + String(localized: "setting.general.language.duplicated_alert \(duplicatedLanguage.localizedName)\(String(localized: setField.localizedStringResource))\(setLanguage.localizedName)") + } + + let duplicatedLanguage: Language + + let setField: Field + + let setLanguage: Language + + enum Field: CustomLocalizedStringResourceConvertible { + var localizedStringResource: LocalizedStringResource { + switch self { + case .first: + "setting.general.language.duplicated_alert.field.first" + case .second: + "setting.general.language.duplicated_alert.field.second" + } + } + + case first + case second + } + } + + @State private var languageDuplicatedAlert: LanguageDuplicateAlert? + private var showLanguageDuplicatedAlert: Binding { + .init { + languageDuplicatedAlert != nil + } set: { newValue in + if !newValue { + languageDuplicatedAlert = nil + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift b/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift index 1cb7714e0..b51403174 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import SwiftUI @available(macOS 13, *) @@ -33,11 +34,8 @@ struct PrivacyTab: View { .formStyle(.grouped) } - @AppStorage("EZConfiguration_kAllowCrashLogKey") - private var allowCollectCrashLog = true - - @AppStorage("EZConfiguration_kAllowAnalyticsKey") - private var allowCollectAnalytics = true + @Default(.allowCrashLog) private var allowCollectCrashLog + @Default(.allowAnalytics) private var allowCollectAnalytics } @available(macOS 13, *) diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/SecureTextField.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/SecureTextField.swift new file mode 100644 index 000000000..d2632a544 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/SecureTextField.swift @@ -0,0 +1,83 @@ +// +// SecureTextField.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13.0, *) +struct SecureTextField: View { + let title: LocalizedStringKey + let placeholder: LocalizedStringKey + + @Binding var text: String? + + @State private var showText: Bool = false + + private enum Focus { + case secure, text + } + + @FocusState private var focus: Focus? + + @Environment(\.scenePhase) private var scenePhase + @Environment(\.lineLimit) private var lineLimit + + var body: some View { + HStack { + ZStack { + SecureField(title, text: $text ?? "") + .lineLimit(lineLimit) + .focused($focus, equals: .secure) + .opacity(showText ? 0 : 1) + TextField(title, text: $text ?? "", prompt: Text(placeholder)) + .lineLimit(lineLimit) + .focused($focus, equals: .text) + .opacity(showText || (text?.isEmpty ?? true) ? 1 : 0) + } + + Button(action: { + showText.toggle() + }) { + Image(systemName: showText ? "eye.slash.fill" : "eye.fill") + } + } + .onChange(of: focus) { newValue in + // if the PasswordField is focused externally, then make sure the correct field is actually focused + if newValue != nil { + focus = showText ? .text : .secure + } + } + .onChange(of: scenePhase) { newValue in + if newValue != .active { + showText = false + } + } + .onChange(of: showText) { newValue in + if focus != nil { // Prevents stealing focus to this field if another field is focused, or nothing is focused + DispatchQueue.main.async { // Needed for general iOS 16 bug with focus + focus = newValue ? .text : .secure + } + } + } + } +} + +@available(macOS 13.0, *) +struct SecureInput_Previews: PreviewProvider { + static var previews: some View { + Group { + SecureTextField(title: "caiyun_translate", placeholder: "service.configuration.input.placeholder", text: .constant("1234567")) + .padding() + .previewLayout(.fixed(width: 400, height: 100)) + + SecureTextField(title: "caiyun_translate", placeholder: "service.configuration.input.placeholder", text: .constant("")) + .padding() + .preferredColorScheme(.dark) + .previewLayout(.fixed(width: 400, height: 100)) + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift new file mode 100644 index 000000000..83fa05c33 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift @@ -0,0 +1,126 @@ +// +// ServiceConfigurationCells.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import SwiftUI + +@available(macOS 13.0, *) +struct ServiceConfigurationSecureInputCell: View { + @Default var value: String? + let textFieldTitleKey: LocalizedStringKey + let placeholder: LocalizedStringKey + + init( + textFieldTitleKey: LocalizedStringKey, + key: Defaults.Key, + placeholder: LocalizedStringKey = "service.configuration.input.placeholder" + ) { + self.textFieldTitleKey = textFieldTitleKey + self.placeholder = placeholder + _value = .init(key) + } + + var body: some View { + SecureTextField(title: textFieldTitleKey, placeholder: placeholder, text: $value) + } +} + +@available(macOS 13.0, *) +struct ServiceConfigurationInputCell: View { + @Default var value: String? + let textFieldTitleKey: LocalizedStringKey + let placeholder: LocalizedStringKey + + init(textFieldTitleKey: LocalizedStringKey, key: Defaults.Key, placeholder: LocalizedStringKey) { + self.textFieldTitleKey = textFieldTitleKey + self.placeholder = placeholder + _value = .init(key) + } + + var body: some View { + TextField(textFieldTitleKey, text: $value ?? "", prompt: Text(placeholder)) + .padding(10.0) + } +} + +@available(macOS 13.0, *) +struct ServiceConfigurationPickerCell: View { + @Default var value: T + let titleKey: LocalizedStringKey + let values: [T] + + init(titleKey: LocalizedStringKey, key: Defaults.Key, values: [T]) { + self.titleKey = titleKey + self.values = values + _value = .init(key) + } + + var body: some View { + Picker(titleKey, selection: $value) { + ForEach(values, id: \.self) { value in + Text(value.title) + } + } + .padding(10.0) + } +} + +class ConfigurationToggleViewModel: ObservableObject { + @Published var isOn = false +} + +@available(macOS 13.0, *) +struct ServiceConfigurationToggleCell: View { + @Default var value: String + let titleKey: LocalizedStringKey + + @ObservedObject var viewModel = ConfigurationToggleViewModel() + + init(titleKey: LocalizedStringKey, key: Defaults.Key) { + self.titleKey = titleKey + _value = .init(key) + viewModel.isOn = value == "1" + } + + var body: some View { + Toggle(titleKey, isOn: $viewModel.isOn) + .padding(10.0) + .onChange(of: viewModel.isOn) { newValue in + value = newValue ? "1" : "0" + } + } +} + +@available(macOS 13.0, *) +#Preview { + Group { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.openai.api_key.title", + key: .openAIAPIKey, + placeholder: "service.configuration.openai.api_key.placeholder" + ) + + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.openai.endpoint.title", + key: .openAIEndPoint, + placeholder: "service.configuration.openai.endpoint.placeholder" + ) + + // model + ServiceConfigurationPickerCell( + titleKey: "service.configuration.openai.model.title", + key: .openAIModel, + values: OpenAIModels.allCases + ) + + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.translation.title", + key: .openAITranslation + ) + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift new file mode 100644 index 000000000..7827063b3 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift @@ -0,0 +1,123 @@ +// +// ServiceConfigurationSecretSectionView.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import Defaults +import SwiftUI + +@available(macOS 13.0, *) +struct ServiceConfigurationSecretSectionView: View { + var service: QueryService + let content: Content + + @StateObject private var viewModel: ServiceValidationViewModel + + init( + service: QueryService, + observeKeys: [Defaults.Key], + @ViewBuilder content: () -> Content + ) { + self.service = service + self.content = content() + _viewModel = .init(wrappedValue: ServiceValidationViewModel(observing: observeKeys)) + } + + var header: some View { + HStack(alignment: .lastTextBaseline) { + Text(service.name()) + Spacer() + } + } + + var footer: some View { + Button { + validate() + } label: { + Group { + if viewModel.isValidating { + ProgressView() + .controlSize(.small) + .progressViewStyle(.circular) + } else { + Text("service.configuration.validate") + } + } + } + .disabled(viewModel.isValidateBtnDisabled) + } + + var body: some View { + Section { + content + } header: { + header + } footer: { + footer + } + .alert(viewModel.alertMessage, isPresented: $viewModel.isAlertPresented, actions: { + Button("ok") { + viewModel.reset() + } + }) + } + + func validate() { + viewModel.isValidating.toggle() + service.validate { _, error in + DispatchQueue.main.async { + guard viewModel.isValidating else { return } + viewModel.isValidating = false + viewModel.alertMessage = error == nil ? "service.configuration.validation_success" : "service.configuration.validation_fail" + print("\(service.serviceType()) validate \(error == nil ? "success" : "fail")!") + viewModel.isAlertPresented = true + } + } + } +} + +@MainActor +private class ServiceValidationViewModel: ObservableObject { + @Published var isAlertPresented = false + + @Published var isValidating = false + + @Published var alertMessage: LocalizedStringKey = "" + + @Published var isValidateBtnDisabled = false + + var cancellables: [AnyCancellable] = [] + + init(observing keys: [Defaults.Key]) { + cancellables.append( + // check secret key empty input + Defaults.publisher(keys: keys) + .sink { [weak self] _ in + let hasEmptyInput = keys.contains(where: { (Defaults[$0] ?? "").isEmpty }) + DispatchQueue.main.async { + self?.isValidateBtnDisabled = hasEmptyInput + } + } + ) + } + + func reset() { + isValidating = false + alertMessage = "" + isAlertPresented = false + } +} + +@available(macOS 13.0, *) +#Preview { + ServiceConfigurationSecretSectionView(service: EZBingService(), observeKeys: [.bingCookieKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.bing.cookie.title", + key: .bingCookieKey + ) + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift new file mode 100644 index 000000000..98a36f433 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift @@ -0,0 +1,85 @@ +// +// ServiceConfigurationSection.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/14. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import SwiftUI + +@available(macOS 12.0, *) +struct ServiceStringConfigurationSection: View { + /// Title of text field + let textFieldTitleKey: LocalizedStringKey + /// Header of section. If there is no need to add an header, just leave empty string + let headerTitleKey: LocalizedStringKey + /// Defaults key for configuration. Please refer to `Configuration` - `Configuration` + let key: Defaults.Key + /// Prompt of text field + let prompt: LocalizedStringKey + /// Footer of section. Add comments, footnotes or links to describe the field. + @ViewBuilder let footer: () -> F + + var body: some View { + ServiceConfigurationSection( + headerTitleKey, + key: key, + field: { value in + let value = Binding.init { + value.wrappedValue ?? "" + } set: { newValue in + value.wrappedValue = newValue.trimmingCharacters(in: .whitespaces) + } + TextField(textFieldTitleKey, text: value, prompt: Text(prompt)) + .lineLimit(1) + }, + footer: footer + ) + } +} + +@available(macOS 12.0, *) +struct ServiceConfigurationSection: View { + @Default var value: T + + init( + _ titleKey: LocalizedStringKey, + key: Defaults.Key, + @ViewBuilder field: @escaping (Binding) -> V, + footer: (() -> F)? + ) { + self.titleKey = titleKey + _value = .init(key) + self.footer = footer + self.field = field + } + + let field: (Binding) -> V + let footer: (() -> F)? + + let titleKey: LocalizedStringKey + + var body: some View { + Section { + field($value) + } header: { + HStack(alignment: .lastTextBaseline) { + Text(titleKey) + Spacer() + Button("service.configuration.reset") { + _value.reset() + } + .buttonStyle(.plain) + .foregroundStyle(Color.accentColor) + .font(.footnote) + } + } footer: { + if let footer { + footer() + .font(.footnote) + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift new file mode 100644 index 000000000..77a0f5a1c --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift @@ -0,0 +1,183 @@ +// +// ServiceTab.swift +// Easydict +// +// Created by phlpsong on 2024/1/6. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import SwiftUI + +@available(macOS 13, *) +struct ServiceTab: View { + @StateObject private var viewModel: ServiceTabViewModel = .init() + + var body: some View { + HStack { + VStack { + WindowTypePicker(windowType: $viewModel.windowType) + .padding() + List(selection: $viewModel.selectedService) { + ServiceItems() + } + .listStyle(.plain) + .scrollIndicators(.never) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.bottom) + .padding(.horizontal) + } + Group { + if let service = viewModel.selectedService { + // To provide configuration options for a service, follow these steps + // 1. If the Service is an object of Objc, expose it to Swift. + // 2. Create a new file in the Utility - Extensions - QueryService+ConfigurableService, + // 3. referring to OpenAIService+ConfigurableService, `extension` the Service as `ConfigurableService` to provide the configuration items. + if let service = service as? (any ConfigurableService) { + AnyView(service.configurationView()) + } else { + HStack { + Spacer() + // No configuration for service xxx + Text("setting.service.detail.no_configuration \(service.name())") + Spacer() + } + } + } else { + HStack { + Spacer() + Text("setting.service.detail.no_selection") + Spacer() + } + } + } + .layoutPriority(1) + } + .environmentObject(viewModel) + } +} + +private class ServiceTabViewModel: ObservableObject { + @Published var windowType = EZWindowType.mini { + didSet { + if oldValue != windowType { + updateServices() + selectedService = nil + } + } + } + + @Published var selectedService: QueryService? + + @Published private(set) var services: [QueryService] = EZLocalStorage.shared().allServices(.mini) + + func updateServices() { + services = getServices() + } + + func getServices() -> [QueryService] { + EZLocalStorage.shared().allServices(windowType) + } + + func onServiceItemMove(fromOffsets: IndexSet, toOffset: Int) { + var services = services + + services.move(fromOffsets: fromOffsets, toOffset: toOffset) + + let serviceTypes = services.map { service in + service.serviceType() + } + + EZLocalStorage.shared().setAllServiceTypes(serviceTypes, windowType: windowType) + + postUpdateServiceNotification() + + updateServices() + } + + func postUpdateServiceNotification() { + let userInfo: [String: Any] = [EZWindowTypeKey: windowType.rawValue] + let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo) + NotificationCenter.default.post(notification) + } +} + +@available(macOS 13.0, *) +private struct ServiceItems: View { + @EnvironmentObject private var viewModel: ServiceTabViewModel + + private var servicesWithID: [(QueryService, String)] { + viewModel.services.map { service in + (service, service.name()) + } + } + + var body: some View { + ForEach(servicesWithID, id: \.1) { service, _ in + ServiceItemView(service: service) + .tag(service) + } + .onMove(perform: viewModel.onServiceItemMove) + } +} + +@available(macOS 13.0, *) +private struct ServiceItemView: View { + let service: QueryService + + @EnvironmentObject private var viewModel: ServiceTabViewModel + + private var enabled: Binding { + .init { + service.enabled + } set: { newValue in + guard service.enabled != newValue else { return } + service.enabled = newValue + if newValue { + service.enabledQuery = newValue + } + EZLocalStorage.shared().setService(service, windowType: viewModel.windowType) + viewModel.postUpdateServiceNotification() + } + } + + var body: some View { + Toggle(isOn: enabled) { + HStack { + Image(service.serviceType().rawValue) + .resizable() + .scaledToFit() + .frame(width: 20.0, height: 20.0) + Text(service.name()) + .lineLimit(1) + .fixedSize() + } + } + .padding(4.0) + .toggleStyle(.switch) + .controlSize(.small) + .listRowSeparator(.hidden) + .listRowInsets(.init()) + .padding(10) + } +} + +@available(macOS 13, *) +private struct WindowTypePicker: View { + @Binding var windowType: EZWindowType + + var body: some View { + HStack { + Picker(selection: $windowType) { + ForEach([EZWindowType]([.mini, .fixed, .main]), id: \.rawValue) { windowType in + Text(windowType.localizedStringResource) + .tag(windowType) + } + } label: { + EmptyView() + } + .labelsHidden() + .pickerStyle(.segmented) + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift new file mode 100644 index 000000000..558bcb5cc --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift @@ -0,0 +1,27 @@ +// +// ShortcutTab.swift +// Easydict +// +// Created by Sharker on 2024/1/21. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +struct ShortcutTab: View { + var body: some View { + Form { + // Global shortcut + GlobalShortcutSettingView() + // In app shortcut + AppShortcutSettingView() + } + .formStyle(.grouped) + } +} + +@available(macOS 13, *) +#Preview { + ShortcutTab() +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift new file mode 100644 index 000000000..62773bb6e --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift @@ -0,0 +1,59 @@ +// +// AppShortcutSetting.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +extension ShortcutTab { + struct AppShortcutSettingView: View { + @State private var shortcutDataList = [ + KeyHolderDataItem(type: .clearInput), + KeyHolderDataItem(type: .clearAll), + KeyHolderDataItem(type: .copy), + KeyHolderDataItem(type: .copyFirstResult), + KeyHolderDataItem(type: .focus), + KeyHolderDataItem(type: .play), + KeyHolderDataItem(type: .retry), + KeyHolderDataItem(type: .toggle), + KeyHolderDataItem(type: .pin), + KeyHolderDataItem(type: .hide), + KeyHolderDataItem(type: .increaseFontSize), + KeyHolderDataItem(type: .decreaseFontSize), + KeyHolderDataItem(type: .google), + KeyHolderDataItem(type: .eudic), + KeyHolderDataItem(type: .appleDic), + ] + @State var confictAlterMessage: ShortcutConfictAlertMessage = .init(title: "", message: "") + var body: some View { + let showAlter = Binding( + get: { + !confictAlterMessage.message.isEmpty + }, + set: { _ in } + ) + Section { + ForEach(shortcutDataList) { item in + KeyHolderRowView(title: item.type.localizedStringKey(), type: item.type, confictAlterMessage: $confictAlterMessage) + } + } header: { + Text("app_shortcut_setting") + } + + .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), + isPresented: showAlter, + presenting: confictAlterMessage) + { _ in + Button(String(localized: "shortcut_confict_confirm")) { + confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") + } + } message: { message in + Text(message.message) + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GlobalShortcutSetting.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GlobalShortcutSetting.swift new file mode 100644 index 000000000..0fdcb9083 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GlobalShortcutSetting.swift @@ -0,0 +1,49 @@ +// +// GlobalShortcutSetting.swift +// Easydict +// +// Created by Sharker on 2024/1/1. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +extension ShortcutTab { + struct GlobalShortcutSettingView: View { + @State var confictAlterMessage: ShortcutConfictAlertMessage = .init(title: "", message: "") + @State private var shortcutDataList = [ + KeyHolderDataItem(type: .inputTranslate), + KeyHolderDataItem(type: .snipTranslate), + KeyHolderDataItem(type: .selectTranslate), + KeyHolderDataItem(type: .showMiniWindow), + KeyHolderDataItem(type: .silentScreenshotOcr), + ] + var body: some View { + let showAlter = Binding( + get: { + !confictAlterMessage.message.isEmpty + }, set: { _ in + } + ) + Section { + ForEach(shortcutDataList) { item in + KeyHolderRowView(title: item.type.localizedStringKey(), type: item.type, confictAlterMessage: $confictAlterMessage) + } + } header: { + Text("global_shortcut_setting") + } + + .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), + isPresented: showAlter, + presenting: confictAlterMessage) + { _ in + Button(String(localized: "shortcut_confict_confirm")) { + confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") + } + } message: { message in + Text(message.message) + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderAlterView.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderAlterView.swift new file mode 100644 index 000000000..5fa9fe233 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderAlterView.swift @@ -0,0 +1,42 @@ +// +// KeyHolderAlterView.swift +// Easydict +// +// Created by Sharker on 2024/2/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +struct KeyHolderAlterView: ViewModifier { + init(showAlter: Binding, confictAlterMessage: Binding) { + _showAlter = showAlter + _confictAlterMessage = confictAlterMessage + } + + @Binding var showAlter: Bool + @Binding public var confictAlterMessage: ShortcutConfictAlertMessage + + func body(content: Content) -> some View { + content + .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), + isPresented: $showAlter, + presenting: confictAlterMessage) + { _ in + Button(String(localized: "shortcut_confict_confirm")) { + confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") + } + } message: { message in + Text(message.message) + } + } +} + +@available(macOS 13, *) +public extension View { + @ViewBuilder + func keyHolderConfictAlter(_ showAlter: Binding, _ confictAlterMessage: Binding) -> some View { + modifier(KeyHolderAlterView(showAlter: showAlter, confictAlterMessage: confictAlterMessage)) + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderRowView.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderRowView.swift new file mode 100644 index 000000000..42cbc41d2 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderRowView.swift @@ -0,0 +1,24 @@ +// +// KeyHolderRowView.swift +// Easydict +// +// Created by Sharker on 2024/2/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +struct KeyHolderRowView: View { + @State public var title: String + @State public var type: ShortcutType + @Binding public var confictAlterMessage: ShortcutConfictAlertMessage + + var body: some View { + HStack { + Text(LocalizedStringKey(title)) + Spacer() + KeyHolderWrapper(shortcutType: type, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 26) + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift new file mode 100644 index 000000000..b366864cc --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift @@ -0,0 +1,182 @@ +// +// KeyHolderWrapper.swift +// Easydict +// +// Created by Sharker on 2024/1/2. +// Copyright © 2024 izual. All rights reserved. +// + +import AppKit +import Defaults +import KeyHolder +import Magnet +import SwiftUI + +public struct KeyHolderDataItem: Identifiable { + public var id: String { type.localizedStringKey() } + var type: ShortcutType +} + +@available(macOS 13, *) +struct KeyHolderWrapper: NSViewRepresentable { + func makeCoordinator() -> Coordinator { + .init(shortcutType: type, confictAlterMessage: $confictAlterMessage) + } + + private var type: ShortcutType + @Binding var confictAlterMessage: ShortcutConfictAlertMessage + init(shortcutType: ShortcutType, confictAlterMessage: Binding) { + type = shortcutType + _confictAlterMessage = confictAlterMessage + } + + func makeNSView(context: Context) -> some NSView { + let recordView = RecordView(frame: CGRect.zero) + recordView.tintColor = NSColor(red: 0.164, green: 0.517, blue: 0.823, alpha: 1) + recordView.delegate = context.coordinator + recordView.layer?.cornerRadius = 6.0 + recordView.layer?.masksToBounds = true + recordView.clearButtonMode = .whenRecorded + + context.coordinator.restoreKeyCombo(recordView) + return recordView + } + + func updateNSView(_: NSViewType, context _: Context) {} +} + +@available(macOS 13, *) +extension KeyHolderWrapper { + class Coordinator: NSObject, RecordViewDelegate { + private var type: ShortcutType + @Binding var confictAlterMessage: ShortcutConfictAlertMessage + init(shortcutType: ShortcutType, confictAlterMessage: Binding) { + type = shortcutType + _confictAlterMessage = confictAlterMessage + } + + func recordViewShouldBeginRecording(_: KeyHolder.RecordView) -> Bool { + true + } + + func recordView(_: KeyHolder.RecordView, canRecordKeyCombo _: Magnet.KeyCombo) -> Bool { + true + } + + func recordViewDidEndRecording(_: RecordView) {} + + func recordView(_ recordView: RecordView, didChangeKeyCombo keyCombo: KeyCombo?) { + if let key = keyCombo { + // shortcut validate confict + if Shortcut.validateShortcut(key) { + let title = String(localized: "shortcut_confict_title \(key.keyEquivalentModifierMaskString + key.characters)") + let message = String(localized: "shortcut_confict_message \(Shortcut.shared.confictMenuItem?.title ?? "")") + confictAlterMessage = ShortcutConfictAlertMessage( + title: title, + message: message + ) + recordView.clear() + return + } + } else { // clear shortcut + Shortcut.shared.updateMenu(type) + } + storeKeyCombo(with: keyCombo) + Shortcut.shared.bindingShortcut(keyCombo: keyCombo, type: type) + } + + func restoreKeyCombo(_ recordView: RecordView) { + var keyCombo: KeyCombo? + switch type { + case .inputTranslate: + keyCombo = Defaults[.inputShortcut] + case .snipTranslate: + keyCombo = Defaults[.snipShortcut] + case .selectTranslate: + keyCombo = Defaults[.selectionShortcut] + case .silentScreenshotOcr: + keyCombo = Defaults[.screenshotOCRShortcut] + case .showMiniWindow: + keyCombo = Defaults[.showMiniWindowShortcut] + case .clearInput: + keyCombo = Defaults[.clearInputShortcut] + case .clearAll: + keyCombo = Defaults[.clearAllShortcut] + case .copy: + keyCombo = Defaults[.copyShortcut] + case .copyFirstResult: + keyCombo = Defaults[.copyFirstResultShortcut] + case .focus: + keyCombo = Defaults[.focusShortcut] + case .play: + keyCombo = Defaults[.playShortcut] + case .retry: + keyCombo = Defaults[.retryShortcut] + case .toggle: + keyCombo = Defaults[.toggleShortcut] + case .pin: + keyCombo = Defaults[.pinShortcut] + case .hide: + keyCombo = Defaults[.hideShortcut] + case .increaseFontSize: + keyCombo = Defaults[.increaseFontSize] + case .decreaseFontSize: + keyCombo = Defaults[.decreaseFontSize] + case .google: + keyCombo = Defaults[.googleShortcut] + case .eudic: + keyCombo = Defaults[.eudicShortcut] + case .appleDic: + keyCombo = Defaults[.appleDictionaryShortcut] + } + recordView.keyCombo = keyCombo + Shortcut.shared.bindingShortcut(keyCombo: keyCombo, type: type) + } + + // shortcut + func storeKeyCombo(with keyCombo: KeyCombo?) { + switch type { + case .inputTranslate: + Defaults[.inputShortcut] = keyCombo + case .snipTranslate: + Defaults[.snipShortcut] = keyCombo + case .selectTranslate: + Defaults[.selectionShortcut] = keyCombo + case .silentScreenshotOcr: + Defaults[.screenshotOCRShortcut] = keyCombo + case .showMiniWindow: + Defaults[.showMiniWindowShortcut] = keyCombo + case .clearInput: + Defaults[.clearInputShortcut] = keyCombo + case .clearAll: + Defaults[.clearAllShortcut] = keyCombo + case .copy: + Defaults[.copyShortcut] = keyCombo + case .copyFirstResult: + Defaults[.copyFirstResultShortcut] = keyCombo + case .focus: + Defaults[.focusShortcut] = keyCombo + case .play: + Defaults[.playShortcut] = keyCombo + case .retry: + Defaults[.retryShortcut] = keyCombo + case .toggle: + Defaults[.toggleShortcut] = keyCombo + case .pin: + Defaults[.pinShortcut] = keyCombo + case .hide: + Defaults[.hideShortcut] = keyCombo + case .increaseFontSize: + Defaults[.increaseFontSize] = keyCombo + case .decreaseFontSize: + Defaults[.decreaseFontSize] = keyCombo + case .google: + Defaults[.googleShortcut] = keyCombo + case .eudic: + Defaults[.eudicShortcut] = keyCombo + case .appleDic: + Defaults[.appleDictionaryShortcut] = keyCombo + } + } + } +} diff --git a/Easydict/NewApp/View/TapHandlerView.swift b/Easydict/NewApp/View/TapHandlerView.swift new file mode 100644 index 000000000..39bdff50f --- /dev/null +++ b/Easydict/NewApp/View/TapHandlerView.swift @@ -0,0 +1,42 @@ +// +// TapHandlerView.swift +// Easydict +// +// Created by phlpsong on 2024/1/10. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +// Ref: https://stackoverflow.com/a/64194868/8378840 +// Fix conflicts between onTap and onMove modifier +class TapHandlerView: NSView { + var tapAction: () -> Void + + init(_ block: @escaping () -> Void) { + tapAction = block + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + tapAction() + } +} + +struct TapHandler: NSViewRepresentable { + let tapAction: () -> Void + + func makeNSView(context _: Context) -> TapHandlerView { + TapHandlerView(tapAction) + } + + func updateNSView(_ nsView: TapHandlerView, context _: Context) { + nsView.tapAction = tapAction + } +} diff --git a/Easydict/NewApp/View/WindowAccessor.swift b/Easydict/NewApp/View/WindowAccessor.swift new file mode 100644 index 000000000..b748023cb --- /dev/null +++ b/Easydict/NewApp/View/WindowAccessor.swift @@ -0,0 +1,23 @@ +// +// WindowAccessor.swift +// Easydict +// +// Created by phlpsong on 2024/1/9. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +struct WindowAccessor: NSViewRepresentable { + @Binding var window: NSWindow? + + func makeNSView(context _: Context) -> NSView { + let view = NSView() + DispatchQueue.main.async { + window = view.window + } + return view + } + + func updateNSView(_: NSView, context _: Context) {} +} diff --git a/EasydictHelper/Info.plist b/EasydictHelper/Info.plist index d8b9f2dde..9a30b4d8d 100644 --- a/EasydictHelper/Info.plist +++ b/EasydictHelper/Info.plist @@ -27,7 +27,7 @@ LSUIElement NSHumanReadableCopyright - Copyright © 2022 tisfeng. All rights reserved. + Copyright © 2023-2024 tisfeng. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/README.md b/README.md index 91ab698d1..2e5b2fa26 100644 --- a/README.md +++ b/README.md @@ -820,6 +820,13 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2023-12-05 | ㅤ—— | 20 | | | 2023-12-07 | 小逗。🎈 | 5 | | | 2023-12-26 | ㅤ Yee | 5 | 感谢开源 | +| 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! | +| 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | +| 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | +| 2024-01-23 | ㅤ | 5 | | +| 2024-01-28 | ㅤ | 7 | | +| 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。| +| 2024-02-04 | ll | 20 | |

diff --git a/README_EN.md b/README_EN.md index 75f213731..ebf43222c 100644 --- a/README_EN.md +++ b/README_EN.md @@ -817,6 +817,13 @@ If you don't want your username to be displayed in the list, please choose anony | 2023-12-05 | ㅤ—— | 20 | | | 2023-12-07 | 小逗。🎈 | 5 | | | 2023-12-26 | ㅤ Yee | 5 | 感谢开源 | +| 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! | +| 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | +| 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | +| 2024-01-23 | ㅤ | 5 | | +| 2024-01-28 | ㅤ | 7 | | +| 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。| +| 2024-02-04 | ll | 20 | |

diff --git a/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md b/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md index 42a3be4c6..9cfe6d240 100644 --- a/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md +++ b/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md @@ -43,7 +43,7 @@ Longman, Collins, and Oxford are three substantial yet outstanding dictionaries. | Concise English-Chinese dictionary | Chinese-English | [GitHub](https://github.com/skywind3000/ECDICT) | https://drive.google.com/file/d/1-RoulJykOmcADGRHSmUjX2SkwiyLTHP1/view?usp=sharing | | Youdao Words Analysis | Chinese-English | [freemdict](https://downloads.freemdict.com/%E5%B0%9A%E6%9C%AA%E6%95%B4%E7%90%86/%E5%85%B1%E4%BA%AB2020.5.11/qwjs/39_%E6%9C%89%E9%81%93%E8%AF%8D%E8%AF%AD%E8%BE%A8%E6%9E%90/) | https://drive.google.com/file/d/1-HGanRhQDRR0OSMLb19or07lPwn_R0cn/view?usp=sharing | | Great Cictionary | Chinese-English | [mdict](https://mdict.org/post/dacihai/) | https://drive.google.com/file/d/1-8cBLcuA_N4PAjIMn_-d03ELv4uVrmIr/view?usp=sharing | -| Longman Dictionary of Contemporary Advanced English | Chinese-English | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1-7g-hDiwqAFtweL1qePKSRcGFJvruu97/view?usp=share_link | +| Longman Dictionary of Contemporary Advanced English | Chinese-English | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1scunXbe2JppVuKxNvn2uOidTbAZpiktk/view?usp=drive_link | | Collins Advanced English-Chinese Dictionary | Chinese-English | [《柯林斯双解》for macOS](https://placeless.net/blog/macos-dictionaries) | https://drive.google.com/file/d/1-KQmILchx71L2rFqhIZMtusIcemIlM01/view?usp=share_link | | Oxford Advanced Learner's English-Chinese Dictionary (8th Edition) | Chinese-English | [Jianshu](https://www.jianshu.com/p/e279d4a979fa) | https://drive.google.com/file/d/1-N0kiXmfTHREcBtumAmNn4sUM5poyiC7/view?usp=share_link | | Oxford Advanced Learner's English-Chinese Dictionary (8) | Chinese-English | Source unknown, I modified the css myself | https://drive.google.com/file/d/1-SigzdPPjQlycPwBHICgQSUOHpR8mMf7/view?usp=share_link | diff --git a/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md b/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md index d990d6210..5faddb4c0 100644 --- a/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md +++ b/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md @@ -43,9 +43,9 @@ Easydict 自动支持词典 App 中系统自带的词典,如牛津英汉汉英 | 简明英汉字典 | 中英 | [GitHub](https://github.com/skywind3000/ECDICT) | https://drive.google.com/file/d/1-RoulJykOmcADGRHSmUjX2SkwiyLTHP1/view?usp=sharing | | 有道词语辨析 | 中英 | [freemdict](https://downloads.freemdict.com/%E5%B0%9A%E6%9C%AA%E6%95%B4%E7%90%86/%E5%85%B1%E4%BA%AB2020.5.11/qwjs/39_%E6%9C%89%E9%81%93%E8%AF%8D%E8%AF%AD%E8%BE%A8%E6%9E%90/) | https://drive.google.com/file/d/1-HGanRhQDRR0OSMLb19or07lPwn_R0cn/view?usp=sharing | | 大辞海 | 中文 | [mdict](https://mdict.org/post/dacihai/) | https://drive.google.com/file/d/1-8cBLcuA_N4PAjIMn_-d03ELv4uVrmIr/view?usp=sharing | -| 朗文当代高级英语辞典 | 中英 | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1-7g-hDiwqAFtweL1qePKSRcGFJvruu97/view?usp=share_link | +| 朗文当代高级英语辞典 | 中英 | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1scunXbe2JppVuKxNvn2uOidTbAZpiktk/view?usp=drive_link | | 柯林斯高阶英汉双解 | 中英 | [《柯林斯双解》for macOS](https://placeless.net/blog/macos-dictionaries) | https://drive.google.com/file/d/1-KQmILchx71L2rFqhIZMtusIcemIlM01/view?usp=share_link | -| 牛津高阶英汉双解词典(第8版) | 中英 | [简书](https://www.jianshu.com/p/e279d4a979fa) | https://drive.google.com/file/d/1-N0kiXmfTHREcBtumAmNn4sUM5poyiC7/view?usp=share_link | +| 牛津高阶英汉双解词典(第 8 版) | 中英 | [简书](https://www.jianshu.com/p/e279d4a979fa) | https://drive.google.com/file/d/1-N0kiXmfTHREcBtumAmNn4sUM5poyiC7/view?usp=share_link | | 牛津高阶英汉双解词典(8) | 中英 | 来源不详,我自己修改的 css | https://drive.google.com/file/d/1-SigzdPPjQlycPwBHICgQSUOHpR8mMf7/view?usp=share_link | ### 简明英汉字典 @@ -71,7 +71,7 @@ Easydict 自动支持词典 App 中系统自带的词典,如牛津英汉汉英 ![image-20231001184454574](https://raw.githubusercontent.com/tisfeng/ImageBed/main/uPic/image-20231001184454574-1696157094.png) -### 牛津高阶英汉双解词典(第8版) +### 牛津高阶英汉双解词典(第 8 版) ![image-20231001185812289](https://raw.githubusercontent.com/tisfeng/ImageBed/main/uPic/image-20231001185812289-1696157892.png)