diff --git a/.swiftformat b/.swiftformat index 71562a9ad..59076a3ce 100644 --- a/.swiftformat +++ b/.swiftformat @@ -85,7 +85,7 @@ strongifiedSelf --indent 4 --maxwidth 120 --typeattributes prev-line ---varattributes same-line +--storedvarattrs same-line --voidtype tuple --wraparguments before-first --wrapparameters before-first @@ -105,4 +105,7 @@ strongifiedSelf # Following is by Lava ---ifdef no-indent \ No newline at end of file +--ifdef no-indent + +# Why doesn't this work? +--modifierorder public,override, \ No newline at end of file diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index ea0730710..529e81431 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 03247E3A296AE8EC00AFCD67 /* EZLoadingAnimationView.m in Sources */ = {isa = PBXBuildFile; fileRef = 03247E39296AE8EC00AFCD67 /* EZLoadingAnimationView.m */; }; 03262C1C29EEE91700EFECA0 /* EZEnumTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 03262C1B29EEE91700EFECA0 /* EZEnumTypes.m */; }; 03262C2529EFE97B00EFECA0 /* NSViewController+EZWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 03262C2429EFE97B00EFECA0 /* NSViewController+EZWindow.m */; }; + 03280B812C23FE4A00E75A24 /* StreamConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03280B802C23FE4A00E75A24 /* StreamConfigurationView.swift */; }; 0329CD6F29EE924500963F78 /* EZRightClickDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = 0329CD6E29EE924500963F78 /* EZRightClickDetector.m */; }; 033363A0293A05D200FED9C8 /* EZSelectLanguageButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 0333639F293A05D200FED9C8 /* EZSelectLanguageButton.m */; }; 033363A6293C4AFA00FED9C8 /* PrintBeautifulLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 033363A5293C4AFA00FED9C8 /* PrintBeautifulLog.m */; }; @@ -65,7 +66,6 @@ 0364EC8D2C208FB70036B61B /* AXSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0364EC8C2C208FB70036B61B /* AXSwift */; }; 036A0DB82AD8403A006E6D4F /* NSString+EZHandleInputText.m in Sources */ = {isa = PBXBuildFile; fileRef = 036A0DB72AD8403A006E6D4F /* NSString+EZHandleInputText.m */; }; 036A0DBB2AD941F9006E6D4F /* EZReplaceTextButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 036A0DBA2AD941F9006E6D4F /* EZReplaceTextButton.m */; }; - 036BCD482BDE5D0D009C893F /* BuiltInAIService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036BCD472BDE5D0D009C893F /* BuiltInAIService+ConfigurableService.swift */; }; 036BCD4A2BDE8A96009C893F /* DefaultsStoredKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036BCD492BDE8A96009C893F /* DefaultsStoredKey.swift */; }; 036D62812BCAB613002C95C7 /* BuiltInAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036D62802BCAB613002C95C7 /* BuiltInAIService.swift */; }; 036E7D7B293F4FC8002675DF /* EZOpenLinkButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 036E7D7A293F4FC8002675DF /* EZOpenLinkButton.m */; }; @@ -221,6 +221,7 @@ 03F0DB382953428300EBF9C1 /* EZLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F0DB372953428300EBF9C1 /* EZLog.m */; }; 03F14A3B2956016B00CB7379 /* EZVolcanoTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F14A3A2956016B00CB7379 /* EZVolcanoTranslate.m */; }; 03F639952AA6CFBB009B9914 /* EZBingConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 03F639942AA6CFBB009B9914 /* EZBingConfig.m */; }; + 03FA677E2C2EFB10000FEA64 /* LLMStreamService+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FA677D2C2EFB10000FEA64 /* LLMStreamService+Configuration.swift */; }; 03FB3EDD2B1B405B004C3238 /* TencentSigning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FB3EDC2B1B405B004C3238 /* TencentSigning.swift */; }; 03FD68BB2B1DC59600FD388E /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03FD68BA2B1DC59600FD388E /* CryptoSwift */; }; 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */; }; @@ -228,7 +229,6 @@ 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 */; }; - 0A318F3B2B8CCCCD0005EF77 /* CustomOpenAIService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A318F3A2B8CCCCD0005EF77 /* CustomOpenAIService+ConfigurableService.swift */; }; 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8685C72B552A590022534F /* DisabledAppTab.swift */; }; 0A9AFBAB2B7F8D7E0064C9A8 /* CustomOpenAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9AFBAA2B7F8D7E0064C9A8 /* CustomOpenAIService.swift */; }; 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */; }; @@ -243,7 +243,6 @@ 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 */; }; @@ -310,8 +309,6 @@ 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 */ @@ -377,6 +374,7 @@ 03262C1B29EEE91700EFECA0 /* EZEnumTypes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZEnumTypes.m; sourceTree = ""; }; 03262C2329EFE97B00EFECA0 /* NSViewController+EZWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSViewController+EZWindow.h"; sourceTree = ""; }; 03262C2429EFE97B00EFECA0 /* NSViewController+EZWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSViewController+EZWindow.m"; sourceTree = ""; }; + 03280B802C23FE4A00E75A24 /* StreamConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamConfigurationView.swift; sourceTree = ""; }; 0329CD6D29EE924500963F78 /* EZRightClickDetector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZRightClickDetector.h; sourceTree = ""; }; 0329CD6E29EE924500963F78 /* EZRightClickDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZRightClickDetector.m; sourceTree = ""; }; 0333639E293A05D200FED9C8 /* EZSelectLanguageButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZSelectLanguageButton.h; sourceTree = ""; }; @@ -442,7 +440,6 @@ 036A0DB72AD8403A006E6D4F /* NSString+EZHandleInputText.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+EZHandleInputText.m"; sourceTree = ""; }; 036A0DB92AD941F9006E6D4F /* EZReplaceTextButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZReplaceTextButton.h; sourceTree = ""; }; 036A0DBA2AD941F9006E6D4F /* EZReplaceTextButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EZReplaceTextButton.m; sourceTree = ""; }; - 036BCD472BDE5D0D009C893F /* BuiltInAIService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BuiltInAIService+ConfigurableService.swift"; sourceTree = ""; }; 036BCD492BDE8A96009C893F /* DefaultsStoredKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsStoredKey.swift; sourceTree = ""; }; 036D62802BCAB613002C95C7 /* BuiltInAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuiltInAIService.swift; sourceTree = ""; }; 036E7D79293F4FC8002675DF /* EZOpenLinkButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZOpenLinkButton.h; sourceTree = ""; }; @@ -708,6 +705,7 @@ 03F14A3A2956016B00CB7379 /* EZVolcanoTranslate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZVolcanoTranslate.m; sourceTree = ""; }; 03F639932AA6CFBB009B9914 /* EZBingConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZBingConfig.h; sourceTree = ""; }; 03F639942AA6CFBB009B9914 /* EZBingConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZBingConfig.m; sourceTree = ""; }; + 03FA677D2C2EFB10000FEA64 /* LLMStreamService+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LLMStreamService+Configuration.swift"; sourceTree = ""; }; 03FB3EDC2B1B405B004C3238 /* TencentSigning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentSigning.swift; 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 = ""; }; @@ -715,7 +713,6 @@ 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 = ""; }; - 0A318F3A2B8CCCCD0005EF77 /* CustomOpenAIService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomOpenAIService+ConfigurableService.swift"; sourceTree = ""; }; 0A8685C72B552A590022534F /* DisabledAppTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppTab.swift; sourceTree = ""; }; 0A9AFBAA2B7F8D7E0064C9A8 /* CustomOpenAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOpenAIService.swift; sourceTree = ""; }; 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; @@ -730,7 +727,6 @@ 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 = ""; }; @@ -809,8 +805,6 @@ 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 */ @@ -1201,12 +1195,12 @@ 03779F0A2BB25688008D3C42 /* Service */ = { isa = PBXGroup; children = ( - 036D627F2BCAB5C6002C95C7 /* BuiltInAI */, 03779F0D2BB256A7008D3C42 /* OpenAI */, + 036D627F2BCAB5C6002C95C7 /* BuiltInAI */, 0A9AFBA92B7F8D6A0064C9A8 /* CustomOpenAI */, + C415C0AB2B450C4500A9D231 /* Gemini */, C4DD01E72B12B3B00025EE8E /* Tencent */, 2746AEBF2AF95040005FE0A1 /* Caiyun */, - C415C0AB2B450C4500A9D231 /* Gemini */, 62E2BF462B4082BA00E42D38 /* Ali */, ); path = Service; @@ -1216,6 +1210,7 @@ isa = PBXGroup; children = ( 0387FB792BFBA990000A7A82 /* LLMStreamService.swift */, + 03FA677D2C2EFB10000FEA64 /* LLMStreamService+Configuration.swift */, 0396DE542BB5844A009FD2A5 /* BaseOpenAIService.swift */, 03779F0B2BB256A7008D3C42 /* OpenAIService.swift */, 03779F0C2BB256A7008D3C42 /* Prompt.swift */, @@ -2381,8 +2376,7 @@ EAED41EA2B54A4900005FE0A /* ServiceConfigurationView */ = { isa = PBXGroup; children = ( - EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */, - EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */, + EAED41F02B54B1A60005FE0A /* ConfigurationView */, 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */, 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */, EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */, @@ -2393,21 +2387,18 @@ path = ServiceConfigurationView; sourceTree = ""; }; - EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */ = { + EAED41F02B54B1A60005FE0A /* ConfigurationView */ = { isa = PBXGroup; children = ( - EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */, - 0A318F3A2B8CCCCD0005EF77 /* CustomOpenAIService+ConfigurableService.swift */, - 036BCD472BDE5D0D009C893F /* BuiltInAIService+ConfigurableService.swift */, + 03280B802C23FE4A00E75A24 /* StreamConfigurationView.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"; + path = ConfigurationView; sourceTree = ""; }; /* End PBXGroup section */ @@ -2803,6 +2794,7 @@ 03BDA7BF2A26DA280079D04F /* NSScanner+EscapedScanning.m in Sources */, 03542A4C2937B5F100C34C33 /* EZYoudaoTranslate.m in Sources */, 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */, + 03FA677E2C2EFB10000FEA64 /* LLMStreamService+Configuration.swift in Sources */, 03247E362968158B00AFCD67 /* EZScriptExecutor.m in Sources */, 03882F8E29D95044005B5A52 /* ToastWindowController.m in Sources */, 03B0231929231FA6001C7E63 /* SnipViewController.m in Sources */, @@ -2812,10 +2804,8 @@ 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 */, - 036BCD482BDE5D0D009C893F /* BuiltInAIService+ConfigurableService.swift in Sources */, 03D5FCFF2A5EF4E400AD26BE /* EZDeviceSystemInfo.m in Sources */, 27FE95272B3DC55F000AD654 /* EasydictApp.swift in Sources */, 03882F9129D95044005B5A52 /* CTCommon.m in Sources */, @@ -2853,7 +2843,6 @@ 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 */, 03779F132BB256B5008D3C42 /* APIKey.swift in Sources */, @@ -2865,6 +2854,7 @@ 03B3B8B52925DD3D00168E8D /* EZPopButtonViewController.m in Sources */, 03542A5B2938DA2B00C34C33 /* EZDetectLanguageButton.m in Sources */, 03B0232929231FA6001C7E63 /* NSDictionary+MM.m in Sources */, + 03280B812C23FE4A00E75A24 /* StreamConfigurationView.swift in Sources */, 0333FDA32A035BEC00891515 /* NSArray+EZChineseText.m in Sources */, 03B0233229231FA6001C7E63 /* MMLog.swift in Sources */, 03DC7C5E2A3ABE28000BF7C9 /* EZConstKey.m in Sources */, @@ -2958,7 +2948,6 @@ 03B0232729231FA6001C7E63 /* NSColor+MM.m in Sources */, 03B0233529231FA6001C7E63 /* MMFileLogFormatter.m in Sources */, 03DC38C1292CC97900922CB2 /* EZServiceInfo.m in Sources */, - 0A318F3B2B8CCCCD0005EF77 /* CustomOpenAIService+ConfigurableService.swift in Sources */, 03B0232A29231FA6001C7E63 /* NSColor+MyColors.m in Sources */, C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */, 0AC8A83D2B6685EE006DA5CC /* SecureTextField.swift in Sources */, @@ -3005,7 +2994,6 @@ 03B022FD29231FA6001C7E63 /* EZFixedQueryWindow.m in Sources */, 03B0232C29231FA6001C7E63 /* NSView+MM.m in Sources */, 0357B95A2C04387D00A48CB0 /* TextEditorCell.swift in Sources */, - EAED41F22B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift in Sources */, 033C31002A74CECE0095926A /* EZAppleDictionary.m in Sources */, 03E2BF752A298F2B00E010F3 /* NSString+EZUtils.m in Sources */, 03B022F529231FA6001C7E63 /* EZDetectManager.m in Sources */, diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 0421f4c1e..e011c6749 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -11,6 +11,16 @@ } } }, + "%@ API Key" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ API Key" + } + } + } + }, "about" : { "comment" : "about", "localizations" : { @@ -2405,7 +2415,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "完整接口地址" + "value" : "API 请求地址" } } } @@ -2442,22 +2452,6 @@ } } }, - "service.configuration.gemini.api_key.placeholder" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "xxxxxxxxxxxxx" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "xxxxxxxxxxxxx" - } - } - } - }, "service.configuration.input.placeholder" : { "localizations" : { "en" : { @@ -2543,13 +2537,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "https://api.openai.com/v1/chat/completions" + "value" : "The full request URL, for example https://api.openai.com/v1/chat/completions" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "https://api.openai.com/v1/chat/completions" + "value" : "完整请求 URL,例如 https://api.openai.com/v1/chat/completions" } } } @@ -2565,7 +2559,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "完整接口地址" + "value" : "API 请求地址" } } } diff --git a/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift b/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift index 72b97f82b..475267104 100644 --- a/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift +++ b/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift @@ -205,153 +205,44 @@ class ShortcutWrapper { } } -// Service Configuration -extension Defaults.Keys { - // OpenAI - static let openAIAPIKey = Key(apiStoredKey(.openAI)) // EZOpenAIAPIKey - static let openAITranslation = Key( - translationStoredKey(.openAI), - default: "1" - ) - static let openAIDictionary = Key( - dictionaryStoredKey(.openAI), - default: "1" - ) - static let openAISentence = Key( - sentenceStoredKey(.openAI), - default: "1" - ) - static let openAIServiceUsageStatus = Key( - serviceUsageStatusStoredKey(.openAI), - default: .default - ) - static let openAIEndPoint = Key(endpointStoredKey(.openAI)) - static let openAIModel = Key( - modelStoredKey(.openAI), - default: OpenAIModel.gpt3_5_turbo.rawValue - ) - static let openAIAvailableModels = Key( - availableModelsStoredKey(.openAI), - default: OpenAIModel.allCases.map { $0.rawValue }.joined(separator: ",") - ) - static let openAIVaildModels = Key( - validModelsStoredKey(.openAI), - default: OpenAIModel.allCases.map { $0.rawValue } - ) - - // Custom OpenAI - static let customOpenAINameKey = Key( - nameStoredKey(.customOpenAI), - default: NSLocalizedString("custom_openai", comment: "") - ) - static let customOpenAIAPIKey = Key(apiStoredKey(.customOpenAI)) - static let customOpenAITranslation = Key( - translationStoredKey(.customOpenAI), - default: "1" - ) - static let customOpenAIDictionary = Key( - dictionaryStoredKey(.customOpenAI), - default: "0" - ) - static let customOpenAISentence = Key( - sentenceStoredKey(.customOpenAI), - default: "0" - ) - static let customOpenAIServiceUsageStatus = Key( - serviceUsageStatusStoredKey(.builtInAI), - default: .default - ) - static let customOpenAIEndPoint = Key(endpointStoredKey(.customOpenAI)) - static let customOpenAIModel = Key( - modelStoredKey(.customOpenAI), - default: "" - ) - static let customOpenAIAvailableModels = Key( - availableModelsStoredKey(.customOpenAI), - default: "" - ) - static let customOpenAIVaildModels = Key( - validModelsStoredKey(.customOpenAI), - default: [""] - ) - - // Built-in AI - static let builtInAIModel = Key( - modelStoredKey(.builtInAI), - default: "" - ) // EZBuiltInAIModelKey - static let builtInAITranslation = Key( - translationStoredKey(.builtInAI), - default: "1" - ) - static let builtInAIDictionary = Key( - dictionaryStoredKey(.builtInAI), - default: "0" - ) - static let builtInAISentence = Key( - sentenceStoredKey(.builtInAI), - default: "0" - ) - static let builtInAIServiceUsageStatus = Key( - serviceUsageStatusStoredKey(.builtInAI), - default: .default - ) +func defaultsKey(_ key: StoredKey, serviceType: ServiceType) -> Defaults.Key { + defaultsKey(key, serviceType: serviceType, defaultValue: nil) +} - // Gemni - static let geminiAPIKey = Key(apiStoredKey(.gemini)) // EZGeminiAPIKey - static let geminiTranslation = Key( - translationStoredKey(.gemini), - default: "1" - ) - static let geminiDictionary = Key( - dictionaryStoredKey(.gemini), - default: "1" - ) - static let geminiSentence = Key( - sentenceStoredKey(.gemini), - default: "1" - ) - static let geminiServiceUsageStatus = Key( - serviceUsageStatusStoredKey(.gemini), - default: .default - ) - static let geminiModel = Key( - modelStoredKey(.gemini), - default: GeminiModel.gemini1_5_flash.rawValue - ) - static let geminiAvailableModels = Key( - availableModelsStoredKey(.gemini), - default: GeminiModel.allCases.map { $0.rawValue }.joined(separator: ",") - ) - static let geminiValidModels = Key( - validModelsStoredKey(.gemini), - default: GeminiModel.allCases.map { $0.rawValue } +func defaultsKey(_ key: StoredKey, serviceType: ServiceType, defaultValue: T) -> Defaults + .Key { + Defaults.Key( + storedKey(key, serviceType: serviceType), + default: defaultValue ) +} +// Service Configuration +extension Defaults.Keys { // DeepL - static let deepLAuth = Key(EZDeepLAuthKey) + static let deepLAuth = Key(EZDeepLAuthKey, default: "") static let deepLTranslation = Key( EZDeepLTranslationAPIKey, default: DeepLAPIUsagePriority.webFirst ) - static let deepLTranslateEndPointKey = Key(EZDeepLTranslateEndPointKey) + static let deepLTranslateEndPointKey = Key(EZDeepLTranslateEndPointKey, default: "") // Bing - static let bingCookieKey = Key(EZBingCookieKey) + static let bingCookieKey = Key(EZBingCookieKey, default: "") // niu - static let niuTransAPIKey = Key(EZNiuTransAPIKey) + static let niuTransAPIKey = Key(EZNiuTransAPIKey, default: "") // Caiyun - static let caiyunToken = Key(EZCaiyunToken) + static let caiyunToken = Key(EZCaiyunToken, default: "") // tencent - static let tencentSecretId = Key(EZTencentSecretId) - static let tencentSecretKey = Key(EZTencentSecretKey) + static let tencentSecretId = Key(EZTencentSecretId, default: "") + static let tencentSecretKey = Key(EZTencentSecretKey, default: "") // Ali - static let aliAccessKeyId = Key(EZAliAccessKeyId) - static let aliAccessKeySecret = Key(EZAliAccessKeySecret) + static let aliAccessKeyId = Key(EZAliAccessKeyId, default: "") + static let aliAccessKeySecret = Key(EZAliAccessKeySecret, default: "") } /// shortcut diff --git a/Easydict/Swift/Feature/Configuration/DefaultsStoredKey.swift b/Easydict/Swift/Feature/Configuration/DefaultsStoredKey.swift index 203ea793a..a3d09f684 100644 --- a/Easydict/Swift/Feature/Configuration/DefaultsStoredKey.swift +++ b/Easydict/Swift/Feature/Configuration/DefaultsStoredKey.swift @@ -8,64 +8,44 @@ import Foundation -// TODO: refactor key with enum key type. -func storedKey(_ key: String, serviceType: ServiceType) -> String { +func storedKey(_ key: StoredKey, serviceType: ServiceType) -> String { // This key should be compatible with existing OpenAI config keys // EZOpenAIServiceUsageStatusKey // EZOpenAIDictionaryKey - "EZ" + serviceType.rawValue + key + "Key" -} - -func serviceUsageStatusStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZServiceUsageStatusKey, serviceType: serviceType) -} - -func translationStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZTranslationKey, serviceType: serviceType) -} - -func sentenceStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZSentenceKey, serviceType: serviceType) -} - -func dictionaryStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZDictionaryKey, serviceType: serviceType) -} - -func availableModelsStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZAvailableModelsKey, serviceType: serviceType) -} - -func validModelsStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZValidModelsKey, serviceType: serviceType) -} - -func modelStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZModelKey, serviceType: serviceType) -} - -func apiStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZAPIKey, serviceType: serviceType) -} - -func endpointStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZEndpointKey, serviceType: serviceType) -} - -func nameStoredKey(_ serviceType: ServiceType) -> String { - storedKey(EZNameKey, serviceType: serviceType) + "EZ" + serviceType.rawValue + key.rawValue.capitalizeFirstLetter() + "Key" } extension UserDefaults { - static func bool(forKey key: String, serviceType: ServiceType) -> Bool { + static func bool(forKey key: StoredKey, serviceType: ServiceType) -> Bool { let key = storedKey(key, serviceType: serviceType) let value = standard.bool(forKey: key) return value } - static func string(forKey key: String, serviceType: ServiceType) -> String? { + static func string(forKey key: StoredKey, serviceType: ServiceType) -> String? { let key = storedKey(key, serviceType: serviceType) let value = standard.string(forKey: key) return value } } + +// MARK: - StoredKey + +enum StoredKey: String { + case serviceUsageStatus + case translation + case dictionary + case sentence + case supportedModels = "AvailableModels" // save in String: "gpt-3.5, gpt-4" + case validModels // save in [String] + case model + case apiKey = "API" + case endpoint = "EndPoint" + case name +} + +extension String { + func capitalizeFirstLetter() -> String { + prefix(1).uppercased() + dropFirst() + } +} diff --git a/Easydict/Swift/Model/ServiceUsageStatus.swift b/Easydict/Swift/Model/ServiceUsageStatus.swift index bfa04040a..a60d08668 100644 --- a/Easydict/Swift/Model/ServiceUsageStatus.swift +++ b/Easydict/Swift/Model/ServiceUsageStatus.swift @@ -39,6 +39,14 @@ extension ServiceUsageStatus: EnumLocalizedStringConvertible { } } -// MARK: Defaults.Serializable +// MARK: - String + EnumLocalizedStringConvertible + +extension String: EnumLocalizedStringConvertible { + var title: LocalizedStringKey { + LocalizedStringKey(self) + } +} + +// MARK: - ServiceUsageStatus + Defaults.Serializable extension ServiceUsageStatus: Defaults.Serializable {} diff --git a/Easydict/Swift/Service/Ali/AliService.swift b/Easydict/Swift/Service/Ali/AliService.swift index f2d59af25..63e214742 100644 --- a/Easydict/Swift/Service/Ali/AliService.swift +++ b/Easydict/Swift/Service/Ali/AliService.swift @@ -15,15 +15,15 @@ import Foundation class AliService: QueryService { // MARK: Public - override public func link() -> String? { + public override func link() -> String? { "https://translate.alibaba.com/" } - override public func name() -> String { + public override func name() -> String { NSLocalizedString("ali_translate", comment: "The name of Ali Translate") } - override public func supportLanguagesDictionary() -> MMOrderedDictionary { + public override func supportLanguagesDictionary() -> MMOrderedDictionary { let orderedDict = MMOrderedDictionary() for (key, value) in AliTranslateType.supportLanguagesDictionary { orderedDict.setObject(value as NSString, forKey: key.rawValue as NSString) @@ -31,12 +31,12 @@ class AliService: QueryService { return orderedDict } - override public func ocr(_: EZQueryModel) async throws -> EZOCRResult { + public override func ocr(_: EZQueryModel) async throws -> EZOCRResult { logInfo("ali Translate does not support OCR") throw QueryServiceError.notSupported } - override public func autoConvertTraditionalChinese() -> Bool { + public override func autoConvertTraditionalChinese() -> Bool { // If translate traditionalChinese <--> simplifiedChinese, use Ali API directly. if EZLanguageManager.shared().onlyContainsChineseLanguages([ queryModel.queryFromLanguage, @@ -57,9 +57,7 @@ class AliService: QueryService { } override func hasPrivateAPIKey() -> Bool { - let id = Defaults[.aliAccessKeyId] ?? "" - let secret = Defaults[.aliAccessKeySecret] ?? "" - return !id.isEmpty && !secret.isEmpty + !aliAccessKeyId.isEmpty && !aliAccessKeySecret.isEmpty } override func translate( @@ -85,13 +83,11 @@ class AliService: QueryService { easydict://writeKeyValue?EZAliAccessKeyId= easydict://writeKeyValue?EZAliAccessKeySecret= */ - if let id = Defaults[.aliAccessKeyId], - let secret = Defaults[.aliAccessKeySecret], - !id.isEmpty, - !secret.isEmpty { + if !aliAccessKeyId.isEmpty, + !aliAccessKeySecret.isEmpty { requestByAPI( - id: id, - secret: secret, + id: aliAccessKeyId, + secret: aliAccessKeySecret, transType: transType, text: text, from: from, @@ -138,6 +134,14 @@ class AliService: QueryService { } } + private var aliAccessKeyId: String { + Defaults[.aliAccessKeyId] + } + + private var aliAccessKeySecret: String { + Defaults[.aliAccessKeySecret] + } + // swiftlint:disable:next function_parameter_count private func requestByAPI( id: String, diff --git a/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift b/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift index 261e64805..89233e9ca 100644 --- a/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift +++ b/Easydict/Swift/Service/BuiltInAI/BuiltInAIService.swift @@ -13,38 +13,25 @@ import Foundation class BuiltInAIService: BaseOpenAIService { // MARK: Public - override public func name() -> String { + public override func name() -> String { NSLocalizedString("built_in_ai", comment: "") } - override public func serviceType() -> ServiceType { + public override func serviceType() -> ServiceType { .builtInAI } - // MARK: Internal - - override var apiKey: String { - defaultAPIKey - } - - override var endpoint: String { - defaultEndpoint + public override func configurationListItems() -> Any { + StreamConfigurationView( + service: self, + showAPIKeySection: false, + showEndpointSection: false + ) } - override var model: String { - get { - var model = Defaults[.builtInAIModel] - if model.isEmpty { - model = availableModels.first! - } - return model - } - set { - Defaults[.builtInAIModel] = newValue - } - } + // MARK: Internal - override var availableModels: [String] { + override var defaultModels: [String] { [ // Groq free models https://console.groq.com/docs/models "llama3-70b-8192", // 30 RPM @@ -77,4 +64,16 @@ class BuiltInAIService: BaseOpenAIService { "ernie-lite-8k", ] } + + override var apiKey: String { + defaultAPIKey + } + + override var endpoint: String { + defaultEndpoint + } + + override var observeKeys: [Defaults.Key] { + [supportedModelsKey] + } } diff --git a/Easydict/Swift/Service/Caiyun/CaiyunService.swift b/Easydict/Swift/Service/Caiyun/CaiyunService.swift index ee4d20149..8344f2a60 100644 --- a/Easydict/Swift/Service/Caiyun/CaiyunService.swift +++ b/Easydict/Swift/Service/Caiyun/CaiyunService.swift @@ -16,19 +16,19 @@ import Foundation public final class CaiyunService: QueryService { // MARK: Public - override public func serviceType() -> ServiceType { + public override func serviceType() -> ServiceType { .caiyun } - override public func link() -> String? { + public override func link() -> String? { "https://fanyi.caiyunapp.com" } - override public func name() -> String { + public override func name() -> String { NSLocalizedString("caiyun_translate", comment: "The name of Caiyun Translate") } - override public func supportLanguagesDictionary() -> MMOrderedDictionary { + public override func supportLanguagesDictionary() -> MMOrderedDictionary { // TODO: Replace MMOrderedDictionary. let orderedDict = MMOrderedDictionary() for (key, value) in CaiyunTranslateType.supportLanguagesDictionary { @@ -37,20 +37,20 @@ public final class CaiyunService: QueryService { return orderedDict } - override public func ocr(_: EZQueryModel) async throws -> EZOCRResult { + public override func ocr(_: EZQueryModel) async throws -> EZOCRResult { logInfo("Caiyun Translate does not support OCR") throw QueryServiceError.notSupported } - override public func hasPrivateAPIKey() -> Bool { + public override func hasPrivateAPIKey() -> Bool { token != defaultToken } - override public func autoConvertTraditionalChinese() -> Bool { + public override func autoConvertTraditionalChinese() -> Bool { true } - override public func translate( + public override func translate( _ text: String, from: Language, to: Language, @@ -115,7 +115,7 @@ public final class CaiyunService: QueryService { // easydict://writeKeyValue?EZCaiyunToken= private var token: String { let token = Defaults[.caiyunToken] - if let token, !token.isEmpty { + if !token.isEmpty { return token } else { return defaultToken diff --git a/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift index df44e5bd8..49b799174 100644 --- a/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift +++ b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift @@ -13,38 +13,21 @@ import Foundation class CustomOpenAIService: BaseOpenAIService { // MARK: Public - override public func name() -> String { - let name = Defaults[.customOpenAINameKey] - if let name, !name.isEmpty { - return name - } - return NSLocalizedString("custom_openai", comment: "The name of Custom OpenAI Translate") + public override func name() -> String { + let serviceName = Defaults[super.nameKey] + return serviceName.isEmpty ? NSLocalizedString("custom_openai", comment: "") : serviceName } - override public func serviceType() -> ServiceType { + public override func serviceType() -> ServiceType { .customOpenAI } // MARK: Internal - override var apiKey: String { - Defaults[.customOpenAIAPIKey] ?? "" - } - - override var endpoint: String { - Defaults[.customOpenAIEndPoint] ?? "" - } - - override var model: String { - get { - Defaults[.customOpenAIModel] - } - set { - Defaults[.customOpenAIModel] = newValue - } - } - - override var availableModels: [String] { - Defaults[.customOpenAIVaildModels] + override func configurationListItems() -> Any { + StreamConfigurationView( + service: self, + showNameSection: true + ) } } diff --git a/Easydict/Swift/Service/Gemini/GeminiService.swift b/Easydict/Swift/Service/Gemini/GeminiService.swift index 9390769f6..76c8aa3dd 100644 --- a/Easydict/Swift/Service/Gemini/GeminiService.swift +++ b/Easydict/Swift/Service/Gemini/GeminiService.swift @@ -16,26 +16,99 @@ import GoogleGenerativeAI public final class GeminiService: LLMStreamService { // MARK: Public - override public func serviceType() -> ServiceType { + public override func serviceType() -> ServiceType { .gemini } - override public func link() -> String? { + public override func link() -> String? { "https://gemini.google.com/" } - override public func name() -> String { + public override func name() -> String { NSLocalizedString("gemini_translate", comment: "The name of Gemini Translate") } - override public func translate( + public override func translate( _ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> () + ) { + if model.isEmpty { + let emptyModelError = EZError(type: .param, description: "model is empty") + completion(result, emptyModelError) + return + } + + performTranslationTask(text: text, from: from, to: to, completion: completion) + } + + public override func configurationListItems() -> Any { + StreamConfigurationView( + service: self, + showEndpointSection: false + ) + } + + // MARK: Internal + + override var defaultModels: [String] { + GeminiModel.allCases.map(\.rawValue) + } + + override var observeKeys: [Defaults.Key] { + [apiKeyKey, supportedModelsKey] + } + + // https://ai.google.dev/available_regions + override var unsupportedLanguages: [Language] { + [ + .persian, + .filipino, + .khmer, + .lao, + .malay, + .mongolian, + .burmese, + .telugu, + .tamil, + .urdu, + ] + } + + override func serviceChatMessageModels(_ chatQuery: ChatQueryParam) -> [Any] { + var chatModels: [ModelContent] = [] + for prompt in chatMessageDicts(chatQuery) { + if let openAIRole = prompt["role"], + let parts = prompt["content"] { + let role = getGeminiRole(from: openAIRole) + let chat = ModelContent(role: role, parts: parts) + chatModels.append(chat) + } + } + return chatModels + } + + // MARK: Private + + // Set Gemini safety level to BLOCK_NONE + private let blockNoneSettings = [ + SafetySetting(harmCategory: .harassment, threshold: .blockNone), + SafetySetting(harmCategory: .hateSpeech, threshold: .blockNone), + SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockNone), + SafetySetting(harmCategory: .dangerousContent, threshold: .blockNone), + ] + + private func performTranslationTask( + text: String, + from: Language, + to: Language, + completion: @escaping (EZQueryResult, Error?) -> () ) { let queryType = queryType(text: text, from: from, to: to) + // Gemini Docs: https://github.com/google/generative-ai-swift + Task { do { result.isStreamFinished = false @@ -48,7 +121,7 @@ public final class GeminiService: LLMStreamService { var systemInstruction: ModelContent? = try ModelContent(role: "system", systemPrompt) // !!!: gemini-1.0-pro model does not support system instruction https://github.com/google-gemini/generative-ai-python/issues/328 - if model == GeminiModel.gemini1_0_pro.rawValue { + if model == GeminiModel.gemini_1_0_pro.rawValue { systemInstruction = nil enableSystemPromptInChats = true } @@ -73,8 +146,6 @@ public final class GeminiService: LLMStreamService { var resultText = "" - // Gemini Docs: https://github.com/google/generative-ai-swift - let outputContentStream = model.generateContentStream(chatHistory) for try await outputContent in outputContentStream { guard let line = outputContent.text else { @@ -107,66 +178,6 @@ public final class GeminiService: LLMStreamService { } } - // MARK: Internal - - // https://ai.google.dev/available_regions - override var unsupportedLanguages: [Language] { - [ - .persian, - .filipino, - .khmer, - .lao, - .malay, - .mongolian, - .burmese, - .telugu, - .tamil, - .urdu, - ] - } - - // easydict://writeKeyValue?EZGeminiAPIKey=xxx - override var apiKey: String { - Defaults[.geminiAPIKey] ?? "" - } - - override var availableModels: [String] { - Defaults[.geminiValidModels] - } - - override var model: String { - get { - Defaults[.geminiModel] - } - set { - // easydict://writeKeyValue?EZGeminiModelKey=gemini-1.5-flash - Defaults[.geminiModel] = newValue - } - } - - override func serviceChatMessageModels(_ chatQuery: ChatQueryParam) -> [Any] { - var chatModels: [ModelContent] = [] - for prompt in chatMessageDicts(chatQuery) { - if let openAIRole = prompt["role"], - let parts = prompt["content"] { - let role = getGeminiRole(from: openAIRole) - let chat = ModelContent(role: role, parts: parts) - chatModels.append(chat) - } - } - return chatModels - } - - // MARK: Private - - // Set Gemini safety level to BLOCK_NONE - private let blockNoneSettings = [ - SafetySetting(harmCategory: .harassment, threshold: .blockNone), - SafetySetting(harmCategory: .hateSpeech, threshold: .blockNone), - SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockNone), - SafetySetting(harmCategory: .dangerousContent, threshold: .blockNone), - ] - /// Get gemini role, currently only support "user" and "model", "model" is equal to OpenAI "assistant". https://ai.google.dev/gemini-api/docs/get-started/tutorial?lang=swift&hl=zh-cn#multi-turn-conversations-chat private func getGeminiRole(from openAIRole: String) -> String { if openAIRole == "assistant" { @@ -178,3 +189,18 @@ public final class GeminiService: LLMStreamService { } } } + +// MARK: - GeminiModel + +// swiftlint:disable identifier_name +enum GeminiModel: String, CaseIterable { + // Docs: https://ai.google.dev/gemini-api/docs/models/gemini + + // RPM: Requests per minute, TPM: Tokens per minute + // RPD: Requests per day, TPD: Tokens per day + case gemini_1_5_flash = "gemini-1.5-flash" // Free 15 RPM/100million TPM, 1500 RPD/ n/a TPD (1048k context length) + case gemini_1_5_pro = "gemini-1.5-pro" // Free 2 RPM/32,000 TPM, 50 RPD/46,080,000 TPD (1048k context length) + case gemini_1_0_pro = "gemini-1.0-pro" // Free 15 RPM/32,000 TPM, 1,500 RPD/46,080,000 TPD (n/a context length) +} + +// swiftlint:enable identifier_name diff --git a/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift b/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift index 44c63cdff..7ee475ac2 100644 --- a/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift +++ b/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift @@ -18,14 +18,14 @@ import OpenAI public class BaseOpenAIService: LLMStreamService { // MARK: Public - override public func translate( + public override func translate( _ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> () ) { let url = URL(string: endpoint) - let invalidURLError = EZError(type: .param, description: "\(serviceType().rawValue) URL is invalid") + let invalidURLError = EZError(type: .param, description: "`\(serviceType().rawValue)` endpoint is invalid") guard let url, url.isValid else { completion(result, invalidURLError) return @@ -52,8 +52,11 @@ public class BaseOpenAIService: LLMStreamService { let query = ChatQuery(messages: chatHistory, model: model, temperature: 0) let openAI = OpenAI(apiToken: apiKey) + // FIXME: It seems that `control` will cause a memory leak, but it is not clear how to solve it. + unowned let unownedControl = control + // TODO: refactor chatsStream with await - openAI.chatsStream(query: query, url: url, control: control) { [weak self] res in + openAI.chatsStream(query: query, url: url, control: unownedControl) { [weak self] res in guard let self else { return } switch res { diff --git a/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift b/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift new file mode 100644 index 000000000..4a8d1be00 --- /dev/null +++ b/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift @@ -0,0 +1,100 @@ +// +// LLMStreamService+Configuation.swift +// Easydict +// +// Created by tisfeng on 2024/6/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension LLMStreamService { + func setupSubscribers() { + logInfo("setup subscribers") + + Defaults.publisher(nameKey, options: []) + .removeDuplicates() + .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] _ in + self?.notifyServiceConfigurationChanged() + } + .store(in: &cancellables) + + Defaults.publisher(modelKey, options: []) + .removeDuplicates() + .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] in + self?.modelDidChanged($0.newValue) + } + .store(in: &cancellables) + + Defaults.publisher(supportedModelsKey, options: []) + .removeDuplicates() + .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] in + self?.supportedModelsTextDidChanged($0.newValue) + } + .store(in: &cancellables) + } + + func cancelSubscribers() { + logInfo("cancel subscribers") + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } + + func modelDidChanged(_ newModel: String) { + model = newModel + + // Handle some special cases + if !validModels.contains(newModel) { + if newModel.isEmpty { + supportedModels = "" + } else { + if supportedModels.isEmpty { + supportedModels = newModel + } else { + supportedModels = "\(newModel), " + supportedModels + } + } + } + notifyServiceConfigurationChanged(autoQuery: true) + } + + func supportedModelsTextDidChanged(_ newSupportedModels: String) { + supportedModels = newSupportedModels + + if validModels.isEmpty { + model = "" + } else if !validModels.contains(model) { + model = validModels[0] + } + } + + func notifyServiceConfigurationChanged(autoQuery: Bool = false) { + logInfo("service config changed: \(serviceType().rawValue), windowType: \(windowType.rawValue)") + + NotificationCenter.default.postServiceUpdateNotification( + serviceType: serviceType(), + windowType: windowType, + autoQuery: autoQuery + ) + } + + func stringDefaultsKey(_ key: StoredKey) -> Defaults.Key { + stringDefaultsKey(key, defaultValue: "") + } + + func stringDefaultsKey(_ key: StoredKey, defaultValue: String) -> Defaults.Key { + defaultsKey(key, serviceType: serviceType(), defaultValue: defaultValue) + } + + func serviceDefaultsKey(_ key: StoredKey, defaultValue: T) -> Defaults.Key { + defaultsKey(key, serviceType: serviceType(), defaultValue: defaultValue) + } + + func serviceDefaultsKey(_ key: StoredKey) -> Defaults.Key { + defaultsKey(key, serviceType: serviceType()) + } +} diff --git a/Easydict/Swift/Service/OpenAI/LLMStreamService.swift b/Easydict/Swift/Service/OpenAI/LLMStreamService.swift index 2aa4e5f94..5c117ab6c 100644 --- a/Easydict/Swift/Service/OpenAI/LLMStreamService.swift +++ b/Easydict/Swift/Service/OpenAI/LLMStreamService.swift @@ -6,25 +6,29 @@ // Copyright © 2024 izual. All rights reserved. // +import Combine import Defaults import Foundation +import SwiftUI // MARK: - LLMStreamService +// import SwiftUI + @objcMembers @objc(EZLLMStreamService) public class LLMStreamService: QueryService { // MARK: Public - override public func isStream() -> Bool { + public override func isStream() -> Bool { true } - override public func intelligentQueryTextType() -> EZQueryTextType { + public override func intelligentQueryTextType() -> EZQueryTextType { Configuration.shared.intelligentQueryTextTypeForServiceType(serviceType()) } - override public func supportLanguagesDictionary() -> MMOrderedDictionary { + public override func supportLanguagesDictionary() -> MMOrderedDictionary { let allLanguages = EZLanguageManager.shared().allLanguages let supportedLanguages = allLanguages.filter { language in !unsupportedLanguages.contains(language) @@ -37,12 +41,12 @@ public class LLMStreamService: QueryService { return orderedDict } - override public func queryTextType() -> EZQueryTextType { + public override func queryTextType() -> EZQueryTextType { var typeOptions: EZQueryTextType = [] - let isTranslationEnabled = UserDefaults.bool(forKey: EZTranslationKey, serviceType: serviceType()) - let isSentenceEnabled = UserDefaults.bool(forKey: EZSentenceKey, serviceType: serviceType()) - let isDictionaryEnabled = UserDefaults.bool(forKey: EZDictionaryKey, serviceType: serviceType()) + let isTranslationEnabled = Defaults[translationKey].boolValue + let isSentenceEnabled = Defaults[sentenceKey].boolValue + let isDictionaryEnabled = Defaults[dictionaryKey].boolValue if isTranslationEnabled { typeOptions.insert(.translation) @@ -57,9 +61,9 @@ public class LLMStreamService: QueryService { return typeOptions } - override public func serviceUsageStatus() -> EZServiceUsageStatus { - let usageStatus = UserDefaults.string(forKey: EZServiceUsageStatusKey, serviceType: serviceType()) ?? "" - guard let value = UInt(usageStatus) else { return .default } + public override func serviceUsageStatus() -> EZServiceUsageStatus { + let usageStatus = Defaults[serviceUsageStatusKey] + guard let value = UInt(usageStatus.rawValue) else { return .default } return EZServiceUsageStatus(rawValue: value) ?? .default } @@ -69,25 +73,111 @@ public class LLMStreamService: QueryService { let mustOverride = "This property or method must be overridden by a subclass" + var cancellables: Set = [] + + var defaultModels: [String] { + [""] + } + var unsupportedLanguages: [Language] { [] } var model: String { - get { fatalError(mustOverride) } - set { _ = newValue; fatalError(mustOverride) } + get { + var model = Defaults[modelKey] + if !validModels.contains(model) || model.isEmpty { + model = validModels.first ?? "" + Defaults[modelKey] = model + } + return model + } + set { + Defaults[modelKey] = newValue + } } - var availableModels: [String] { - fatalError(mustOverride) + var modelKey: Defaults.Key { + stringDefaultsKey(.model, defaultValue: defaultModels.first ?? "") + } + + var supportedModels: String { + get { Defaults[supportedModelsKey] } + set { + Defaults[supportedModelsKey] = newValue + Defaults[validModelsKey] = validModels(from: newValue) + } + } + + var supportedModelsKey: Defaults.Key { + stringDefaultsKey(.supportedModels, defaultValue: supportedModels(from: defaultModels)) + } + + /// Just getter, we should set supportedModels and get validModels. + var validModels: [String] { + Defaults[validModelsKey] + } + + var validModelsKey: Defaults.Key<[String]> { + serviceDefaultsKey(.validModels, defaultValue: defaultModels) } var apiKey: String { - fatalError(mustOverride) + Defaults[apiKeyKey] + } + + var apiKeyKey: Defaults.Key { + stringDefaultsKey(.apiKey) } var endpoint: String { - fatalError(mustOverride) + Defaults[endpointKey] + } + + var endpointKey: Defaults.Key { + stringDefaultsKey(.endpoint) + } + + var nameKey: Defaults.Key { + stringDefaultsKey(.name) + } + + var translationKey: Defaults.Key { + stringDefaultsKey(.translation) + } + + var sentenceKey: Defaults.Key { + stringDefaultsKey(.sentence) + } + + var dictionaryKey: Defaults.Key { + stringDefaultsKey(.dictionary) + } + + var serviceUsageStatusKey: Defaults.Key { + serviceDefaultsKey(.serviceUsageStatus, defaultValue: .default) + } + + // In general, LLM services need to observe these keys to enable validation button. + var observeKeys: [Defaults.Key] { + [ + apiKeyKey, + endpointKey, + supportedModelsKey, + ] + } + + var apiKeyPlaceholder: LocalizedStringKey { + "\(serviceType().rawValue) API Key" + } + + func validModels(from supportedModels: String) -> [String] { + supportedModels.components(separatedBy: ",") + .map { $0.trim() }.filter { !$0.isEmpty } + } + + func supportedModels(from validModels: [String]) -> String { + validModels.joined(separator: ", ") } /// Base on chat query, convert prompt dict to LLM service prompt model. diff --git a/Easydict/Swift/Service/OpenAI/OpenAIService.swift b/Easydict/Swift/Service/OpenAI/OpenAIService.swift index 396da9ce0..e021db294 100644 --- a/Easydict/Swift/Service/OpenAI/OpenAIService.swift +++ b/Easydict/Swift/Service/OpenAI/OpenAIService.swift @@ -8,6 +8,7 @@ import Defaults import Foundation +import SwiftUI // MARK: - OpenAIService @@ -15,46 +16,46 @@ import Foundation class OpenAIService: BaseOpenAIService { // MARK: Public - override public func serviceType() -> ServiceType { + public override func serviceType() -> ServiceType { .openAI } - override public func name() -> String { + public override func name() -> String { NSLocalizedString("openai_translate", comment: "") } - override public func link() -> String? { + public override func link() -> String? { "https://chatgpt.com" } // MARK: Internal - override var availableModels: [String] { - Defaults[.openAIVaildModels] + override var defaultModels: [String] { + OpenAIModel.allCases.map(\.rawValue) } - override var model: String { - get { - Defaults[.openAIModel] - } - set { - // easydict://writeKeyValue?EZOpenAIModelKey=gpt-3.5-turbo - Defaults[.openAIModel] = newValue - } + override var endpoint: String { + super.endpoint.isEmpty ? "https://api.openai.com/v1/chat/completions" : super.endpoint } - override var apiKey: String { - // easydict://writeKeyValue?EZOpenAIAPIKey= - Defaults[.openAIAPIKey] ?? "" + override var apiKeyPlaceholder: LocalizedStringKey { + "service.configuration.openai.api_key.placeholder" } - override var endpoint: String { - // easydict://writeKeyValue?EZOpenAIEndPointKey= - var endPoint = Defaults[.openAIEndPoint] ?? "" - if endPoint.isEmpty { - endPoint = "https://api.openai.com/v1/chat/completions" - } - - return endPoint + override func configurationListItems() -> Any { + StreamConfigurationView(service: self) } } + +// MARK: - OpenAIModel + +// swiftlint:disable identifier_name +enum OpenAIModel: String, CaseIterable { + // Docs: https://platform.openai.com/docs/models + + case gpt_3_5_turbo = "gpt-3.5-turbo" // Currently points to gpt-3.5-turbo-0125. Input: $0.50 | Output: $1.50 (16k) + case gpt_4_turbo = "gpt-4-turbo" // Currently points to gpt-4-turbo-2024-04-09. Input: $10 | Output: $30 (128k) + case gpt_4o = "gpt-4o" // Currently points to gpt-4o-2024-05-13. Input: $5 | Output: $15 (128k context length) +} + +// swiftlint:enable identifier_name diff --git a/Easydict/Swift/Service/Tencent/TencentService.swift b/Easydict/Swift/Service/Tencent/TencentService.swift index 743a5cef1..282b4254c 100644 --- a/Easydict/Swift/Service/Tencent/TencentService.swift +++ b/Easydict/Swift/Service/Tencent/TencentService.swift @@ -14,19 +14,19 @@ import Foundation public final class TencentService: QueryService { // MARK: Public - override public func serviceType() -> ServiceType { + public override func serviceType() -> ServiceType { .tencent } - override public func link() -> String? { + public override func link() -> String? { "https://fanyi.qq.com" } - override public func name() -> String { + public override func name() -> String { NSLocalizedString("tencent_translate", comment: "The name of Tencent Translate") } - override public func supportLanguagesDictionary() -> MMOrderedDictionary { + public override func supportLanguagesDictionary() -> MMOrderedDictionary { let orderedDict = MMOrderedDictionary() for (key, value) in TencentTranslateType.supportLanguagesDictionary { orderedDict.setObject(value as NSString, forKey: key.rawValue as NSString) @@ -34,27 +34,27 @@ public final class TencentService: QueryService { return orderedDict } - override public func ocr(_: EZQueryModel) async throws -> EZOCRResult { + public override func ocr(_: EZQueryModel) async throws -> EZOCRResult { logInfo("Tencent Translate currently does not support OCR") throw QueryServiceError.notSupported } - override public func needPrivateAPIKey() -> Bool { + public override func needPrivateAPIKey() -> Bool { true } - override public func hasPrivateAPIKey() -> Bool { + public override func hasPrivateAPIKey() -> Bool { if secretId == defaultSecretId, secretKey == defaultSecretKey { return false } return true } - override public func totalFreeQueryCharacterCount() -> Int { + public override func totalFreeQueryCharacterCount() -> Int { 500 * 10000 } - override public func translate( + public override func translate( _ text: String, from: Language, to: Language, @@ -135,7 +135,7 @@ public final class TencentService: QueryService { // easydict://writeKeyValue?EZTencentSecretId=xxx private var secretId: String { let secretId = Defaults[.tencentSecretId] - if let secretId, !secretId.isEmpty { + if !secretId.isEmpty { return secretId } else { return defaultSecretId @@ -145,7 +145,7 @@ public final class TencentService: QueryService { // easydict://writeKeyValue?EZTencentSecretKey=xxx private var secretKey: String { let secretKey = Defaults[.tencentSecretKey] - if let secretKey, !secretKey.isEmpty { + if !secretKey.isEmpty { return secretKey } else { return defaultSecretKey diff --git a/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift b/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift index 83c897869..87ca45106 100644 --- a/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift +++ b/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift @@ -28,3 +28,24 @@ extension NSNotification { public static let linkButtonUpdated = Notification.Name.linkButtonUpdated } + +@objc +extension NotificationCenter { + func postServiceUpdateNotification( + serviceType: ServiceType = .init(rawValue: ""), + windowType: EZWindowType = .none, + autoQuery: Bool = false + ) { + let userInfo: [String: Any] = [ + EZServiceTypeKey: serviceType.rawValue, + EZWindowTypeKey: windowType.rawValue, + EZAutoQueryKey: autoQuery, + ] + let notification = Notification(name: .serviceHasUpdated, userInfo: userInfo) + post(notification) + } + + func postServiceUpdateNotification() { + postServiceUpdateNotification(autoQuery: false) + } +} diff --git a/Easydict/Swift/Utility/Extensions/String/String+Extension.swift b/Easydict/Swift/Utility/Extensions/String/String+Extension.swift index 8331567b7..463cd6cb0 100644 --- a/Easydict/Swift/Utility/Extensions/String/String+Extension.swift +++ b/Easydict/Swift/Utility/Extensions/String/String+Extension.swift @@ -9,6 +9,10 @@ import Foundation extension String { + var boolValue: Bool { + (self as NSString).boolValue + } + /// Truncate string max lenght to 200. func truncated(_ maxLength: Int = 200) -> String { String(prefix(maxLength)) diff --git a/Easydict/Swift/Utility/GlobalContext.swift b/Easydict/Swift/Utility/GlobalContext.swift index 3fbe43892..063996af5 100644 --- a/Easydict/Swift/Utility/GlobalContext.swift +++ b/Easydict/Swift/Utility/GlobalContext.swift @@ -45,6 +45,8 @@ class GlobalContext: NSObject { let updaterController: SPUStandardUpdaterController + var subscribeWindowType: EZWindowType = .none + // MARK: Private private let updaterHelper: SPUUpdaterHelper diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurableService.swift deleted file mode 100644 index 833a456f8..000000000 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurableService.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ConfigurableService.swift -// Easydict -// -// Created by 戴藏龙 on 2024/1/14. -// Copyright © 2024 izual. All rights reserved. -// - -import Foundation -import SwiftUI - -// MARK: - ConfigurableService - -/// A service can provide configuration view in setting -protocol ConfigurableService { - // swiftlint:disable:next type_name - associatedtype T: View - - /// Items in Configuration Form. Use ServiceStringConfigurationSection or other customize view. - @ViewBuilder - func configurationListItems() -> T -} - -extension ConfigurableService { - func configurationView() -> some View { - Form { - configurationListItems() - } - .formStyle(.grouped) - } -} diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/AliService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/AliService+ConfigurableService.swift similarity index 68% rename from Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/AliService+ConfigurableService.swift rename to Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/AliService+ConfigurableService.swift index f9712071d..ee711645b 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/AliService+ConfigurableService.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/AliService+ConfigurableService.swift @@ -6,15 +6,11 @@ // Copyright © 2024 izual. All rights reserved. // -import Foundation import SwiftUI -extension AliService: ConfigurableService { - func configurationListItems() -> some View { - ServiceConfigurationSecretSectionView( - service: self, - observeKeys: [.aliAccessKeyId, .aliAccessKeySecret] - ) { +extension AliService { + override func configurationListItems() -> Any? { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.aliAccessKeyId, .aliAccessKeySecret]) { ServiceConfigurationSecureInputCell( textFieldTitleKey: "service.configuration.ali.access_key_id.title", key: .aliAccessKeyId diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/BingService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/BingService+ConfigurableService.swift similarity index 80% rename from Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/BingService+ConfigurableService.swift rename to Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/BingService+ConfigurableService.swift index a9f232b7b..2719aacc9 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/BingService+ConfigurableService.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/BingService+ConfigurableService.swift @@ -6,11 +6,10 @@ // Copyright © 2024 izual. All rights reserved. // -import Foundation import SwiftUI -extension EZBingService: ConfigurableService { - func configurationListItems() -> some View { +extension EZBingService { + open override func configurationListItems() -> Any? { ServiceConfigurationSecretSectionView(service: self, observeKeys: [.bingCookieKey]) { ServiceConfigurationSecureInputCell( textFieldTitleKey: "service.configuration.bing.cookie.title", diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/CaiyunService+ConfigurableService.swift similarity index 77% rename from Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift rename to Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/CaiyunService+ConfigurableService.swift index f47c30505..9623ad922 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/CaiyunService+ConfigurableService.swift @@ -9,8 +9,10 @@ import Foundation import SwiftUI -extension CaiyunService: ConfigurableService { - func configurationListItems() -> some View { +// MARK: - CaiyunService + ConfigurableService + +extension CaiyunService { + public override func configurationListItems() -> Any? { ServiceConfigurationSecretSectionView(service: self, observeKeys: [.caiyunToken]) { ServiceConfigurationSecureInputCell( textFieldTitleKey: "service.configuration.caiyun.token.title", diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/DeepLTranslate+ConfigurableService.swift similarity index 77% rename from Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift rename to Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/DeepLTranslate+ConfigurableService.swift index 3feb2eec1..de79178c3 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/DeepLTranslate+ConfigurableService.swift @@ -10,21 +10,9 @@ import Defaults import Foundation import SwiftUI -// MARK: - EZDeepLTranslate + ConfigurableService - -extension EZDeepLTranslate: ConfigurableService { - func configurationListItems() -> some View { - EZDeepLTranslateConfigurationView(service: self) - } -} - -// MARK: - EZDeepLTranslateConfigurationView - -private struct EZDeepLTranslateConfigurationView: View { - let service: EZDeepLTranslate - - var body: some View { - ServiceConfigurationSecretSectionView(service: service, observeKeys: [.deepLAuth]) { +extension EZDeepLTranslate { + open override func configurationListItems() -> Any? { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.deepLAuth]) { ServiceConfigurationSecureInputCell( textFieldTitleKey: "service.configuration.deepl.auth_key.title", key: .deepLAuth diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/NiuTransTranslate+ConfigurableService.swift similarity index 77% rename from Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift rename to Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/NiuTransTranslate+ConfigurableService.swift index 0c685165a..881e34dab 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/NiuTransTranslate+ConfigurableService.swift @@ -9,8 +9,10 @@ import Foundation import SwiftUI -extension EZNiuTransTranslate: ConfigurableService { - func configurationListItems() -> some View { +// MARK: - EZNiuTransTranslate + ConfigurableService + +extension EZNiuTransTranslate { + open override func configurationListItems() -> Any? { ServiceConfigurationSecretSectionView(service: self, observeKeys: [.niuTransAPIKey]) { ServiceConfigurationSecureInputCell( textFieldTitleKey: "service.configuration.niutrans.api_key.title", diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/StreamConfigurationView.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/StreamConfigurationView.swift new file mode 100644 index 000000000..aa4535592 --- /dev/null +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/StreamConfigurationView.swift @@ -0,0 +1,141 @@ +// +// StreamConfigurationView.swift +// Easydict +// +// Created by tisfeng on 2024/6/20. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import Defaults +import Foundation +import SwiftUI + +// MARK: - StreamConfigurationView + +struct StreamConfigurationView: View { + // MARK: Lifecycle + + init( + service: LLMStreamService, + showNameSection: Bool = false, + showAPIKeySection: Bool = true, + showEndpointSection: Bool = true, + showSupportedModelsSection: Bool = true, + showUsedModelSection: Bool = true, + showTranslationToggle: Bool = true, + showSentenceToggle: Bool = true, + showDictionaryToggle: Bool = true, + showUsageStatusPicker: Bool = true + ) { + self.service = service + + self.showNameSection = showNameSection + self.showAPIKeySection = showAPIKeySection + self.showEndpointSection = showEndpointSection + self.showSupportedModelsSection = showSupportedModelsSection + self.showUsedModelSection = showUsedModelSection + self.showTranslationToggle = showTranslationToggle + self.showSentenceToggle = showSentenceToggle + self.showDictionaryToggle = showDictionaryToggle + self.showUsageStatusPicker = showUsageStatusPicker + + // Disable user to edit built-in supported models. + self.isEditable = service.serviceType() != .builtInAI + + #if DEBUG + self.isEditable = isEditable || Defaults[.enableBetaFeature] + #endif + } + + // MARK: Internal + + let service: LLMStreamService + + let showNameSection: Bool + let showAPIKeySection: Bool + let showEndpointSection: Bool + let showSupportedModelsSection: Bool + let showUsedModelSection: Bool + let showTranslationToggle: Bool + let showSentenceToggle: Bool + let showDictionaryToggle: Bool + let showUsageStatusPicker: Bool + + var isEditable = true + + var body: some View { + ServiceConfigurationSecretSectionView( + service: service, + observeKeys: service.observeKeys + ) { + if showNameSection { + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.custom_openai.name.title", + key: service.nameKey, + placeholder: "custom_openai", + limitLength: 20 + ) + } + + if showAPIKeySection { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.openai.api_key.title", + key: service.apiKeyKey, + placeholder: service.apiKeyPlaceholder + ) + } + + if showEndpointSection { + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.openai.endpoint.title", + key: service.endpointKey, + placeholder: "service.configuration.openai.endpoint.placeholder" + ) + } + + if showSupportedModelsSection { + TextEditorCell( + titleKey: "service.configuration.custom_openai.supported_models.title", + storedValueKey: service.supportedModelsKey, + placeholder: "service.configuration.custom_openai.model.placeholder" + ).disabled(!isEditable) + } + + if showUsedModelSection { + PickerCell( + titleKey: "service.configuration.openai.model.title", + selectionKey: service.modelKey, + valuesKey: service.validModelsKey + ) + } + + if showTranslationToggle { + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.translation.title", + key: service.translationKey + ) + } + if showSentenceToggle { + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.sentence.title", + key: service.sentenceKey + ) + } + if showDictionaryToggle { + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.dictionary.title", + key: service.dictionaryKey + ) + } + + if showUsageStatusPicker { + ServiceConfigurationPickerCell( + titleKey: "service.configuration.openai.usage_status.title", + key: service.serviceUsageStatusKey, + values: ServiceUsageStatus.allCases + ) + } + } + } +} diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/TencentService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/TencentService+ConfigurableService.swift similarity index 70% rename from Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/TencentService+ConfigurableService.swift rename to Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/TencentService+ConfigurableService.swift index 146e8d64a..73014326a 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/TencentService+ConfigurableService.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ConfigurationView/TencentService+ConfigurableService.swift @@ -9,17 +9,13 @@ import Foundation import SwiftUI -extension TencentService: ConfigurableService { - func configurationListItems() -> some View { - ServiceConfigurationSecretSectionView( - service: self, - observeKeys: [.tencentSecretId, .tencentSecretKey] - ) { +extension TencentService { + public override func configurationListItems() -> Any? { + 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/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/BuiltInAIService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/BuiltInAIService+ConfigurableService.swift deleted file mode 100644 index 7895a4965..000000000 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/BuiltInAIService+ConfigurableService.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// BuiltInAIService+ConfigurableService.swift -// Easydict -// -// Created by tisfeng on 2024/4/28. -// Copyright © 2024 izual. All rights reserved. -// - -import Defaults -import SwiftUI - -// MARK: - BuiltInAIService + ConfigurableService - -extension BuiltInAIService: ConfigurableService { - func configurationListItems() -> some View { - ServiceConfigurationSecretSectionView( - service: self, observeKeys: [] - ) { - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.translation.title", - key: .builtInAITranslation - ) - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.sentence.title", - key: .builtInAISentence - ) - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.dictionary.title", - key: .builtInAIDictionary - ) - ServiceConfigurationPickerCell( - titleKey: "service.configuration.openai.usage_status.title", - key: .builtInAIServiceUsageStatus, - values: ServiceUsageStatus.allCases - ) - } - } -} diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/CustomOpenAIService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/CustomOpenAIService+ConfigurableService.swift deleted file mode 100644 index f738bc2c5..000000000 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/CustomOpenAIService+ConfigurableService.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// CustomOpenAIService+ConfigurableService.swift -// Easydict -// -// Created by phlpsong on 2024/2/26. -// Copyright © 2024 izual. All rights reserved. -// - -import Combine -import Defaults -import Foundation -import SwiftUI - -// MARK: - CustomOpenAIService + ConfigurableService - -extension CustomOpenAIService: ConfigurableService { - func configurationListItems() -> some View { - CustomOpenAIServiceConfigurationView(service: self) - } -} - -// MARK: - CustomOpenAIServiceConfigurationView - -private struct CustomOpenAIServiceConfigurationView: View { - // MARK: Lifecycle - - init(service: CustomOpenAIService) { - self.service = service - self.viewModel = CustomOpenAIViewModel(service: service) - } - - // MARK: Internal - - let service: CustomOpenAIService - - var body: some View { - ServiceConfigurationSecretSectionView( - service: service, - observeKeys: [.customOpenAIAPIKey, .customOpenAIEndPoint, .customOpenAIAvailableModels] - ) { - // title - ServiceConfigurationInputCell( - textFieldTitleKey: "service.configuration.custom_openai.name.title", - key: .customOpenAINameKey, - placeholder: "custom_openai", - limitLength: 20 - ) - ServiceConfigurationSecureInputCell( - textFieldTitleKey: "service.configuration.openai.api_key.title", - key: .customOpenAIAPIKey, - placeholder: "service.configuration.openai.api_key.placeholder" - ) - // endpoint - ServiceConfigurationInputCell( - textFieldTitleKey: "service.configuration.openai.endpoint.title", - key: .customOpenAIEndPoint, - placeholder: "service.configuration.openai.endpoint.placeholder" - ) - - // models - TextEditorCell( - title: "service.configuration.custom_openai.supported_models.title", - text: viewModel.$availableModels ?? "", - placeholder: "service.configuration.custom_openai.model.placeholder" - ) - - Picker( - "service.configuration.openai.model.title", - selection: viewModel.$model - ) { - ForEach(viewModel.validModels, id: \.self) { value in - Text(value) - } - } - .padding(10.0) - - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.translation.title", - key: .customOpenAITranslation - ) - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.sentence.title", - key: .customOpenAISentence - ) - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.dictionary.title", - key: .customOpenAIDictionary - ) - ServiceConfigurationPickerCell( - titleKey: "service.configuration.openai.usage_status.title", - key: .customOpenAIServiceUsageStatus, - values: ServiceUsageStatus.allCases - ) - } - .onDisappear { - viewModel.invalidate() - } - } - - // MARK: Private - - @ObservedObject private var viewModel: CustomOpenAIViewModel -} - -// MARK: - CustomOpenAIViewModel - -private class CustomOpenAIViewModel: ObservableObject { - // MARK: Lifecycle - - init(service: CustomOpenAIService) { - self.service = service - Defaults.publisher(.customOpenAIModel, options: []) - .removeDuplicates() - .sink { _ in - self.modelChanged() - } - .store(in: &cancellables) - Defaults.publisher(.customOpenAINameKey, options: []) - .removeDuplicates() - .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) - .sink { _ in - self.serviceConfigChanged() - } - .store(in: &cancellables) - Defaults.publisher(.customOpenAIAvailableModels) - .removeDuplicates() - .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) - .sink { _ in - self.modelsTextChanged() - } - .store(in: &cancellables) - } - - // MARK: Internal - - let service: CustomOpenAIService - - @Default(.customOpenAIModel) var model - @Default(.customOpenAIAvailableModels) var availableModels - - @Published var validModels: [String] = [] - - func invalidate() { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() - } - - // MARK: Private - - private var cancellables: Set = [] - - private func modelChanged() { - if !validModels.contains(model) { - if model.isEmpty { - availableModels = "" - } else { - if availableModels?.isEmpty == true { - availableModels = model - } else { - availableModels = "\(model), " + (availableModels ?? "") - } - } - } - - serviceConfigChanged() - } - - private func modelsTextChanged() { - guard let availableModels else { return } - - validModels = availableModels.components(separatedBy: ",") - .map { $0.trim() }.filter { !$0.isEmpty } - - if validModels.isEmpty { - model = "" - } else if !validModels.contains(model) { - model = validModels[0] - } - - Defaults[.customOpenAIVaildModels] = validModels - } - - private func serviceConfigChanged() { - objectWillChange.send() - - let userInfo: [String: Any] = [ - EZWindowTypeKey: service.windowType.rawValue, - EZServiceTypeKey: service.serviceType().rawValue, - ] - let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo) - NotificationCenter.default.post(notification) - } -} diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift deleted file mode 100644 index 0568af977..000000000 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// GeminiService+ConfigurableService.swift -// Easydict -// -// Created by phlpsong on 2024/1/31. -// Copyright © 2024 izual. All rights reserved. -// - -import Combine -import Defaults -import Foundation -import SwiftUI - -// MARK: - GeminiService + ConfigurableService - -extension GeminiService: ConfigurableService { - func configurationListItems() -> some View { - GeminiServiceConfigurationView(service: self) - } -} - -// MARK: - GeminiServiceConfigurationView - -private struct GeminiServiceConfigurationView: View { - // MARK: Lifecycle - - init(service: GeminiService) { - self.service = service - self.viewModel = GeminiViewModel(service: service) - } - - // MARK: Internal - - let service: GeminiService - - var body: some View { - ServiceConfigurationSecretSectionView( - service: service, - observeKeys: [.geminiAPIKey, .geminiAvailableModels] - ) { - ServiceConfigurationSecureInputCell( - textFieldTitleKey: "service.configuration.openai.api_key.title", - key: .geminiAPIKey, - placeholder: "service.configuration.gemini.api_key.placeholder" - ) - // supported models - TextField( - "service.configuration.custom_openai.supported_models.title", - text: viewModel.$availableModels ?? "", - prompt: Text("service.configuration.custom_openai.model.placeholder") - ) - .padding(10.0) - Picker( - "service.configuration.openai.model.title", - selection: viewModel.$model - ) { - ForEach(viewModel.validModels, id: \.self) { value in - Text(value) - } - } - .padding(10.0) - - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.translation.title", - key: .geminiTranslation - ) - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.sentence.title", - key: .geminiSentence - ) - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.dictionary.title", - key: .geminiDictionary - ) - ServiceConfigurationPickerCell( - titleKey: "service.configuration.openai.usage_status.title", - key: .geminiServiceUsageStatus, - values: ServiceUsageStatus.allCases - ) - } - .onDisappear { - viewModel.invalidate() - } - } - - // MARK: Private - - @ObservedObject private var viewModel: GeminiViewModel -} - -// MARK: - GeminiViewModel - -private class GeminiViewModel: ObservableObject { - // MARK: Lifecycle - - init(service: GeminiService) { - self.service = service - Defaults.publisher(.geminiModel, options: []) - .removeDuplicates() - .sink { _ in - self.modelChanged() - } - .store(in: &cancellables) - Defaults.publisher(.geminiAvailableModels) - .removeDuplicates() - .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) - .sink { _ in - self.modelsTextChanged() - } - .store(in: &cancellables) - } - - // MARK: Internal - - let service: GeminiService - - @Default(.geminiModel) var model - @Default(.geminiAvailableModels) var availableModels - - @Published var validModels: [String] = [] - - func invalidate() { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() - } - - // MARK: Private - - private var cancellables: Set = [] - - private func modelChanged() { - if !validModels.contains(model) { - if model.isEmpty { - availableModels = "" - } else { - if availableModels?.isEmpty == true { - availableModels = model - } else { - availableModels = availableModels ?? "" - } - } - } - serviceConfigChanged() - } - - private func modelsTextChanged() { - guard let availableModels else { return } - - validModels = availableModels.components(separatedBy: ",") - .map { $0.trim() }.filter { !$0.isEmpty } - - if validModels.isEmpty { - model = "" - } else if !validModels.contains(model) { - model = validModels[0] - } - - Defaults[.geminiValidModels] = validModels - } - - private func serviceConfigChanged() { - objectWillChange.send() - - let userInfo: [String: Any] = [ - EZWindowTypeKey: service.windowType.rawValue, - EZServiceTypeKey: service.serviceType().rawValue, - ] - let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo) - NotificationCenter.default.post(notification) - } -} - -// MARK: - GeminiModel - -// swiftlint:disable identifier_name -enum GeminiModel: String, CaseIterable { - // Docs: https://ai.google.dev/gemini-api/docs/models/gemini - - // RPM: Requests per minute, TPM: Tokens per minute - // RPD: Requests per day, TPD: Tokens per day - case gemini1_0_pro = "gemini-1.0-pro" // Free 15 RPM/32,000 TPM, 1,500 RPD/46,080,000 TPD (n/a context length) - case gemini1_5_flash = "gemini-1.5-flash" // Free 15 RPM/100million TPM, 1500 RPD/ n/a TPD (1048k context length) - case gemini1_5_pro = "gemini-1.5-pro" // Free 2 RPM/32,000 TPM, 50 RPD/46,080,000 TPD (1048k context length) -} - -// swiftlint:enable identifier_name diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift deleted file mode 100644 index 59eb1a4b4..000000000 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// OpenAIService+ConfigurableService.swift -// Easydict -// -// Created by 戴藏龙 on 2024/1/14. -// Copyright © 2024 izual. All rights reserved. -// - -import Combine -import Defaults -import Foundation -import SwiftUI - -// MARK: - OpenAIService + ConfigurableService - -extension OpenAIService: ConfigurableService { - func configurationListItems() -> some View { - OpenAIServiceConfigurationView(service: self) - } -} - -// MARK: - OpenAIServiceConfigurationView - -private struct OpenAIServiceConfigurationView: View { - // MARK: Lifecycle - - init(service: OpenAIService) { - self.service = service - self.viewModel = OpenAIServiceViewModel(service: service) - } - - // MARK: Internal - - let service: OpenAIService - - var body: some View { - ServiceConfigurationSecretSectionView( - service: service, observeKeys: [.openAIAPIKey, .openAIEndPoint, .openAIAvailableModels] - ) { - 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" - ) - - // models - TextEditorCell( - title: "service.configuration.custom_openai.supported_models.title", - text: viewModel.$availableModels ?? "", - placeholder: "service.configuration.custom_openai.model.placeholder" - ) - - Picker( - "service.configuration.openai.model.title", - selection: viewModel.$model - ) { - ForEach(viewModel.validModels, id: \.self) { value in - Text(value) - } - } - .padding(10.0) - - 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: ServiceUsageStatus.allCases - ) - } - .onDisappear { - viewModel.invalidate() - } - } - - // MARK: Private - - @ObservedObject private var viewModel: OpenAIServiceViewModel -} - -// MARK: - OpenAIServiceViewModel - -private class OpenAIServiceViewModel: ObservableObject { - // MARK: Lifecycle - - init(service: OpenAIService) { - self.service = service - Defaults.publisher(.openAIModel, options: []) - .removeDuplicates() - .sink { _ in - self.modelChanged() - } - .store(in: &cancellables) - Defaults.publisher(.openAIAvailableModels) - .removeDuplicates() - .throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true) - .sink { _ in - self.modelsTextChanged() - } - .store(in: &cancellables) - } - - // MARK: Internal - - let service: OpenAIService - - @Default(.openAIModel) var model - @Default(.openAIAvailableModels) var availableModels - - @Published var validModels: [String] = [] - - func invalidate() { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() - } - - // MARK: Private - - private var cancellables: Set = [] - - private func modelChanged() { - // Currently, user of low os versions can change OpenAI model using URL scheme, like easydict://writeKeyValue?EZOpenAIModelKey=gpt-4 - // In this case, model may not be included in validModels, we need to handle it. - - if !validModels.contains(model) { - if model.isEmpty { - availableModels = "" - } else { - if availableModels?.isEmpty == true { - availableModels = model - } else { - availableModels = "\(model), " + (availableModels ?? "") - } - } - } - - serviceConfigChanged() - } - - private func modelsTextChanged() { - guard let availableModels else { return } - - validModels = availableModels.components(separatedBy: ",") - .map { $0.trim() }.filter { !$0.isEmpty } - - if validModels.isEmpty { - model = "" - } else if !validModels.contains(model) { - model = validModels[0] - } - - Defaults[.openAIVaildModels] = validModels - } - - private func serviceConfigChanged() { - objectWillChange.send() - - let userInfo: [String: Any] = [ - EZWindowTypeKey: service.windowType.rawValue, - EZServiceTypeKey: service.serviceType().rawValue, - ] - let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo) - NotificationCenter.default.post(notification) - } -} - -// MARK: - OpenAIModel - -// swiftlint:disable identifier_name -enum OpenAIModel: String, CaseIterable { - // Docs: https://platform.openai.com/docs/models - - case gpt3_5_turbo = "gpt-3.5-turbo" // Currently points to gpt-3.5-turbo-0125. Input: $0.50 | Output: $1.50 (16k) - case gpt4_turbo = "gpt-4-turbo" // Currently points to gpt-4-turbo-2024-04-09. Input: $10 | Output: $30 (128k) - case gpt4o = "gpt-4o" // Currently points to gpt-4o-2024-05-13. Input: $5 | Output: $15 (128k context length) -} - -// swiftlint:enable identifier_name diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/SecureTextField.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/SecureTextField.swift index 588d371bf..dbd6134e9 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/SecureTextField.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/SecureTextField.swift @@ -16,19 +16,19 @@ struct SecureTextField: View { let title: LocalizedStringKey let placeholder: LocalizedStringKey - @Binding var text: String? + @Binding var text: String var body: some View { HStack { ZStack { - SecureField(title, text: $text ?? "") + SecureField(title, text: $text) .lineLimit(lineLimit) .focused($focus, equals: .secure) .opacity(showText ? 0 : 1) - TextField(title, text: $text ?? "", prompt: Text(placeholder)) + TextField(title, text: $text, prompt: Text(placeholder)) .lineLimit(lineLimit) .focused($focus, equals: .text) - .opacity(showText || (text?.isEmpty ?? true) ? 1 : 0) + .opacity(showText || (text.isEmpty) ? 1 : 0) } Button(action: { diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationCells.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationCells.swift index d9cd9a55a..d2d0dd784 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationCells.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationCells.swift @@ -17,7 +17,7 @@ struct ServiceConfigurationSecureInputCell: View { init( textFieldTitleKey: LocalizedStringKey, - key: Defaults.Key, + key: Defaults.Key, placeholder: LocalizedStringKey = "service.configuration.input.placeholder" ) { self.textFieldTitleKey = textFieldTitleKey @@ -27,7 +27,7 @@ struct ServiceConfigurationSecureInputCell: View { // MARK: Internal - @Default var value: String? + @Default var value: String let textFieldTitleKey: LocalizedStringKey let placeholder: LocalizedStringKey @@ -43,7 +43,7 @@ struct ServiceConfigurationInputCell: View { init( textFieldTitleKey: LocalizedStringKey, - key: Defaults.Key, + key: Defaults.Key, placeholder: LocalizedStringKey, limitLength: Int = Int.max ) { @@ -55,14 +55,14 @@ struct ServiceConfigurationInputCell: View { // MARK: Internal - @Default var value: String? + @Default var value: String let textFieldTitleKey: LocalizedStringKey let placeholder: LocalizedStringKey var limitLength: Int var body: some View { - TextField(textFieldTitleKey, text: $value ?? "", prompt: Text(placeholder)) + TextField(textFieldTitleKey, text: $value, prompt: Text(placeholder)) .padding(10.0) .onReceive(Just(value)) { _ in limit(limitLength) @@ -72,8 +72,8 @@ struct ServiceConfigurationInputCell: View { // MARK: Private private func limit(_ max: Int) { - if let value, value.count > max { - self.value = String(value.prefix(max)) + if value.count > max { + value = String(value.prefix(max)) } } } @@ -105,10 +105,55 @@ struct ServiceConfigurationPickerCell: View { + // MARK: Lifecycle + + init(titleKey: LocalizedStringKey, selectionKey: Defaults.Key, valuesKey: Defaults.Key<[T]>) { + self.titleKey = titleKey + _selection = .init(selectionKey) + _values = .init(valuesKey) + } + + // MARK: Internal + + let titleKey: LocalizedStringKey + + @Default var selection: T + @Default var values: [T] + + var body: some View { + Picker(titleKey, selection: $selection) { + ForEach(values, id: \.self) { value in + Text(value.title) + } + } + .padding(10.0) + } +} + +// MARK: - ToggleViewModel + +class ToggleViewModel: ObservableObject { + // MARK: Lifecycle + + init(key: Defaults.Key) { + self.key = key + self._value = .init(key) + self.isOn = Defaults[key] == "1" + } -class ConfigurationToggleViewModel: ObservableObject { - @Published var isOn = false + // MARK: Internal + + let key: Defaults.Key + @Default var value: String + + @Published var isOn: Bool { + didSet { + value = isOn ? "1" : "0" + } + } } // MARK: - ServiceConfigurationToggleCell @@ -118,49 +163,18 @@ struct ServiceConfigurationToggleCell: View { init(titleKey: LocalizedStringKey, key: Defaults.Key) { self.titleKey = titleKey - _value = .init(key) - viewModel.isOn = value == "1" + self.viewModel = ToggleViewModel(key: key) } // MARK: Internal - @Default var value: String let titleKey: LocalizedStringKey - @ObservedObject var viewModel = ConfigurationToggleViewModel() + // Since we previously used String for the toggle value, we have to connect String <--> Bool with a viewModel. + @ObservedObject var viewModel: ToggleViewModel var body: some View { Toggle(titleKey, isOn: $viewModel.isOn) .padding(10.0) - .onChange(of: viewModel.isOn) { newValue in - value = newValue ? "1" : "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" - ) - - ServiceConfigurationPickerCell( - titleKey: "service.configuration.openai.usage_status.title", - key: .openAIServiceUsageStatus, - values: ServiceUsageStatus.allCases - ) - - ServiceConfigurationToggleCell( - titleKey: "service.configuration.openai.translation.title", - key: .openAITranslation - ) } } diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift index cb1da2d17..7888c4bdc 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift @@ -17,15 +17,15 @@ struct ServiceConfigurationSecretSectionView: View { init( service: QueryService, - observeKeys: [Defaults.Key], + observeKeys: [Defaults.Key], @ViewBuilder content: () -> Content ) { self.service = service self.content = content() - self._viewModel = StateObject(wrappedValue: ServiceValidationViewModel( + self.viewModel = ServiceValidationViewModel( service: service, observing: observeKeys - )) + ) } // MARK: Internal @@ -107,7 +107,7 @@ struct ServiceConfigurationSecretSectionView: View { // MARK: Private - @StateObject private var viewModel: ServiceValidationViewModel + @ObservedObject private var viewModel: ServiceValidationViewModel } // MARK: - ServiceValidationViewModel @@ -116,20 +116,22 @@ struct ServiceConfigurationSecretSectionView: View { private class ServiceValidationViewModel: ObservableObject { // MARK: Lifecycle - init(service: QueryService, observing keys: [Defaults.Key]) { + init(service: QueryService, observing keys: [Defaults.Key]) { self.service = service self.name = service.name() + // check secret key empty input Defaults.publisher(keys: keys) .throttle(for: 0.5, scheduler: DispatchQueue.main, latest: true) .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } - let hasEmptyInput = keys.contains(where: { (Defaults[$0] ?? "").isEmpty }) + let hasEmptyInput = keys.contains(where: { Defaults[$0].isEmpty }) guard isValidateBtnDisabled != hasEmptyInput else { return } self.isValidateBtnDisabled = hasEmptyInput } .store(in: &cancellables) + serviceUpdatePublisher .sink { [weak self] notification in self?.didReceive(notification) diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/TextEditorCell.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/TextEditorCell.swift index f0ea569ca..f5f4fed34 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/TextEditorCell.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/TextEditorCell.swift @@ -6,22 +6,35 @@ // Copyright © 2024 izual. All rights reserved. // +import Defaults import SwiftUI // MARK: - TextEditorCell struct TextEditorCell: View { + // MARK: Lifecycle + + init( + titleKey: LocalizedStringKey, + storedValueKey: Defaults.Key, + placeholder: LocalizedStringKey + ) { + self.titleKey = titleKey + self.placeholder = placeholder + _value = .init(storedValueKey) + } + // MARK: Internal - let title: LocalizedStringKey - @Binding var text: String + let titleKey: LocalizedStringKey + @Default var value: String let placeholder: LocalizedStringKey var body: some View { HStack(alignment: .center, spacing: 20) { - Text(title) + Text(titleKey) - TrailingTextEditorWithPlaceholder(text: $text, placeholder: placeholder) + TrailingTextEditorWithPlaceholder(text: $value, placeholder: placeholder) .padding(.horizontal, 3) .padding(.top, 5) .padding(.bottom, 7) @@ -60,7 +73,7 @@ struct TrailingTextEditorWithPlaceholder: View { .padding(.horizontal, 5) .background(GeometryReader { geometry in Color.clear.onAppear { - // 22 is one line height, if placeholder is more than one line, alway set alignment to .leading + // 22 is one line height, if placeholder is more than one line, always set alignment to .leading if geometry.size.height > 22 { oneLineAlignment = .topLeading } diff --git a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift index f29ea782e..e6a606efa 100644 --- a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift +++ b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift @@ -32,17 +32,15 @@ struct ServiceTab: View { 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()) + if let view = service.configurationListItems() as? (any View) { + Form { + AnyView(view) + } + .formStyle(.grouped) + } else { HStack { Spacer() - // No configuration for service xxx Text("setting.service.detail.no_configuration \(service.name())") Spacer() } @@ -106,9 +104,7 @@ private class ServiceTabViewModel: ObservableObject { } func postUpdateServiceNotification() { - let userInfo: [String: Any] = [EZWindowTypeKey: windowType.rawValue] - let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo) - NotificationCenter.default.post(notification) + NotificationCenter.default.postServiceUpdateNotification(windowType: windowType) } } diff --git a/Easydict/objc/Service/Model/EZConstKey.h b/Easydict/objc/Service/Model/EZConstKey.h index 4a94c7ed9..60f716567 100644 --- a/Easydict/objc/Service/Model/EZConstKey.h +++ b/Easydict/objc/Service/Model/EZConstKey.h @@ -13,44 +13,6 @@ NS_ASSUME_NONNULL_BEGIN static NSString *const EZBetaFeatureKey = @"EZBetaFeatureKey"; -static NSString *const EZServiceUsageStatusKey = @"ServiceUsageStatus"; -static NSString *const EZTranslationKey = @"Translation"; -static NSString *const EZDictionaryKey = @"Dictionary"; -static NSString *const EZSentenceKey = @"Sentence"; -static NSString *const EZAvailableModelsKey = @"AvailableModels"; -static NSString *const EZValidModelsKey = @"ValidModels"; -static NSString *const EZModelKey = @"Model"; -static NSString *const EZAPIKey = @"API"; -static NSString *const EZEndpointKey = @"EndPoint"; -static NSString *const EZNameKey = @"Name"; - -// OpenAI -static NSString *const EZOpenAIAPIKey = @"EZOpenAIAPIKey"; -static NSString *const EZOpenAIEndPointKey = @"EZOpenAIEndPointKey"; -static NSString *const EZOpenAITranslationKey = @"EZOpenAITranslationKey"; -static NSString *const EZOpenAIDictionaryKey = @"EZOpenAIDictionaryKey"; -static NSString *const EZOpenAISentenceKey = @"EZOpenAISentenceKey"; -static NSString *const EZOpenAIServiceUsageStatusKey = @"EZOpenAIServiceUsageStatusKey"; -static NSString *const EZOpenAIModelKey = @"EZOpenAIModelKey"; -static NSString *const EZOpenAIAvailableModelsKey = @"EZOpenAIAvailableModelsKey"; -static NSString *const EZOpenAIValidModelsKey = @"EZOpenAIValidModelsKey"; - -// Custom OpenAI -static NSString *const EZCustomOpenAINameKey = @"EZCustomOpenAINameKey"; -static NSString *const EZCustomOpenAIEndPointKey = @"EZCustomOpenAIEndPointKey"; -static NSString *const EZCustomOpenAIAPIKey = @"EZCustomOpenAIAPIKey"; -static NSString *const EZCustomOpenAITranslationKey = @"EZCustomOpenAITranslationKey"; -static NSString *const EZCustomOpenAIDictionaryKey = @"EZCustomOpenAIDictionaryKey"; -static NSString *const EZCustomOpenAISentenceKey = @"EZCustomOpenAISentenceKey"; -static NSString *const EZCustomOpenAIServiceUsageStatusKey = @"EZCustomOpenAIServiceUsageStatusKey"; -static NSString *const EZCustomOpenAIAvailableModelsKey = @"EZCustomOpenAIAvailableModelsKey"; -static NSString *const EZCustomOpenAIModelKey = @"EZCustomOpenAIModelKey"; -static NSString *const EZCustomOpenAIValidModelsKey = @"EZCustomOpenAIValidModelsKey"; - -// Built-in AI -static NSString *const EZBuiltInAIModelKey = @"EZBuiltInAIModelKey"; - - static NSString *const EZDeepLAuthKey = @"EZDeepLAuthKey"; static NSString *const EZDeepLTranslateEndPointKey = @"EZDeepLTranslateEndPointKey"; diff --git a/Easydict/objc/Service/Model/EZQueryService.h b/Easydict/objc/Service/Model/EZQueryService.h index 95db440e6..0345c377e 100644 --- a/Easydict/objc/Service/Model/EZQueryService.h +++ b/Easydict/objc/Service/Model/EZQueryService.h @@ -65,6 +65,8 @@ NS_SWIFT_NAME(QueryService) - (void)startQuery:(EZQueryModel *)queryModel completion:(void (^)(EZQueryResult *result, NSError *_Nullable error))completion; +- (nullable id)configurationListItems; + @end diff --git a/Easydict/objc/Service/Model/EZQueryService.m b/Easydict/objc/Service/Model/EZQueryService.m index f5a5f124c..3d6de6d37 100644 --- a/Easydict/objc/Service/Model/EZQueryService.m +++ b/Easydict/objc/Service/Model/EZQueryService.m @@ -35,6 +35,10 @@ - (instancetype)init { return self; } +- (void)dealloc { + MMLogInfo(@"dealloc service: %@", self); +} + - (EZAudioPlayer *)audioPlayer { if (!_audioPlayer) { _audioPlayer = [[EZAudioPlayer alloc] init]; @@ -212,6 +216,10 @@ - (void)startQuery:(EZQueryModel *)queryModel [self translate:queryText from:from to:to completion:completion]; } +- (nullable id)configurationListItems { + return nil; +} + #pragma mark - 必须重写的子类方法 - (EZServiceType)serviceType { diff --git a/Easydict/objc/Utility/EZLinkParser/EZSchemeParser.m b/Easydict/objc/Utility/EZLinkParser/EZSchemeParser.m index 33e1ba0fc..826ecb02c 100644 --- a/Easydict/objc/Utility/EZLinkParser/EZSchemeParser.m +++ b/Easydict/objc/Utility/EZLinkParser/EZSchemeParser.m @@ -192,23 +192,6 @@ - (NSArray *)allowedReadWriteKeys { NSArray *readWriteKeys = @[ EZBetaFeatureKey, - EZOpenAIAPIKey, - EZOpenAIDictionaryKey, - EZOpenAISentenceKey, - EZOpenAIServiceUsageStatusKey, - EZOpenAIModelKey, - EZOpenAIAvailableModelsKey, - - EZCustomOpenAINameKey, - EZCustomOpenAIEndPointKey, - EZCustomOpenAIAPIKey, - EZCustomOpenAIAvailableModelsKey, - EZCustomOpenAIModelKey, - EZCustomOpenAITranslationKey, - EZCustomOpenAIDictionaryKey, - EZCustomOpenAISentenceKey, - EZCustomOpenAIServiceUsageStatusKey, - EZYoudaoTranslationKey, EZYoudaoDictionaryKey, @@ -223,7 +206,6 @@ - (NSArray *)allowedReadWriteKeys { EZAliAccessKeyId, EZAliAccessKeySecret, - EZGeminiAPIKey, EZIntelligentQueryModeKey, ]; @@ -304,31 +286,4 @@ - (NSDictionary *)getKeyValues:(NSString *)text { return dict; } -- (NSString *)keyValuesOfServiceType:(EZServiceType)serviceType key:(NSString *)key value:(NSString *)value { - /** - easydict://writeKeyValue?ServiceType=OpenAI&ServiceUsageStatus=1 - - easydict://writeKeyValue?OpenAIServiceUsageStatus=1 - - easydict://writeKeyValue?OpenAIQueryServiceType=1 - */ - NSString *keyValueString = @""; - - NSArray *allowdKeyNames = @[ - EZServiceUsageStatusKey, - EZQueryTextTypeKey, - ]; - - NSArray *allServiceTypes = [EZServiceTypes.shared allServiceTypes]; - - BOOL validKey = [allServiceTypes containsObject:serviceType] && [allowdKeyNames containsObject:key]; - - if (validKey) { - NSString *keyString = [NSString stringWithFormat:@"%@%@", serviceType, key]; - keyValueString = [NSString stringWithFormat:@"%@=%@", keyString, value]; - } - - return keyValueString; -} - @end diff --git a/Easydict/objc/ViewController/Storage/EZLocalStorage.h b/Easydict/objc/ViewController/Storage/EZLocalStorage.h index 96894fb65..7166f31a6 100644 --- a/Easydict/objc/ViewController/Storage/EZLocalStorage.h +++ b/Easydict/objc/ViewController/Storage/EZLocalStorage.h @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN static NSString *const EZServiceHasUpdatedNotification = @"EZServiceHasUpdatedNotification"; static NSString *const EZWindowTypeKey = @"EZWindowTypeKey"; +static NSString *const EZAutoQueryKey = @"EZAutoQueryKey"; @interface EZLocalStorage : NSObject diff --git a/Easydict/objc/ViewController/Storage/QueryServiceRecord.swift b/Easydict/objc/ViewController/Storage/QueryServiceRecord.swift index 7e43c8ba1..ff4405317 100644 --- a/Easydict/objc/ViewController/Storage/QueryServiceRecord.swift +++ b/Easydict/objc/ViewController/Storage/QueryServiceRecord.swift @@ -13,7 +13,7 @@ public class QueryServiceRecord: NSObject { // MARK: Lifecycle @objc - override public init() {} + public override init() {} @objc init(serviceType: ServiceType = .apple, queryCount: Int = 0, queryCharacterCount: Int = 0) { diff --git a/Easydict/objc/ViewController/View/ResultView/EZResultView.m b/Easydict/objc/ViewController/View/ResultView/EZResultView.m index 957533a72..120ed7df5 100644 --- a/Easydict/objc/ViewController/View/ResultView/EZResultView.m +++ b/Easydict/objc/ViewController/View/ResultView/EZResultView.m @@ -436,7 +436,7 @@ - (BOOL)isLLLStreamService:(EZQueryService *)service { - (void)showModelSelectionMenu:(EZButton *)sender { EZLLMStreamService *service = (EZLLMStreamService *)self.result.service; NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Menu"]; - for (NSString *model in service.availableModels) { + for (NSString *model in service.validModels) { NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:model action:@selector(modelDidSelected:) keyEquivalent:@""]; item.target = self; [menu addItem:item]; @@ -449,16 +449,9 @@ - (void)modelDidSelected:(NSMenuItem *)sender { if (![service.model isEqualToString:sender.title]) { service.model = sender.title; self.serviceModelButton.title = service.model; - [self postServiceUpdatedNotification:service.serviceType]; } } -- (void)postServiceUpdatedNotification:(EZServiceType)serviceType { - NSDictionary *userInfo = @{EZServiceTypeKey : serviceType}; - NSNotification *notification = [NSNotification notificationWithName:EZServiceHasUpdatedNotification object:nil userInfo:userInfo]; - [[NSNotificationCenter defaultCenter] postNotification:notification]; -} - #pragma mark - Animation - (void)rotateArrowButton { diff --git a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index 0adb856c5..0bcf5ed77 100644 --- a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -144,7 +144,7 @@ - (void)setupUI { // Avoid recycling call, resize window --> update window height --> resize window if (self.lockResizeWindow) { -// MMLogInfo(@"lockResizeWindow"); + // MMLogInfo(@"lockResizeWindow"); return; } @@ -210,6 +210,8 @@ - (void)setupServices:(NSArray *)allServices { [services addObject:service]; [serviceTypes addObject:service.serviceType]; + + [self trySetupSubscribersForService:service oldService:nil]; } EZServiceType serviceType = service.serviceType; @@ -233,7 +235,8 @@ - (void)setupServices:(NSArray *)allServices { - (void)dealloc { MMLogInfo(@"dealloc: %@", self); - [[NSNotificationCenter defaultCenter] removeObserver:self]; + [NSNotificationCenter.defaultCenter removeObserver:self]; + [NSNotificationCenter.defaultCenter removeObserver:self name:NotificationName.didChangeFontSize object:nil]; } #pragma mark - NSNotificationCenter @@ -244,16 +247,28 @@ - (void)activeDictionariesChanged:(NSNotification *)notification { - (void)handleServiceUpdate:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; - EZWindowType type = [userInfo[EZWindowTypeKey] integerValue]; - NSString *serviceType = [notification.userInfo objectForKey:EZServiceTypeKey]; - MMLogInfo(@"Notify to update service: %@", serviceType); + EZWindowType windowType = [userInfo[EZWindowTypeKey] integerValue]; + NSString *serviceType = userInfo[EZServiceTypeKey]; + BOOL autoQuery = [userInfo[EZAutoQueryKey] boolValue]; + + MMLogInfo(@"handle service update notification: %@, userInfo: %@", serviceType, userInfo); + + // If window is deallocing, we should not continue to update. + if (GlobalContext.shared.subscribeWindowType == EZWindowTypeNone && self.windowType == EZWindowTypeMain) { + return; + } if ([serviceType length] != 0) { - [self updateService:serviceType autoQuery:YES]; + [self updateService:serviceType autoQuery:autoQuery]; return; } - if (type == self.windowType || !userInfo) { - [self resetAllCellWithServices:[self latestServices]]; + + if (!userInfo || windowType == self.windowType || windowType == EZWindowTypeNone) { + [self resetAllCellWithServices:[self latestServices] completion:^{ + if (autoQuery) { + [self queryCurrentModel]; + } + }]; } } @@ -424,19 +439,13 @@ - (BOOL)handleEasydictScheme:(NSString *)text { // If write, need to update. if (actionKey && [self.schemeParser isWriteActionKey:actionKey]) { // Besides current window, other pages need to be notified, such as the settings service page. - [self postUpdateServiceNotification]; + [NSNotificationCenter.defaultCenter postServiceUpdateNotification]; } }]; return YES; } -- (void)postUpdateServiceNotification { - // Need to update all types window. - NSNotification *notification = [NSNotification notificationWithName:EZServiceHasUpdatedNotification object:nil userInfo:nil]; - [[NSNotificationCenter defaultCenter] postNotification:notification]; -} - - (void)startOCRImage:(NSImage *)image actionType:(EZActionType)actionType { MMLogInfo(@"start OCR Image: %@, actionType: %@", @(image.size), actionType); @@ -770,7 +779,7 @@ - (void)queryWithModel:(EZQueryModel *)queryModel service:(EZQueryService *)serv result.isShowing = NO; } -// MMLogInfo(@"update service: %@, %@", service.serviceType, result); + // MMLogInfo(@"update service: %@, %@", service.serviceType, result); [self updateCellWithResult:result reloadData:YES]; if (service.autoCopyTranslatedTextBlock) { @@ -792,7 +801,7 @@ - (void)queryWithModel:(EZQueryModel *)queryModel return; } -// MMLogInfo(@"query service: %@", service.serviceType); + // MMLogInfo(@"query service: %@", service.serviceType); EZQueryResult *result = service.result; @@ -822,7 +831,7 @@ - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView { // View-base 设置某个元素的具体视图 - (nullable NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(nullable NSTableColumn *)tableColumn row:(NSInteger)row { -// MMLogInfo(@"tableView for row: %ld", row); + // MMLogInfo(@"tableView for row: %ld", row); if (row == 0) { self.queryView = [self createQueryView]; @@ -896,7 +905,7 @@ - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row { height = EZResultViewMiniHeight; } } -// MMLogInfo(@"row: %ld, height: %@", row, @(height)); + // MMLogInfo(@"row: %ld, height: %@", row, @(height)); return height; } @@ -987,7 +996,7 @@ - (void)updateTableViewRowIndexes:(NSIndexSet *)rowIndexes reloadData:(BOOL)reloadData animate:(BOOL)animateFlag completionHandler:(void (^)(void))completionHandler { -// MMLogInfo(@"updateTableViewRowIndexes: %@", rowIndexes); + // MMLogInfo(@"updateTableViewRowIndexes: %@", rowIndexes); // !!!: Since the caller may be in non-main thread, we need to dispatch to main thread, but canont always use dispatch_async, it will cause the animation not smooth. dispatch_block_on_main_safely(^{ @@ -1004,11 +1013,11 @@ - (void)updateTableViewRowIndexes:(NSIndexSet *)rowIndexes [NSAnimationContext runAnimationGroup:^(NSAnimationContext *_Nonnull context) { context.duration = duration; // !!!: Must first notify the update tableView cell height, and then calculate the tableView height. -// MMLogInfo(@"noteHeightOfRowsWithIndexesChanged: %@", rowIndexes); + // MMLogInfo(@"noteHeightOfRowsWithIndexesChanged: %@", rowIndexes); [self.tableView noteHeightOfRowsWithIndexesChanged:rowIndexes]; [self updateWindowViewHeight]; } completionHandler:^{ -// MMLogInfo(@"completionHandler, updateTableViewRowIndexes: %@", rowIndexes); + // MMLogInfo(@"completionHandler, updateTableViewRowIndexes: %@", rowIndexes); if (completionHandler) { completionHandler(); } @@ -1119,21 +1128,27 @@ - (void)updateService:(NSString *)serviceType autoQuery:(BOOL)autoQuery { NSMutableArray *newServices = [self.services mutableCopy]; for (EZQueryService *service in self.services) { if (service.serviceType == serviceType) { + if (!autoQuery) { + [self updateCellWithResult:service.result reloadData:YES completionHandler:nil]; + return; + } + EZQueryService *updatedService = [EZLocalStorage.shared service:serviceType windowType:self.windowType]; + [self trySetupSubscribersForService:updatedService oldService:service]; NSInteger index = [self.serviceTypes indexOfObject:serviceType]; newServices[index] = updatedService; self.services = newServices.copy; - if (autoQuery) { - [self resetCellWithService:updatedService autoQuery:autoQuery]; - } - break; + + [self resetCellWithService:updatedService autoQuery:autoQuery]; + + return; } } } -- (void)resetAllCellWithServices:(NSArray *)allServices { +- (void)resetAllCellWithServices:(NSArray *)allServices completion:(void (^)(void))completion { [self setupServices:allServices]; - [self reloadTableViewData:nil]; + [self reloadTableViewData:completion]; } /// Get latest services from local storage. @@ -1141,6 +1156,29 @@ - (void)resetAllCellWithServices:(NSArray *)allServices { return [EZLocalStorage.shared allServices:self.windowType]; } +- (void)trySetupSubscribersForService:(EZQueryService *)service oldService:(nullable EZQueryService *)oldService { + // TODO: We should set subscribers when init EZLLMStreamService. + + /** + We only setup subscribers in `one` query window, we do not need extra notification in other place when init(), like settings. + + When notify a service configuration changed, it will init a new service, this is bad. + But for some strange reason, the old service can not be deallocated, this will cause a memory leak, and we also need to cancel old services subscribers. + + This code is so ugly, we should fix it later. + */ + + BOOL enableSubscribe = GlobalContext.shared.subscribeWindowType == EZWindowTypeNone || GlobalContext.shared.subscribeWindowType == self.windowType; + + if ([service isKindOfClass:EZLLMStreamService.class] && enableSubscribe) { + [((EZLLMStreamService *)service) setupSubscribers]; + [((EZLLMStreamService *)oldService) cancelSubscribers]; + + GlobalContext.shared.subscribeWindowType = self.windowType; + } +} + + #pragma mark - Update Data. - (void)resetQueryAndResults { @@ -1171,7 +1209,7 @@ - (nullable EZResultView *)resultCellOfResult:(EZQueryResult *)result { return resultCell; } } - + return nil; } @@ -1281,7 +1319,7 @@ - (EZQueryView *)createQueryView { mm_weakify(self); [queryView setUpdateInputTextBlock:^(NSString *text, CGFloat queryViewHeight) { mm_strongify(self); -// MMLogInfo(@"UpdateQueryTextBlock"); + // MMLogInfo(@"UpdateQueryTextBlock"); // !!!: The code here is a bit messy, so you need to be careful about changing it. @@ -1503,11 +1541,11 @@ - (void)updateWindowViewHeightWithLock:(BOOL)lockFlag self.lockResizeWindow = YES; } -// MMLogInfo(@"updateWindowViewHeightWithLock"); + // MMLogInfo(@"updateWindowViewHeightWithLock"); CGFloat tableViewHeight = [self getScrollViewContentHeight]; CGFloat height = [self getRestrainedScrollViewHeight:tableViewHeight]; -// MMLogInfo(@"getRestrainedScrollViewHeight: %@", @(height)); + // MMLogInfo(@"getRestrainedScrollViewHeight: %@", @(height)); CGSize maxWindowSize = [EZLayoutManager.shared maximumWindowSize:self.windowType]; @@ -1551,7 +1589,7 @@ - (void)updateWindowViewHeightWithLock:(BOOL)lockFlag self.lockResizeWindow = NO; } -// MMLogInfo(@"window frame: %@", @(window.frame)); + // MMLogInfo(@"window frame: %@", @(window.frame)); } - (CGFloat)getRestrainedScrollViewHeight:(CGFloat)scrollViewContentHeight { @@ -1573,10 +1611,10 @@ - (CGFloat)getScrollViewContentHeight { NSInteger rowCount = [self numberOfRowsInTableView:self.tableView]; for (int i = 0; i < rowCount; i++) { CGFloat rowHeight = [self tableView:self.tableView heightOfRow:i]; -// MMLogInfo(@"row: %d, Height: %.1f", i, rowHeight); + // MMLogInfo(@"row: %d, Height: %.1f", i, rowHeight); scrollViewContentHeight += (rowHeight + EZVerticalCellSpacing_7); } -// MMLogInfo(@"scrollViewContentHeight: %.1f", scrollViewContentHeight); + // MMLogInfo(@"scrollViewContentHeight: %.1f", scrollViewContentHeight); return scrollViewContentHeight; @@ -1588,7 +1626,7 @@ - (CGFloat)getContentHeight { self.scrollView.height = 0; CGFloat documentViewHeight = self.scrollView.documentView.height; // actually is tableView height -// MMLogInfo(@"documentView height: %@", @(documentViewHeight)); + // MMLogInfo(@"documentView height: %@", @(documentViewHeight)); return documentViewHeight; } diff --git a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m index 7af33c460..fa5c6bbcc 100644 --- a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m +++ b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m @@ -82,7 +82,7 @@ - (void)setQueryViewController:(EZBaseQueryViewController *)viewController { - (void)setPin:(BOOL)pin { _pin = pin; - + // !!!: Do not use kCGMaximumWindowLevel, otherwise it will obscure the tooltip. NSWindowLevel level = self.pin ? kCGUtilityWindowLevel : kCGNormalWindowLevel; self.level = level; @@ -104,13 +104,20 @@ - (NSTimeInterval)animationResizeTime:(NSRect)newFrame { - (void)dealloc { - MMLogInfo(@"dealloc: %@", self); + MMLogInfo(@"dealloc query window: %@", self); + + // We should reset subscribeWindowType when query window dealloc. + if (GlobalContext.shared.subscribeWindowType == self.windowType) { + GlobalContext.shared.subscribeWindowType = EZWindowTypeNone; + + [NSNotificationCenter.defaultCenter postServiceUpdateNotification]; + } } #pragma mark - NSWindowDelegate, NSNotification - (void)windowDidBecomeKey:(NSNotification *)notification { -// MMLogInfo(@"windowDidBecomeKey: %@", self); + // MMLogInfo(@"windowDidBecomeKey: %@", self); // We need to update the window type when the window becomes the key window. [EZWindowManager.shared updateFloatingWindowType:self.windowType isShowing:YES]; @@ -121,15 +128,15 @@ - (void)windowDidBecomeKey:(NSNotification *)notification { } - (void)windowDidResignKey:(NSNotification *)notification { -// MMLogInfo(@"windowDidResignKey: %@", self); + // MMLogInfo(@"windowDidResignKey: %@", self); // Close floating window when losing focus if it's not pinned or main window. [EZWindowManager.shared closeFloatingWindowIfNotPinned:self.windowType exceptWindowType:EZWindowTypeMain]; } - (void)windowDidResize:(NSNotification *)aNotification { -// MMLog(@"windowDidResize: %@, windowType: %ld", @(self.frame), self.windowType); - + // MMLog(@"windowDidResize: %@, windowType: %ld", @(self.frame), self.windowType); + [[EZLayoutManager shared] updateWindowFrame:self]; if (self.resizeWindowBlock) { @@ -151,7 +158,7 @@ - (BOOL)windowShouldClose:(NSWindow *)sender { // Window is hidden or showing. - (void)windowDidChangeOcclusionState:(NSNotification *)notification { -// MMLogInfo(@"window Did Change Occlusion State"); + // MMLogInfo(@"window Did Change Occlusion State"); // Window is obscured if (self.occlusionState != NSWindowOcclusionStateVisible) { diff --git a/Easydict/objc/ViewController/Window/WindowManager/EZWindowManager.m b/Easydict/objc/ViewController/Window/WindowManager/EZWindowManager.m index 62baf1ad2..fcfe99ff5 100644 --- a/Easydict/objc/ViewController/Window/WindowManager/EZWindowManager.m +++ b/Easydict/objc/ViewController/Window/WindowManager/EZWindowManager.m @@ -689,6 +689,7 @@ - (void)destroyMainWindow { if ([EZMainQueryWindow isAlive]) { [EZMainQueryWindow destroySharedInstance]; + _mainWindow = nil; } } diff --git a/Podfile b/Podfile index 10562a015..979ffbfa8 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ target 'Easydict' do pod 'JLRoutes', '~> 2.1' # Swift format - pod 'SwiftFormat/CLI', '~> 0.53.2' + pod 'SwiftFormat/CLI', '~> 0.54' pod 'SwiftLint', '~> 0.54.0' end diff --git a/Podfile.lock b/Podfile.lock index 88a90eaea..bae33f8b7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -16,7 +16,7 @@ PODS: - KVOController (1.2.0) - Masonry (1.1.0) - ReactiveObjC (3.1.1) - - SwiftFormat/CLI (0.53.10) + - SwiftFormat/CLI (0.54.0) - SwiftLint (0.54.0) DEPENDENCIES: @@ -25,7 +25,7 @@ DEPENDENCIES: - KVOController (~> 1.2.0) - Masonry (~> 1.1.0) - ReactiveObjC (~> 3.1.1) - - SwiftFormat/CLI (~> 0.53.2) + - SwiftFormat/CLI (~> 0.54) - SwiftLint (~> 0.54.0) SPEC REPOS: @@ -44,9 +44,9 @@ SPEC CHECKSUMS: KVOController: d72ace34afea42468329623b3379ab3cd1d286b6 Masonry: 678fab65091a9290e40e2832a55e7ab731aad201 ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040 - SwiftFormat: 5967522a8e82d562b2508363d3ddec424fee1e9e + SwiftFormat: 0e0b577434e6aa63bc82a8905b40d9597b8452d4 SwiftLint: c1de071d9d08c8aba837545f6254315bc900e211 -PODFILE CHECKSUM: 3c89974b4597474c7f2881849bf1cc4d5b0c774f +PODFILE CHECKSUM: b38d6c42d6418a0fcc74bc0514c2f683236f940f COCOAPODS: 1.14.2 diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock index 88a90eaea..bae33f8b7 100644 --- a/Pods/Manifest.lock +++ b/Pods/Manifest.lock @@ -16,7 +16,7 @@ PODS: - KVOController (1.2.0) - Masonry (1.1.0) - ReactiveObjC (3.1.1) - - SwiftFormat/CLI (0.53.10) + - SwiftFormat/CLI (0.54.0) - SwiftLint (0.54.0) DEPENDENCIES: @@ -25,7 +25,7 @@ DEPENDENCIES: - KVOController (~> 1.2.0) - Masonry (~> 1.1.0) - ReactiveObjC (~> 3.1.1) - - SwiftFormat/CLI (~> 0.53.2) + - SwiftFormat/CLI (~> 0.54) - SwiftLint (~> 0.54.0) SPEC REPOS: @@ -44,9 +44,9 @@ SPEC CHECKSUMS: KVOController: d72ace34afea42468329623b3379ab3cd1d286b6 Masonry: 678fab65091a9290e40e2832a55e7ab731aad201 ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040 - SwiftFormat: 5967522a8e82d562b2508363d3ddec424fee1e9e + SwiftFormat: 0e0b577434e6aa63bc82a8905b40d9597b8452d4 SwiftLint: c1de071d9d08c8aba837545f6254315bc900e211 -PODFILE CHECKSUM: 3c89974b4597474c7f2881849bf1cc4d5b0c774f +PODFILE CHECKSUM: b38d6c42d6418a0fcc74bc0514c2f683236f940f COCOAPODS: 1.14.2 diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj index 0cd7f5e0c..f4dc6acb2 100644 --- a/Pods/Pods.xcodeproj/project.pbxproj +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 55; objects = { /* Begin PBXAggregateTarget section */ @@ -1543,7 +1543,7 @@ LastUpgradeCheck = 1500; }; buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; - compatibilityVersion = "Xcode 15.0"; + compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( diff --git a/Pods/SwiftFormat/CommandLineTool/swiftformat b/Pods/SwiftFormat/CommandLineTool/swiftformat index e1e26447c..bfd28635c 100755 Binary files a/Pods/SwiftFormat/CommandLineTool/swiftformat and b/Pods/SwiftFormat/CommandLineTool/swiftformat differ diff --git a/Pods/SwiftFormat/README.md b/Pods/SwiftFormat/README.md index e21a4b820..db054fc50 100644 --- a/Pods/SwiftFormat/README.md +++ b/Pods/SwiftFormat/README.md @@ -252,7 +252,7 @@ let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.9"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.0"), ], targets: [.target(name: "BuildTools", path: "")] ) @@ -286,7 +286,7 @@ You can also use `swift run -c release --package-path BuildTools swiftformat "$S 1. Add the `swiftformat` binary to your project directory via [CocoaPods](https://cocoapods.org/), by adding the following line to your Podfile then running `pod install`: ```ruby - pod 'SwiftFormat/CLI', '~> 0.49' + pod 'SwiftFormat/CLI', '~> 0.54' ``` **NOTE:** This will only install the pre-built command-line app, not the source code for the SwiftFormat framework. @@ -354,7 +354,7 @@ You can use `SwiftFormat` as a SwiftPM command plugin. ```swift dependencies: [ // ... - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.9"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.0"), ] ``` @@ -826,6 +826,8 @@ It is common practice to include the file name, creation date and/or the current * `{file}` - the name of the file * `{year}` - the current year * `{created}` - the date on which the file was created +* `{created.name}` - the name of the user who first committed the file +* `{created.email}` - the email of the user who first committed the file * `{created.year}` - the year in which the file was created For example, a header template of: @@ -842,7 +844,7 @@ Will be formatted as: // Created by John Smith on 01/02/2016. ``` -**NOTE:** the `{year}` value and `{created}` date format are determined from the current locale and timezone of the machine running the script. +**NOTE:** the `{year}` value and `{created}` date format are determined from the current locale and timezone of the machine running the script. `{created.name}` and `{created.email}` requires the project to be version controlled by git. FAQ @@ -963,6 +965,7 @@ SwiftFormat is not a commercially-funded product, it's a labor of love given fre Credits ------------ +* [Cal Stephens](https://github.com/calda) - Numerous new formatting rules, options and bug fixes * [Tony Arnold](https://github.com/tonyarnold) - SwiftFormat for Xcode * [Vincent Bernier](https://github.com/vinceburn) - SwiftFormat for Xcode settings UI * [Vikram Kriplaney](https://github.com/markiv) - SwiftFormat for Xcode icon and search feature @@ -970,7 +973,6 @@ Credits * [Maxime Marinel](https://github.com/bourvill) - Git pre-commit hook script * [Romain Pouclet](https://github.com/palleas) - Homebrew formula * [Aerobounce](https://github.com/aerobounce) - Homebrew cask and Sublime Text plugin -* [Cal Stephens](https://github.com/calda) - Several new formatting rules and options * [Facundo Menzella](https://github.com/facumenzella) - Several new formatting rules and options * [Ali Akhtarzada](https://github.com/aliak00) - Several path-related CLI enhancements * [Yonas Kolb](https://github.com/yonaskolb) - Swift Package Manager integration @@ -987,6 +989,7 @@ Credits * [Saleem Abdulrasool](https://github.com/compnerd) - Windows build workflow * [Arthur Semenyutin](https://github.com/vox-humana) - Docker image * [Marco Eidinger](https://github.com/MarcoEidinger) - Swift Package Manager plugin +* [Hampus Tågerud](https://github.com/hampustagerud) - Git integration for fileHeader rule * [Nick Lockwood](https://github.com/nicklockwood) - Everything else ([Full list of contributors](https://github.com/nicklockwood/SwiftFormat/graphs/contributors))