diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 12acca6f2..e98fe4c7d 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -238,6 +238,17 @@ 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 */; }; @@ -730,6 +741,17 @@ 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 = ""; }; @@ -2350,7 +2372,10 @@ EAED41EA2B54A4900005FE0A /* ServiceConfiguration */ = { isa = PBXGroup; children = ( + 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */, EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */, + 0AC8A8462B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift */, + 0AC8A8442B6A4D97006DA5CC /* ServiceConfigurationCells.swift */, ); path = ServiceConfiguration; sourceTree = ""; @@ -2359,6 +2384,7 @@ isa = PBXGroup; children = ( EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */, + 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */, ); path = Protocol; sourceTree = ""; @@ -2367,6 +2393,13 @@ 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 = ""; @@ -2735,6 +2768,7 @@ 03991158292927E000E1B06D /* EZTitlebar.m in Sources */, 03D8A65C2A433B4100D9A968 /* EZConfiguration+EZUserData.m in Sources */, 03BD282229486CF200F5891A /* EZBlueTextButton.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 */, @@ -2761,6 +2795,7 @@ 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 */, @@ -2797,6 +2832,7 @@ 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 */, 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */, @@ -2826,6 +2862,7 @@ 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 */, @@ -2853,6 +2890,7 @@ 033C30FC2A7409C40095926A /* TTTDictionary.m in Sources */, 03B0232D29231FA6001C7E63 /* NSArray+MM.m in Sources */, 039E5021296E5D9900072344 /* EZScrollViewController.m 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 */, @@ -2890,6 +2928,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 */, @@ -2918,6 +2957,8 @@ 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 */, @@ -2930,6 +2971,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 */, @@ -2941,6 +2983,7 @@ 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 */, @@ -2948,6 +2991,7 @@ 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 */, 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */, 03B0232B29231FA6001C7E63 /* NSMutableAttributedString+MM.m in Sources */, diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 66f840155..bfa0f2504 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -32,5 +32,8 @@ #import "DarkModeManager.h" #import "EZScriptExecutor.h" #import "EZOpenAIService.h" +#import "EZNiuTransTranslate.h" +#import "EZDeepLTranslate.h" +#import "EZBingService.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 257c47129..a2ab7b33c 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -2415,44 +2415,242 @@ } } }, - "service.configuration.openai.api_key.footer" : { + "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" : "API Key的一些说明或者加入链接" + "value" : "https://api-free.deepl.com/v2/translate" } } } }, - "service.configuration.openai.api_key.header" : { + "service.configuration.deepl.endpoint.title" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "API Endpoint" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "完整接口地址" } } } }, - "service.configuration.openai.api_key.prompt" : { + "service.configuration.deepl.translation.title" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "value" : "API Usage Priority" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "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" } } } @@ -2462,30 +2660,178 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "API Key" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "API Key" } } } }, - "service.configuration.openai.translation.footer" : { - + "service.configuration.openai.dictionary.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dictionary" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查单词" + } + } + } }, - "service.configuration.openai.translation.header" : { - + "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.translation.prompt" : { - + "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.service_configuration.reset" : { + "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" : { @@ -2501,6 +2847,86 @@ } } }, + "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" : { diff --git a/Easydict/Feature/Service/Ali/AliService.swift b/Easydict/Feature/Service/Ali/AliService.swift index 9788fc063..f783ac636 100644 --- a/Easydict/Feature/Service/Ali/AliService.swift +++ b/Easydict/Feature/Service/Ali/AliService.swift @@ -37,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() diff --git a/Easydict/Feature/Service/Caiyun/CaiyunService.swift b/Easydict/Feature/Service/Caiyun/CaiyunService.swift index b36cfbdf8..55a2d8b74 100644 --- a/Easydict/Feature/Service/Caiyun/CaiyunService.swift +++ b/Easydict/Feature/Service/Caiyun/CaiyunService.swift @@ -38,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 diff --git a/Easydict/Feature/Service/Gemini/GeminiService.swift b/Easydict/Feature/Service/Gemini/GeminiService.swift index 6df5cfcff..43331a553 100644 --- a/Easydict/Feature/Service/Gemini/GeminiService.swift +++ b/Easydict/Feature/Service/Gemini/GeminiService.swift @@ -6,6 +6,7 @@ // Copyright © 2024 izual. All rights reserved. // +import Defaults import Foundation import GoogleGenerativeAI @@ -59,7 +60,7 @@ public final class GeminiService: QueryService { // easydict://writeKeyValue?EZGeminiAPIKey=xxx private var apiKey: String { - let apiKey = UserDefaults.standard.string(forKey: EZGeminiAPIKey) + let apiKey = Defaults[.geminiAPIKey] if let apiKey, !apiKey.isEmpty { return apiKey } else { @@ -107,7 +108,9 @@ public final class GeminiService: QueryService { resultString += line result.translatedResults = [resultString] - completion(result, nil) + await MainActor.run { + completion(result, nil) + } } } else { @@ -117,7 +120,9 @@ public final class GeminiService: QueryService { } result.translatedResults = [resultString] - completion(result, nil) + await MainActor.run { + completion(result, nil) + } } } catch { /** @@ -131,8 +136,9 @@ public final class GeminiService: QueryService { let errorString = String(describing: error) let errorMessage = errorString.extract(withPattern: "message: \"([^\"]*)\"") ?? errorString ezError?.errorDataMessage = errorMessage - - completion(result, ezError) + await MainActor.run { + completion(result, ezError) + } } } } 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 a2a5f7734..2a1646fea 100644 --- a/Easydict/Feature/Service/Model/EZQueryService.m +++ b/Easydict/Feature/Service/Model/EZQueryService.m @@ -13,6 +13,7 @@ #import "NSString+EZUtils.h" #import "EZConfiguration.h" #import "Easydict-Swift.h" +#import "EZEventMonitor.h" #define MethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ @@ -84,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/Utility/Swift/Binding/Binding+DidSet.swift b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift index 244b735cb..960afabdd 100644 --- a/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift +++ b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift @@ -21,3 +21,10 @@ extension Binding { ) } } + +func ?? (lhs: Binding, rhs: T) -> Binding { + Binding( + get: { lhs.wrappedValue ?? rhs }, + set: { lhs.wrappedValue = $0 } + ) +} diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index 44a5a5e5b..5c753265a 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -1024,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]; @@ -1295,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]; }]; diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index 3b3305739..214e1bb5c 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -134,21 +134,21 @@ class DefaultsWrapper { // Service Configuration extension Defaults.Keys { - // OPENAI + // OpenAI static let openAIAPIKey = Key("EZOpenAIAPIKey") - static let openAITranslation = Key("EZOpenAITranslationKey") - static let openAIDictionary = Key("EZOpenAIDictionaryKey") - static let openAISentence = Key("EZOpenAISentenceKey") - static let openAIServiceUsageStatus = Key("EZOpenAIServiceUsageStatusKey") - static let openAIDomain = Key("EZOpenAIDomainKey") + 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") + static let openAIModel = Key("EZOpenAIModelKey", default: OpenAIModels.gpt3_5_turbo_0125) - // DEEPL + // DeepL static let deepLAuth = Key("EZDeepLAuthKey") + static let deepLTranslation = Key("EZDeepLTranslationAPIKey", default: DeepLAPIUsagePriority.webFirst) static let deepLTranslateEndPointKey = Key("EZDeepLTranslateEndPointKey") - // BING + // Bing static let bingCookieKey = Key("EZBingCookieKey") // niu @@ -161,9 +161,12 @@ extension Defaults.Keys { static let tencentSecretId = Key("EZTencentSecretId") static let tencentSecretKey = Key("EZTencentSecretKey") - // ALI + // Ali static let aliAccessKeyId = Key("EZAliAccessKeyId") static let aliAccessKeySecret = Key("EZAliAccessKeySecret") + + // Gemni + static let geminiAPIKey = Key("EZGeminiAPIKey") } /// shortcut 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 index 960cada25..091631ed8 100644 --- a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift @@ -6,30 +6,99 @@ // Copyright © 2024 izual. All rights reserved. // +import Defaults import Foundation import SwiftUI -@available(macOS 12.0, *) +@available(macOS 13.0, *) extension EZOpenAIService: ConfigurableService { func configurationListItems() -> some View { - ServiceStringConfigurationSection( - textFieldTitleKey: "service.configuration.openai.api_key.header", - headerTitleKey: "service.configuration.openai.api_key.title", - key: .openAIAPIKey, - prompt: "service.configuration.openai.api_key.prompt", - footer: { - Text("service.configuration.openai.api_key.footer") - } - ) - - ServiceStringConfigurationSection( - textFieldTitleKey: "service.configuration.openai.translation.header", - headerTitleKey: "service.configuration.openai.translation.title", - key: .openAITranslation, - prompt: "service.configuration.openai.translation.prompt", - footer: { - Text("service.configuration.openai.translation.footer") - } - ) + 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/Protocol/ServiceSecretConfigreValidatable.swift b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift new file mode 100644 index 000000000..89f3f21a5 --- /dev/null +++ b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift @@ -0,0 +1,24 @@ +// +// 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() + translate("hello world!", from: .english, to: .simplifiedChinese, completion: completion) + } +} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index bb631a6e0..643ee1c23 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -69,11 +69,13 @@ struct SettingView: View { window.standardWindowButton(.zoomButton)?.isEnabled = false // Keep the settings page windows all the same width to avoid strange animations. - let maxWidth = 650 + let maxWidth = 750 let height = switch selection { case .general: - maxWidth - case .service, .disabled, .shortcut: + maxWidth - 100 + case .service: + 600 + case .disabled, .shortcut: 500 case .privacy: 320 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..7586cfdf7 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift @@ -0,0 +1,116 @@ +// +// 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.isAlertPresented = false + } + }) + } + + func validate() { + viewModel.isValidating.toggle() + service.validate { _, error in + DispatchQueue.main.async { + viewModel.alertMessage = error == nil ? "service.configuration.validation_success" : "service.configuration.validation_fail" + print("\(service.serviceType()) validate \(error == nil ? "success" : "fail")!") + viewModel.isValidating.toggle() + viewModel.isAlertPresented.toggle() + } + } + } +} + +@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 + } + } + ) + } +} + +@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 index 42c59d0ee..98a36f433 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift @@ -30,9 +30,10 @@ struct ServiceStringConfigurationSection: View { let value = Binding.init { value.wrappedValue ?? "" } set: { newValue in - value.wrappedValue = newValue + value.wrappedValue = newValue.trimmingCharacters(in: .whitespaces) } TextField(textFieldTitleKey, text: value, prompt: Text(prompt)) + .lineLimit(1) }, footer: footer ) @@ -67,7 +68,7 @@ struct ServiceConfigurationSection: HStack(alignment: .lastTextBaseline) { Text(titleKey) Spacer() - Button("service.service_configuration.reset") { + Button("service.configuration.reset") { _value.reset() } .buttonStyle(.plain)