From 5ca8939c927ec27f23a5d4e962c12847d37818a7 Mon Sep 17 00:00:00 2001 From: Phillip Song <103433299+phlpsong@users.noreply.github.com> Date: Sat, 23 Mar 2024 22:58:59 +0800 Subject: [PATCH] Switch model in query window (#457) * feat: switch model in query window * Update Easydict/Feature/ViewController/View/ResultView/EZResultView.m Co-authored-by: Tisfeng * refactor: rewrite OpenAIService in Swift * fix: missing change in project file * fix: update popup menu position * perf(UI): modify to pop up NSMenu below the sender * style: format code --------- Co-authored-by: Tisfeng --- Easydict.xcodeproj/project.pbxproj | 22 ++- Easydict/App/Easydict-Bridging-Header.h | 1 - .../CustomOpenAI/CustomOpenAIService.swift | 18 ++- .../Feature/Service/Model/EZServiceTypes.m | 1 - .../EZOpenAILikeService+EZPromptMessages.h | 2 +- .../Service/OpenAI/EZOpenAILikeService.h | 3 +- .../Service/OpenAI/EZOpenAILikeService.m | 11 ++ .../Feature/Service/OpenAI/EZOpenAIService.h | 17 --- .../Feature/Service/OpenAI/EZOpenAIService.m | 134 ------------------ .../Service/OpenAI/OpenAIService.swift | 122 ++++++++++++++++ .../Utility/EZLinkParser/EZSchemeParser.m | 1 - .../LanguageButton/EZDetectLanguageButton.m | 9 +- .../LanguageButton/EZSelectLanguageButton.m | 9 +- .../View/PopUpButton/EZPopUpButton.m | 7 +- .../View/ResultView/EZResultView.m | 113 +++++++++------ .../ViewController/View/Titlebar/EZTitlebar.m | 16 +-- .../Configuration+Defaults.swift | 2 +- .../AppKit/NSMenu+PopUpBelowView.swift | 18 +++ ...tomOpenAIService+ConfigurableService.swift | 2 + .../OpenAIService+ConfigurableService.swift | 19 ++- .../ServiceConfigurationCells.swift | 2 +- 21 files changed, 294 insertions(+), 235 deletions(-) delete mode 100644 Easydict/Feature/Service/OpenAI/EZOpenAIService.h delete mode 100644 Easydict/Feature/Service/OpenAI/EZOpenAIService.m create mode 100644 Easydict/Feature/Service/OpenAI/OpenAIService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/AppKit/NSMenu+PopUpBelowView.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 83d55ca7a..1d97b189a 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ 038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A723F2B62C0B9004995E3 /* String+Regex.swift */; }; 038EA1AA2B41169C008A6DD1 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1A92B41169C008A6DD1 /* ZipArchive */; }; 038EA1AD2B41282F008A6DD1 /* MJExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1AC2B41282F008A6DD1 /* MJExtension */; }; + 038F1F8F2BAD838F00CD2F65 /* NSMenu+PopUpBelowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038F1F8E2BAD838F00CD2F65 /* NSMenu+PopUpBelowView.swift */; }; 0396D611292C932F006A11D9 /* EZSelectLanguageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */; }; 0396D615292CC4C3006A11D9 /* EZLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 0396D614292CC4C3006A11D9 /* EZLocalStorage.m */; }; 03991158292927E000E1B06D /* EZTitlebar.m in Sources */ = {isa = PBXBuildFile; fileRef = 03991157292927E000E1B06D /* EZTitlebar.m */; }; @@ -94,7 +95,6 @@ 0399116A292AA2EF00E1B06D /* EZLayoutManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 03991169292AA2EF00E1B06D /* EZLayoutManager.m */; }; 0399C6A529A747E600B4AFCC /* EZDeepLTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 0399C6A429A747E600B4AFCC /* EZDeepLTranslateResponse.m */; }; 0399C6A829A74E0F00B4AFCC /* EZQueryResult+EZDeepLTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 0399C6A729A74E0F00B4AFCC /* EZQueryResult+EZDeepLTranslateResponse.m */; }; - 0399C6AC29A860AA00B4AFCC /* EZOpenAIService.m in Sources */ = {isa = PBXBuildFile; fileRef = 0399C6AB29A860AA00B4AFCC /* EZOpenAIService.m */; }; 0399C6B829A9F4B800B4AFCC /* EZSchemeParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 0399C6B729A9F4B800B4AFCC /* EZSchemeParser.m */; }; 039B694F2A9D9F370063709D /* EZWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 039B694E2A9D9F370063709D /* EZWebViewManager.m */; }; 039CC90D292F664E0037B91E /* NSObject+EZWindowType.m in Sources */ = {isa = PBXBuildFile; fileRef = 039CC90C292F664E0037B91E /* NSObject+EZWindowType.m */; }; @@ -232,6 +232,7 @@ 03FD68BB2B1DC59600FD388E /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03FD68BA2B1DC59600FD388E /* CryptoSwift */; }; 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */; }; 0A057D6D2B499A000025C51D /* ServiceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A057D6C2B499A000025C51D /* ServiceTab.swift */; }; + 0A0781332BA6D9280002DA09 /* OpenAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0781322BA6D9280002DA09 /* OpenAIService.swift */; }; 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */; }; 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */; }; 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */; }; @@ -480,6 +481,7 @@ 03882F8B29D95044005B5A52 /* CoolToast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoolToast.h; sourceTree = ""; }; 03882F8C29D95044005B5A52 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 038A723F2B62C0B9004995E3 /* String+Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Regex.swift"; sourceTree = ""; }; + 038F1F8E2BAD838F00CD2F65 /* NSMenu+PopUpBelowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenu+PopUpBelowView.swift"; sourceTree = ""; }; 0396D60F292C932F006A11D9 /* EZSelectLanguageCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZSelectLanguageCell.h; sourceTree = ""; }; 0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZSelectLanguageCell.m; sourceTree = ""; }; 0396D613292CC4C3006A11D9 /* EZLocalStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZLocalStorage.h; sourceTree = ""; }; @@ -494,8 +496,6 @@ 0399C6A429A747E600B4AFCC /* EZDeepLTranslateResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZDeepLTranslateResponse.m; sourceTree = ""; }; 0399C6A629A74E0F00B4AFCC /* EZQueryResult+EZDeepLTranslateResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "EZQueryResult+EZDeepLTranslateResponse.h"; sourceTree = ""; }; 0399C6A729A74E0F00B4AFCC /* EZQueryResult+EZDeepLTranslateResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "EZQueryResult+EZDeepLTranslateResponse.m"; sourceTree = ""; }; - 0399C6AA29A860AA00B4AFCC /* EZOpenAIService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZOpenAIService.h; sourceTree = ""; }; - 0399C6AB29A860AA00B4AFCC /* EZOpenAIService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZOpenAIService.m; sourceTree = ""; }; 0399C6B629A9F4B800B4AFCC /* EZSchemeParser.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZSchemeParser.h; sourceTree = ""; }; 0399C6B729A9F4B800B4AFCC /* EZSchemeParser.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZSchemeParser.m; sourceTree = ""; }; 039B694D2A9D9F370063709D /* EZWebViewManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZWebViewManager.h; sourceTree = ""; }; @@ -746,6 +746,7 @@ 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+EncryptAES.swift"; sourceTree = ""; }; 06E15747A7BD34D510ADC6A8 /* Pods-Easydict.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.debug.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.debug.xcconfig"; sourceTree = ""; }; 0A057D6C2B499A000025C51D /* ServiceTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTab.swift; sourceTree = ""; }; + 0A0781322BA6D9280002DA09 /* OpenAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIService.swift; sourceTree = ""; }; 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppInfo.swift"; sourceTree = ""; }; 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+DidSet.swift"; sourceTree = ""; }; 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name.swift"; sourceTree = ""; }; @@ -1276,6 +1277,14 @@ path = String; sourceTree = ""; }; + 038F1F8D2BAD835500CD2F65 /* AppKit */ = { + isa = PBXGroup; + children = ( + 038F1F8E2BAD838F00CD2F65 /* NSMenu+PopUpBelowView.swift */, + ); + path = AppKit; + sourceTree = ""; + }; 0396D612292CBDFD006A11D9 /* Storage */ = { isa = PBXGroup; children = ( @@ -1300,8 +1309,7 @@ 0399C6A929A8608000B4AFCC /* OpenAI */ = { isa = PBXGroup; children = ( - 0399C6AA29A860AA00B4AFCC /* EZOpenAIService.h */, - 0399C6AB29A860AA00B4AFCC /* EZOpenAIService.m */, + 0A0781322BA6D9280002DA09 /* OpenAIService.swift */, 03FC69962B399EF00035D2DA /* EZOpenAIChatResponse.h */, 03FC69992B39D13A0035D2DA /* EZOpenAIChatResponse.m */, 03CF27F92B3A787900E19B57 /* EZOpenAILikeService+EZPromptMessages.h */, @@ -2411,6 +2419,7 @@ EA9943E62B534D7C00EE7B97 /* Extensions */ = { isa = PBXGroup; children = ( + 038F1F8D2BAD835500CD2F65 /* AppKit */, EA1013412B5DBDA5005E43F9 /* Defaults */, 038A723E2B62C07B004995E3 /* String */, EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */, @@ -2911,7 +2920,6 @@ 03DC7C6A2A3CA852000BF7C9 /* EZAppCell.m in Sources */, 9643D9392B6F49E0000FBEA6 /* AppShortcutSetting.swift in Sources */, 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */, - 0399C6AC29A860AA00B4AFCC /* EZOpenAIService.m in Sources */, 03542A432937B45E00C34C33 /* EZBaiduTranslate.m in Sources */, 03BB2DEB29F57DC000447EDD /* NSImage+EZSymbolmage.m in Sources */, 03B0230629231FA6001C7E63 /* EZLabel.m in Sources */, @@ -2952,6 +2960,7 @@ 03B0233229231FA6001C7E63 /* MMLog.swift in Sources */, 03DC7C5E2A3ABE28000BF7C9 /* EZConstKey.m in Sources */, 62E2BF4C2B4082BA00E42D38 /* AliTranslateType.swift in Sources */, + 0A0781332BA6D9280002DA09 /* OpenAIService.swift in Sources */, EA3B81F92B5254AA004C0E8B /* Configuration+Defaults.swift in Sources */, 03E3E7C22ADE318800812C84 /* EZQueryMenuTextView.m in Sources */, 03B0231829231FA6001C7E63 /* SnipWindowController.m in Sources */, @@ -3071,6 +3080,7 @@ 0AC8A8452B6A4D97006DA5CC /* ServiceConfigurationCells.swift in Sources */, 276742092B3DC230002A2C75 /* AboutTab.swift in Sources */, 03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */, + 038F1F8F2BAD838F00CD2F65 /* NSMenu+PopUpBelowView.swift in Sources */, DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */, 03B0232429231FA6001C7E63 /* NSUserDefaults+MM.m in Sources */, 03542A5E2938F05B00C34C33 /* EZLanguageModel.m in Sources */, diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index aef51ee03..eb6509328 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -31,7 +31,6 @@ #import "EZLanguageManager.h" #import "DarkModeManager.h" #import "EZScriptExecutor.h" -#import "EZOpenAIService.h" #import "EZOpenAILikeService.h" #import "EZNiuTransTranslate.h" #import "EZDeepLTranslate.h" diff --git a/Easydict/Feature/Service/CustomOpenAI/CustomOpenAIService.swift b/Easydict/Feature/Service/CustomOpenAI/CustomOpenAIService.swift index 653e02bab..efc531c82 100644 --- a/Easydict/Feature/Service/CustomOpenAI/CustomOpenAIService.swift +++ b/Easydict/Feature/Service/CustomOpenAI/CustomOpenAIService.swift @@ -40,7 +40,19 @@ class CustomOpenAIService: OpenAILikeService { } override var model: String { - Defaults[.customOpenAIModel] + get { + Defaults[.customOpenAIModel] + } + + set { + Defaults[.customOpenAIModel] = newValue + } + } + + override var availableModels: [String] { + let models = Defaults[.customOpenAIModelsAvailable] + guard let models, !models.isEmpty else { return [] } + return models.components(separatedBy: ",").filter { !$0.isEmpty } } override func serviceType() -> ServiceType { @@ -92,4 +104,8 @@ class CustomOpenAIService: OpenAILikeService { guard let value = UInt(customOpenAIServiceUsageStatus.rawValue) else { return .default } return EZServiceUsageStatus(rawValue: value) ?? .default } + + override func hasPrivateAPIKey() -> Bool { + !apiKey.isEmpty + } } diff --git a/Easydict/Feature/Service/Model/EZServiceTypes.m b/Easydict/Feature/Service/Model/EZServiceTypes.m index eb98a924d..f54fe8de4 100644 --- a/Easydict/Feature/Service/Model/EZServiceTypes.m +++ b/Easydict/Feature/Service/Model/EZServiceTypes.m @@ -13,7 +13,6 @@ #import "EZDeepLTranslate.h" #import "EZVolcanoTranslate.h" #import "EZAppleService.h" -#import "EZOpenAIService.h" #import "EZBingService.h" #import "EZConfiguration.h" #import "EZAppleDictionary.h" diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAILikeService+EZPromptMessages.h b/Easydict/Feature/Service/OpenAI/EZOpenAILikeService+EZPromptMessages.h index 11096e661..66855019e 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAILikeService+EZPromptMessages.h +++ b/Easydict/Feature/Service/OpenAI/EZOpenAILikeService+EZPromptMessages.h @@ -7,7 +7,7 @@ // #import -#import "EZOpenAIService.h" +#import "Easydict-Swift.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.h b/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.h index b74659d20..5b94a5367 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.h +++ b/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.h @@ -18,7 +18,8 @@ NS_SWIFT_NAME(OpenAILikeService) @property (nonatomic, copy, readonly) NSString *apiKey; @property (nonatomic, copy, readonly) NSString *endPoint; -@property (nonatomic, copy, readonly) NSString *model; +@property (nonatomic, copy) NSString *model; +@property (nonatomic, copy, readonly) NSArray *availableModels; @property (nonatomic, copy) NSString *defaultAPIKey; @property (nonatomic, copy) NSString *defaultModel; diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.m b/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.m index a357f277b..2732ae35a 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.m +++ b/Easydict/Feature/Service/OpenAI/EZOpenAILikeService.m @@ -20,6 +20,8 @@ @implementation EZOpenAILikeService +@synthesize model = _model; + - (NSString *)apiKey { MethodNotImplemented(); return nil; @@ -35,6 +37,15 @@ - (NSString *)model { return nil; } +- (void)setModel:(NSString *)model { + MethodNotImplemented(); +} + +- (NSArray *)availableModels { + MethodNotImplemented(); + return nil; +} + /// Use OpenAI to translate text. - (void)translate:(NSString *)text from:(EZLanguage)from to:(EZLanguage)to completion:(void (^)(EZQueryResult *, NSError *_Nullable))completion { text = [text removeInvisibleChar]; diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAIService.h b/Easydict/Feature/Service/OpenAI/EZOpenAIService.h deleted file mode 100644 index 8b2f134d6..000000000 --- a/Easydict/Feature/Service/OpenAI/EZOpenAIService.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// EZOpenAIService.h -// Easydict -// -// Created by tisfeng on 2023/2/24. -// Copyright © 2023 izual. All rights reserved. -// - -#import "EZOpenAILikeService.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface EZOpenAIService : EZOpenAILikeService - -@end - -NS_ASSUME_NONNULL_END diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m b/Easydict/Feature/Service/OpenAI/EZOpenAIService.m deleted file mode 100644 index 986758bc8..000000000 --- a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m +++ /dev/null @@ -1,134 +0,0 @@ -// -// EZOpenAIService.m -// Easydict -// -// Created by tisfeng on 2023/2/24. -// Copyright © 2023 izual. All rights reserved. -// - -#import "EZOpenAIService.h" -#import "NSString+EZUtils.h" -#import "EZConfiguration.h" -#import "EZOpenAIChatResponse.h" -#import "EZOpenAILikeService+EZPromptMessages.h" -#import "Easydict-Swift.h" - -@interface EZOpenAIService () - -@end - - -@implementation EZOpenAIService - -- (instancetype)init { - if (self = [super init]) { - self.defaultModel = @"gpt-3.5-turbo-1106"; - } - return self; -} - -- (NSString *)apiKey { - // easydict://writeKeyValue?EZOpenAIAPIKey= - - NSString *apiKey = [[NSUserDefaults standardUserDefaults] stringForKey:EZOpenAIAPIKey] ?: @""; - return apiKey; -} - -- (NSString *)endPoint { - NSString *endPoint = [[NSUserDefaults standardUserDefaults] stringForKey:EZOpenAIEndPointKey] ?: @""; - if (endPoint.length == 0) { - endPoint = @"https://api.openai.com/v1/chat/completions"; - } - return endPoint; -} - -- (NSString *)model { - // easydict://writeKeyValue?EZOpenAIModelKey= - - NSString *model = [[NSUserDefaults standardUserDefaults] stringForKey:EZOpenAIModelKey]; - - // If there is no own key, only the default model is allowed to be used, such as gemini-pro - if (![self hasPrivateAPIKey]) { - // In development mode, the default model is allowed to be modified. -#if !DEBUG - model = self.defaultModel; -#endif - } - - if (model.length == 0) { - model = self.defaultModel; - } - - return model; -} - -#pragma mark - 重写父类方法 - -- (EZServiceType)serviceType { - return EZServiceTypeOpenAI; -} - -- (EZQueryTextType)queryTextType { - EZQueryTextType type = EZQueryTextTypeNone; - BOOL enableTranslation = [[NSUserDefaults mm_readString:EZOpenAITranslationKey defaultValue:@"1"] boolValue]; - BOOL enableDictionary = [[NSUserDefaults mm_readString:EZOpenAIDictionaryKey defaultValue:@"1"] boolValue]; - BOOL enableSentence = [[NSUserDefaults mm_readString:EZOpenAISentenceKey defaultValue:@"1"] boolValue]; - if (enableTranslation) { - type = type | EZQueryTextTypeTranslation; - } - if (enableDictionary) { - type = type | EZQueryTextTypeDictionary; - } - if (enableSentence) { - type = type | EZQueryTextTypeSentence; - } - if (type == EZQueryTextTypeNone) { - type = EZQueryTextTypeTranslation; - } - - return type; -} - -- (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; - return type; -} - -- (EZServiceUsageStatus)serviceUsageStatus { - EZServiceUsageStatus serviceUsageStatus = [[NSUserDefaults mm_readString:EZOpenAIServiceUsageStatusKey defaultValue:@"0"] integerValue]; - return serviceUsageStatus; -} - -- (NSString *)name { - return NSLocalizedString(@"openai_translate", nil); -} - -- (NSString *)link { - return @"https://chat.openai.com"; -} - -// Supported languages, key is EZLanguage, value is the same as the key. -- (MMOrderedDictionary *)supportLanguagesDictionary { - MMOrderedDictionary *orderedDict = [[MMOrderedDictionary alloc] init]; - - NSArray *allLanguages = [EZLanguageManager.shared allLanguages]; - for (EZLanguage language in allLanguages) { - NSString *value = language; - if ([language isEqualToString:EZLanguageClassicalChinese]) { - value = kEZLanguageWenYanWen; - } - - // OpenAI does not support Burmese 🥲 - if (![language isEqualToString:EZLanguageBurmese]) { - [orderedDict setObject:value forKey:language]; - } - } - - return orderedDict; -} - -- (BOOL)hasPrivateAPIKey { - return ![self.apiKey isEqualToString:self.defaultAPIKey]; -} - -@end diff --git a/Easydict/Feature/Service/OpenAI/OpenAIService.swift b/Easydict/Feature/Service/OpenAI/OpenAIService.swift new file mode 100644 index 000000000..7b543d069 --- /dev/null +++ b/Easydict/Feature/Service/OpenAI/OpenAIService.swift @@ -0,0 +1,122 @@ +// +// OpenAIService.swift +// Easydict +// +// Created by phlpsong on 2024/3/17. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +@objc(EZOpenAIService) +class OpenAIService: OpenAILikeService { + // MARK: Lifecycle + + override init() { + super.init() + self.defaultModel = OpenAIModel.gpt3_5_turbo_0125.rawValue + } + + // MARK: Public + + override public func name() -> String { + NSLocalizedString("openai_translate", comment: "") + } + + // MARK: Internal + + override var apiKey: String { + Defaults[.openAIAPIKey] ?? "" + } + + override var endPoint: String { + let endPoint = Defaults[.openAIEndPoint] ?? "" + if endPoint.isEmpty { + return "https://api.openai.com/v1/chat/completions" + } + return endPoint + } + + override var model: String { + get { + var model = Defaults[.openAIModel].rawValue + if !hasPrivateAPIKey() { + #if DEBUG + model = defaultModel + #endif + } + if model.isEmpty { + model = defaultModel + } + return model + } + + set { + let new = OpenAIModel(rawValue: newValue) ?? .gpt3_5_turbo_0125 + Defaults[.openAIModel] = new + } + } + + override var availableModels: [String] { + OpenAIModel.allCases.map { $0.rawValue } + } + + override func link() -> String? { + "https://chat.openai.com" + } + + override func serviceType() -> ServiceType { + .openAI + } + + override func intelligentQueryTextType() -> EZQueryTextType { + Configuration.shared.intelligentQueryTextTypeForServiceType(serviceType()) + } + + override func supportLanguagesDictionary() -> MMOrderedDictionary { + let orderedDict = MMOrderedDictionary() + for language in EZLanguageManager.shared().allLanguages { + var value = language.rawValue + if language == Language.classicalChinese { + value = kEZLanguageWenYanWen + } + // OpenAI does not support Burmese 🥲 + if language != Language.burmese { + orderedDict.setObject(value as NSString, forKey: language.rawValue as NSString) + } + } + + return orderedDict + } + + override func queryTextType() -> EZQueryTextType { + var typeOptions: EZQueryTextType = [] + let isTranslationEnabled = Defaults[.openAITranslation] == "1" + let isDictionaryEnabled = Defaults[.openAIDictionary] == "1" + let isSentenceEnabled = Defaults[.openAISentence] == "1" + if isTranslationEnabled { + typeOptions.insert(.translation) + } + if isDictionaryEnabled { + typeOptions.insert(.dictionary) + } + if isSentenceEnabled { + typeOptions.insert(.sentence) + } + if typeOptions == [] { + typeOptions = [.translation] + } + return typeOptions + } + + override func serviceUsageStatus() -> EZServiceUsageStatus { + let usageStatus = Defaults[.openAIServiceUsageStatus] + guard let value = UInt(usageStatus.rawValue) else { return .default } + return EZServiceUsageStatus(rawValue: value) ?? .default + } + + override func hasPrivateAPIKey() -> Bool { + !apiKey.isEmpty && apiKey != defaultAPIKey + } +} diff --git a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m index 7f98263e2..7ad7d5671 100644 --- a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m +++ b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m @@ -7,7 +7,6 @@ // #import "EZSchemeParser.h" -#import "EZOpenAIService.h" #import "EZYoudaoTranslate.h" #import "EZServiceTypes.h" #import "EZDeepLTranslate.h" diff --git a/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZDetectLanguageButton.m b/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZDetectLanguageButton.m index d755df264..190a1a62f 100644 --- a/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZDetectLanguageButton.m +++ b/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZDetectLanguageButton.m @@ -9,6 +9,7 @@ #import "EZDetectLanguageButton.h" #import "EZLanguageManager.h" #import "NSView+EZAnimatedHidden.h" +#import "Easydict-Swift.h" @interface EZDetectLanguageButton () @@ -47,7 +48,7 @@ - (void)setup { // 显示menu [self setupMenu]; - [self.customMenu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0, 0) inView:self]; + [self.customMenu popUpBelowView:self]; }]; } @@ -55,13 +56,13 @@ - (void)setDetectedLanguage:(EZLanguage)detectedLanguage { EZLanguage oldDetectedLanguage = self.detectedLanguage; _detectedLanguage = detectedLanguage; - if (!self.showAutoLanguage && [detectedLanguage isEqualToString: EZLanguageAuto]) { + if (!self.showAutoLanguage && [detectedLanguage isEqualToString:EZLanguageAuto]) { [self setAnimatedHidden:YES]; return; } [self setAnimatedHidden:NO]; - + NSString *detectLanguageTitle = [EZLanguageManager.shared showingLanguageName:detectedLanguage]; NSString *title = NSLocalizedString(@"detected", nil); @@ -113,7 +114,7 @@ - (void)setupMenu { } } - [self.languageDict enumerateKeysAndObjectsUsingBlock:^(EZLanguage _Nonnull key, NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [self.languageDict enumerateKeysAndObjectsUsingBlock:^(EZLanguage _Nonnull key, NSString *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:obj action:@selector(clickItem:) keyEquivalent:@""]; item.tag = idx; item.target = self; diff --git a/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZSelectLanguageButton.m b/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZSelectLanguageButton.m index faa8b1e83..ae73f2782 100644 --- a/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZSelectLanguageButton.m +++ b/Easydict/Feature/ViewController/View/CustomButton/LanguageButton/EZSelectLanguageButton.m @@ -7,6 +7,7 @@ // #import "EZSelectLanguageButton.h" +#import "Easydict-Swift.h" @interface EZSelectLanguageButton () @@ -36,7 +37,7 @@ - (instancetype)init { mm_strongify(self) // 显示menu [self setupMenu]; - [self.customMenu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0, 0) inView:self]; + [self.customMenu popUpBelowView:self]; }]; } return self; @@ -109,7 +110,7 @@ - (void)updateConstraints { #pragma mark - - (void)setupMenu { - EZLanguageManager *languageManager = [EZLanguageManager shared]; + EZLanguageManager *languageManager = [EZLanguageManager shared]; NSArray *allLanguages = [languageManager allLanguages]; self.languageDict = [[MMOrderedDictionary alloc] init]; for (EZLanguage language in allLanguages) { @@ -162,8 +163,8 @@ - (void)setSelectedLanguage:(EZLanguage)selectedLanguage { _selectedLanguage = selectedLanguage; if ([self.languageDict.allKeys containsObject:selectedLanguage]) { - EZLanguageManager *languageManager = [EZLanguageManager shared]; - + EZLanguageManager *languageManager = [EZLanguageManager shared]; + NSString *languageName = [languageManager showingLanguageName:selectedLanguage]; NSString *languageFlag = [languageManager languageFlagEmoji:selectedLanguage]; diff --git a/Easydict/Feature/ViewController/View/PopUpButton/EZPopUpButton.m b/Easydict/Feature/ViewController/View/PopUpButton/EZPopUpButton.m index 6ab632eac..439212ef8 100644 --- a/Easydict/Feature/ViewController/View/PopUpButton/EZPopUpButton.m +++ b/Easydict/Feature/ViewController/View/PopUpButton/EZPopUpButton.m @@ -7,6 +7,7 @@ // #import "EZPopUpButton.h" +#import "Easydict-Swift.h" @interface EZPopUpButton () @@ -31,15 +32,15 @@ - (void)setupUI { self.title = @""; mm_weakify(self) - [self setClickBlock:^(EZButton * _Nonnull button) { + [self setClickBlock:^(EZButton *_Nonnull button) { mm_strongify(self) // 显示menu if (self.titles.count) { [self setupMenu]; - [self.customMenu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0, 0) inView:self]; + [self.customMenu popUpBelowView:self]; } }]; - + [NSView mm_make:^(NSView *_Nonnull titleContainerView) { [self addSubview:titleContainerView]; titleContainerView.layer.backgroundColor = [NSColor redColor].CGColor; diff --git a/Easydict/Feature/ViewController/View/ResultView/EZResultView.m b/Easydict/Feature/ViewController/View/ResultView/EZResultView.m index abdda61fc..3684391d9 100644 --- a/Easydict/Feature/ViewController/View/ResultView/EZResultView.m +++ b/Easydict/Feature/ViewController/View/ResultView/EZResultView.m @@ -15,6 +15,8 @@ #import "NSImage+EZSymbolmage.h" #import "EZWindowManager.h" #import "EZOpenAILikeService.h" +#import "EZLocalStorage.h" +#import "Easydict-Swift.h" @interface EZResultView () @@ -48,9 +50,9 @@ - (void)setup { } dark:^(CALayer *layer) { layer.backgroundColor = [NSColor ez_resultViewBgDarkColor].CGColor; }]; - + mm_weakify(self); - + self.topBarView = [NSView mm_make:^(NSView *_Nonnull view) { mm_strongify(self); [self addSubview:view]; @@ -62,14 +64,14 @@ - (void)setup { }]; }]; self.topBarView.mas_key = @"topBarView"; - + self.serviceIcon = [NSImageView mm_make:^(NSImageView *imageView) { mm_strongify(self); [self addSubview:imageView]; [imageView setImage:[NSImage imageNamed:@"Apple Translate"]]; }]; self.serviceIcon.mas_key = @"typeImageView"; - + self.serviceNameLabel = [NSTextField mm_make:^(NSTextField *label) { mm_strongify(self); [self addSubview:label]; @@ -90,7 +92,7 @@ - (void)setup { self.serviceModelButton.bordered = NO; self.serviceModelButton.cornerRadius = 3.0; self.serviceModelButton.titleFont = [NSFont systemFontOfSize:10]; - + [self.serviceModelButton excuteLight:^(EZButton *button) { button.titleColor = [NSColor mm_colorWithHexString:@"#666666"]; button.backgroundColor = [NSColor mm_colorWithHexString:@"#E2E2E2"]; @@ -103,7 +105,7 @@ - (void)setup { button.backgroundHighlightColor = [NSColor mm_colorWithHexString:@"#585A5C"]; }]; self.serviceModelButton.mas_key = @"modelButton"; - + self.errorImageView = [NSImageView mm_make:^(NSImageView *imageView) { mm_strongify(self); [self addSubview:imageView]; @@ -112,11 +114,11 @@ - (void)setup { [imageView setImage:image]; }]; self.errorImageView.mas_key = @"errorImageView"; - + EZLoadingAnimationView *loadingView = [[EZLoadingAnimationView alloc] init]; [self addSubview:loadingView]; self.loadingView = loadingView; - + EZWordResultView *wordResultView = [[EZWordResultView alloc] initWithFrame:self.bounds]; [self addSubview:wordResultView]; self.wordResultView = wordResultView; @@ -125,22 +127,22 @@ - (void)setup { mm_strongify(self); [self.loadingView startLoading:NO]; }]; - + EZHoverButton *arrowButton = [[EZHoverButton alloc] init]; self.arrowButton = arrowButton; [self addSubview:arrowButton]; NSImage *image = [NSImage imageNamed:@"arrow-down"]; arrowButton.image = image; self.arrowButton.mas_key = @"arrowButton"; - + [arrowButton setClickBlock:^(EZButton *_Nonnull button) { mm_strongify(self); - + if (!self.result.hasShowingResult && self.result.queryModel.queryText.length == 0) { NSLog(@"query text is empty"); return; } - + BOOL oldIsShowing = self.result.isShowing; BOOL newIsShowing = !oldIsShowing; self.result.isShowing = newIsShowing; @@ -149,15 +151,15 @@ - (void)setup { if (newIsShowing) { self.result.manulShow = YES; } - + [self updateArrowButton]; - + if (self.clickArrowBlock) { self.clickArrowBlock(self.result); } - + // TODO: add arrow roate animation. - + // [self rotateArrowButton]; }]; @@ -165,7 +167,7 @@ - (void)setup { EZHoverButton *stopButton = [[EZHoverButton alloc] init]; self.stopButton = stopButton; [self addSubview:stopButton]; - NSImage *stopImage = [NSImage ez_imageWithSymbolName:@"stop.circle"]; + NSImage *stopImage = [NSImage ez_imageWithSymbolName:@"stop.circle"]; stopImage = [stopImage imageWithTintColor:[NSColor mm_colorWithHexString:@"#707070"]]; stopButton.image = stopImage; stopButton.mas_key = @"stopButton"; @@ -198,23 +200,23 @@ - (void)setup { self.retryBlock(self.result); } }]; - - + + CGSize iconSize = CGSizeMake(16, 16); - + [self updateArrowButton]; - + [self.topBarView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.equalTo(self); make.height.mas_equalTo(EZResultViewMiniHeight); }]; - + [self.serviceIcon mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.topBarView).offset(9); make.centerY.equalTo(self.topBarView); make.size.mas_equalTo(iconSize); }]; - + [self.serviceNameLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.serviceIcon.mas_right).offset(4); make.centerY.equalTo(self.topBarView).offset(0); @@ -225,20 +227,20 @@ - (void)setup { make.top.equalTo(self.topBarView).offset(8); make.bottom.equalTo(self.topBarView).offset(-8); }]; - + [self.errorImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.serviceNameLabel.mas_right).offset(8); make.centerY.equalTo(self.topBarView); make.size.mas_equalTo(iconSize); }]; - + [self.loadingView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.serviceNameLabel.mas_right).offset(5); make.centerY.equalTo(self.topBarView); make.height.equalTo(self.topBarView); }]; - - + + [self.arrowButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.topBarView.mas_right).offset(-5); make.centerY.equalTo(self.topBarView); @@ -262,20 +264,25 @@ - (void)setup { - (void)setResult:(EZQueryResult *)result { _result = result; - + EZServiceType serviceType = result.serviceType; self.serviceIcon.image = [NSImage imageNamed:serviceType]; - + self.serviceNameLabel.attributedStringValue = [NSAttributedString mm_attributedStringWithString:result.service.name font:[NSFont systemFontOfSize:13]]; if ([self isOpenAIService:result.service]) { EZOpenAILikeService *service = (EZOpenAILikeService *)result.service; self.serviceModelButton.title = service.model; + mm_weakify(self); + [self.serviceModelButton setMouseUpBlock:^(EZButton *_Nonnull button) { + mm_strongify(self); + [self showModelSelectionMenu:button]; + }]; [self updateServiceModelLabel]; } else { self.serviceModelButton.hidden = YES; } [self.wordResultView refreshWithResult:result]; - + mm_weakify(self); [self.wordResultView setUpdateViewHeightBlock:^(CGFloat wordResultViewHeight) { mm_strongify(self); @@ -283,10 +290,10 @@ - (void)setResult:(EZQueryResult *)result { }]; [self updateAllButtonStatus]; - + CGFloat wordResultViewHeight = self.wordResultView.viewHeight ?: result.webViewManager.wordResultViewHeight; [self updateWordResultViewHeight:wordResultViewHeight]; - + // animation need right frame, but result may change, so have to layout frame. [self updateLoadingAnimation]; } @@ -306,14 +313,14 @@ - (void)updateWordResultViewHeight:(CGFloat)wordResultViewHeight { self.result.isLoading = NO; } } - + [self.wordResultView mas_updateConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.topBarView.mas_bottom); make.left.right.equalTo(self); make.height.mas_equalTo(wordResultViewHeight); }]; - + CGFloat viewHeight = EZResultViewMiniHeight; if (self.result.hasShowingResult && self.result.isShowing) { viewHeight = EZResultViewMiniHeight + wordResultViewHeight; @@ -358,7 +365,7 @@ - (void)updateErrorImage { errorImageName = @"error"; } NSImage *errorImage = [NSImage imageNamed:errorImageName]; - + self.errorImageView.image = errorImage; self.errorImageView.toolTip = toolTip; } @@ -375,7 +382,7 @@ - (void)updateStopButton { if ([self isOpenAIService:self.result.service]) { showStopButton = self.result.hasTranslatedResult && !self.result.isFinished; } - + self.stopButton.hidden = !showStopButton; } @@ -409,6 +416,32 @@ - (void)updateServiceModelLabel { self.serviceModelButton.hidden = showWarningImage || showStopButton || showRetryButton || isLoading; } +- (void)showModelSelectionMenu:(EZButton *)sender { + EZOpenAILikeService *service = (EZOpenAILikeService *)self.result.service; + NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Menu"]; + for (NSString *model in service.availableModels) { + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:model action:@selector(modelDidSelected:) keyEquivalent:@""]; + item.target = self; + [menu addItem:item]; + } + [menu popUpBelowView:sender]; +} + +- (void)modelDidSelected:(NSMenuItem *)sender { + EZOpenAILikeService *service = (EZOpenAILikeService *)self.result.service; + 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 { @@ -418,11 +451,11 @@ - (void)rotateArrowButton { animation.cumulative = YES; animation.repeatCount = 1; animation.duration = 1; - + CGRect oldRect = self.arrowButton.layer.frame; self.arrowButton.layer.anchorPoint = CGPointMake(0.5f, 0.5f); self.arrowButton.layer.frame = oldRect; - + [self.arrowButton.layer addAnimation:animation forKey:@"animation"]; } @@ -463,14 +496,14 @@ - (void)addAnimationGroupForView:(NSView *)view { scaleAnimation.values = @[ @1.0, @1.8, @1.0 ]; scaleAnimation.repeatCount = MAXFLOAT; scaleAnimation.duration = 0.6; - + CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; rotationAnimation.fromValue = @(0); rotationAnimation.toValue = [NSNumber numberWithFloat:90 * (M_PI / 180.0f)]; rotationAnimation.cumulative = YES; rotationAnimation.repeatCount = MAXFLOAT; rotationAnimation.duration = 1; - + CAAnimationGroup *group = [CAAnimationGroup animation]; group.animations = @[ scaleAnimation, rotationAnimation ]; group.duration = 1; diff --git a/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m b/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m index a7d8f4859..8b694579c 100644 --- a/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m +++ b/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m @@ -70,12 +70,11 @@ - (void)setup { [self setupSettingButton]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateConstraints) name:EZQuickLinkButtonUpdateNotification object:nil]; - } - (void)setupSettingButton { EZOpenLinkButton *button = [[EZOpenLinkButton alloc] init]; - NSImage *image = [[NSImage imageWithSystemSymbolName:@"switch.2" accessibilityDescription:nil]imageWithSymbolConfiguration:[NSImageSymbolConfiguration configurationWithScale:NSImageSymbolScaleLarge]]; + NSImage *image = [[NSImage imageWithSystemSymbolName:@"switch.2" accessibilityDescription:nil] imageWithSymbolConfiguration:[NSImageSymbolConfiguration configurationWithScale:NSImageSymbolScaleLarge]]; button.image = image; self.settingButton = button; @@ -99,7 +98,6 @@ - (void)setupSettingButton { [button setMouseUpBlock:^(EZButton *_Nonnull button) { mm_strongify(self); [self showMenu]; - }]; } @@ -186,7 +184,7 @@ - (void)updateConstraints { EZOpenLinkButton *eudicButton = [[EZOpenLinkButton alloc] init]; // !!!: Note that some applications have multiple channel versions. Ref: https://github.com/tisfeng/Raycast-Easydict/issues/16 - BOOL installedEudic = [self checkInstalledApp:@[@"com.eusoft.freeeudic", @"com.eusoft.eudic"]]; + BOOL installedEudic = [self checkInstalledApp:@[ @"com.eusoft.freeeudic", @"com.eusoft.eudic" ]]; eudicButton.hidden = !installedEudic; if (installedEudic) { [self addSubview:eudicButton]; @@ -235,14 +233,14 @@ - (void)updatePinButtonImage { } - (void)showMenu { - NSMenu * menu = [[NSMenu alloc]initWithTitle:@"Menu"]; - NSMenuItem * item1 = [[NSMenuItem alloc]initWithTitle:NSLocalizedString(@"remove_code_comment_symbols", nil) action:@selector(clickAutomaticallyRemoveCodeCommentSymbols) keyEquivalent:@""]; + NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Menu"]; + NSMenuItem *item1 = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"remove_code_comment_symbols", nil) action:@selector(clickAutomaticallyRemoveCodeCommentSymbols) keyEquivalent:@""]; item1.target = self; - NSMenuItem * item2 = [[NSMenuItem alloc]initWithTitle:NSLocalizedString(@"word_segmentation", nil) action:@selector(clickAutomaticWordSegmentation) keyEquivalent:@""]; + NSMenuItem *item2 = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"word_segmentation", nil) action:@selector(clickAutomaticWordSegmentation) keyEquivalent:@""]; item2.target = self; - NSMenuItem * item3 = [[NSMenuItem alloc]initWithTitle:NSLocalizedString(@"go_to_settings", nil) action:@selector(goToSettings) keyEquivalent:@""]; + NSMenuItem *item3 = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"go_to_settings", nil) action:@selector(goToSettings) keyEquivalent:@""]; item3.target = self; [menu addItem:item1]; @@ -250,7 +248,7 @@ - (void)showMenu { [menu addItem:[NSMenuItem separatorItem]]; [menu addItem:item3]; - [menu popUpMenuPositioningItem:nil atLocation:[NSEvent mouseLocation] inView:nil]; + [menu popUpBelowView:self.settingButton]; } - (void)clickAutomaticallyRemoveCodeCommentSymbols { diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index c4a7ca45e..e4db13dfc 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -203,7 +203,7 @@ extension Defaults.Keys { default: OpenAIUsageStats.default ) static let openAIEndPoint = Key(EZOpenAIEndPointKey) - static let openAIModel = Key(EZOpenAIModelKey, default: OpenAIModels.gpt3_5_turbo_0125) + static let openAIModel = Key(EZOpenAIModelKey, default: OpenAIModel.gpt3_5_turbo_0125) // Custom OpenAI static let customOpenAINameKey = Key( diff --git a/Easydict/NewApp/Utility/Extensions/AppKit/NSMenu+PopUpBelowView.swift b/Easydict/NewApp/Utility/Extensions/AppKit/NSMenu+PopUpBelowView.swift new file mode 100644 index 000000000..317eee0e1 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/AppKit/NSMenu+PopUpBelowView.swift @@ -0,0 +1,18 @@ +// +// NSMenu+PopUp.swift +// Easydict +// +// Created by tisfeng on 2024/3/22. +// Copyright © 2024 izual. All rights reserved. +// + +import AppKit +import Foundation + +@objc +extension NSMenu { + func popUp(belowView view: NSView) { + let point = CGPoint(x: 0, y: view.frame.height + 8) + popUp(positioning: nil, at: point, in: view) + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CustomOpenAIService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CustomOpenAIService+ConfigurableService.swift index 1672f8bd9..6803e95c6 100644 --- a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CustomOpenAIService+ConfigurableService.swift +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CustomOpenAIService+ConfigurableService.swift @@ -164,6 +164,8 @@ private class CustomOpenAIViewModel: ObservableObject { } private func serviceConfigChanged() { + // looks like Defaults changed but View not update in this case + objectWillChange.send() let userInfo: [String: Any] = [ EZServiceTypeKey: service.serviceType().rawValue, ] diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift index 739e9d2c8..bb2670721 100644 --- a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift @@ -11,10 +11,10 @@ import Defaults import Foundation import SwiftUI -// MARK: - EZOpenAIService + ConfigurableService +// MARK: - OpenAIService + ConfigurableService @available(macOS 13.0, *) -extension EZOpenAIService: ConfigurableService { +extension OpenAIService: ConfigurableService { func configurationListItems() -> some View { OpenAIServiceConfigurationView(service: self) } @@ -26,14 +26,14 @@ extension EZOpenAIService: ConfigurableService { private struct OpenAIServiceConfigurationView: View { // MARK: Lifecycle - init(service: EZOpenAIService) { + init(service: OpenAIService) { self.service = service self.viewModel = OpenAIServiceViewModel(service: service) } // MARK: Internal - let service: EZOpenAIService + let service: OpenAIService var body: some View { ServiceConfigurationSecretSectionView( @@ -54,9 +54,8 @@ private struct OpenAIServiceConfigurationView: View { ServiceConfigurationPickerCell( titleKey: "service.configuration.openai.model.title", key: .openAIModel, - values: OpenAIModels.allCases + values: OpenAIModel.allCases ) - ServiceConfigurationToggleCell( titleKey: "service.configuration.openai.translation.title", key: .openAITranslation @@ -132,10 +131,10 @@ protocol EnumLocalizedStringConvertible { var title: String { get } } -// MARK: - OpenAIModels +// MARK: - OpenAIModel // swiftlint:disable identifier_name -enum OpenAIModels: String, CaseIterable { +enum OpenAIModel: String, CaseIterable { case gpt3_5_turbo_0125 = "gpt-3.5-turbo-0125" case gpt4_0125_preview = "gpt-4-0125-preview" } @@ -144,7 +143,7 @@ enum OpenAIModels: String, CaseIterable { // swiftlint:enable identifier_name -extension OpenAIModels: EnumLocalizedStringConvertible { +extension OpenAIModel: EnumLocalizedStringConvertible { var title: String { rawValue } @@ -152,7 +151,7 @@ extension OpenAIModels: EnumLocalizedStringConvertible { // MARK: Defaults.Serializable -extension OpenAIModels: Defaults.Serializable {} +extension OpenAIModel: Defaults.Serializable {} // MARK: - OpenAIUsageStats diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift index 387ebd638..ed31c5fd4 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift @@ -141,7 +141,7 @@ struct ServiceConfigurationToggleCell: View { ServiceConfigurationPickerCell( titleKey: "service.configuration.openai.model.title", key: .openAIModel, - values: OpenAIModels.allCases + values: OpenAIModel.allCases ) ServiceConfigurationToggleCell(