diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 5f04480c3..dbb38eb78 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -28,3 +28,4 @@ #import "MMCrash.h" #import "EZDetectManager.h" #import "EZAppleDictionary.h" +#import "EZServiceTypes.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 517585cf2..0abcddd1c 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -6240,6 +6240,38 @@ } } }, + "service.configuration.duplicate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Duplicate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" + } + } + } + }, + "service.configuration.remove" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remove" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除" + } + } + } + }, "service.configuration.validation_fail" : { "localizations" : { "en" : { diff --git a/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift b/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift index fc0adf0d6..9b172f686 100644 --- a/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift +++ b/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift @@ -216,14 +216,20 @@ class ShortcutWrapper { } } -func defaultsKey(_ key: StoredKey, serviceType: ServiceType) -> Defaults.Key { - defaultsKey(key, serviceType: serviceType, defaultValue: nil) +func defaultsKey(_ key: StoredKey, serviceType: ServiceType, id: String) -> Defaults.Key { + defaultsKey(key, serviceType: serviceType, id: id, defaultValue: nil) } -func defaultsKey(_ key: StoredKey, serviceType: ServiceType, defaultValue: T) -> Defaults +func defaultsKey( + _ key: StoredKey, + serviceType: ServiceType, + id: String?, + defaultValue: T +) + -> Defaults .Key { Defaults.Key( - storedKey(key, serviceType: serviceType), + storedKey(key, serviceType: serviceType, id: id), default: defaultValue ) } diff --git a/Easydict/Swift/Feature/HTTPServer/Vapor/routes.swift b/Easydict/Swift/Feature/HTTPServer/Vapor/routes.swift index 9aff9d762..d15c05e84 100644 --- a/Easydict/Swift/Feature/HTTPServer/Vapor/routes.swift +++ b/Easydict/Swift/Feature/HTTPServer/Vapor/routes.swift @@ -15,11 +15,10 @@ func routes(_ app: Application) throws { app.post("translate") { req async throws -> TranslationResponse in let request = try req.content.decode(TranslationRequest.self) - let serviceType = ServiceType(rawValue: request.serviceType) let appleDictionaryNames = request.appleDictionaryNames - guard let service = ServiceTypes.shared().service(withType: serviceType) else { - throw TranslationError.unsupportedServiceType(serviceType.rawValue) + guard let service = ServiceTypes.shared().service(withTypeId: request.serviceType) else { + throw TranslationError.unsupportedServiceType(request.serviceType) } if let appleDictionary = service as? AppleDictionary, let appleDictionaryNames { @@ -29,7 +28,7 @@ func routes(_ app: Application) throws { if service.isStream() { throw TranslationError .invalidParameter( - "\(serviceType.rawValue) is stream service, which does not support 'translate' API. Please use 'streamTranslate." + "\(request.serviceType) is stream service, which does not support 'translate' API. Please use 'streamTranslate." ) } @@ -53,8 +52,8 @@ func routes(_ app: Application) throws { let request = try req.content.decode(TranslationRequest.self) let serviceType = ServiceType(rawValue: request.serviceType) - guard let service = ServiceTypes.shared().service(withType: serviceType) else { - throw TranslationError.unsupportedServiceType(serviceType.rawValue) + guard let service = ServiceTypes.shared().service(withTypeId: request.serviceType) else { + throw TranslationError.unsupportedServiceType(request.serviceType) } guard let streamService = service as? LLMStreamService else { diff --git a/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift index 49b799174..48a3b0de8 100644 --- a/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift +++ b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift @@ -24,6 +24,21 @@ class CustomOpenAIService: BaseOpenAIService { // MARK: Internal + override func serviceTypeWithUniqueIdentifier() -> String { + guard !uuid.isEmpty else { + return ServiceType.customOpenAI.rawValue + } + return "\(ServiceType.customOpenAI.rawValue)#\(uuid)" + } + + override func isDuplicatable() -> Bool { + true + } + + override func isRemovable(_ type: EZWindowType) -> Bool { + !uuid.isEmpty + } + override func configurationListItems() -> Any { StreamConfigurationView( service: self, diff --git a/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift b/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift index 9f5c93574..66b27f46a 100644 --- a/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift +++ b/Easydict/Swift/Service/OpenAI/BaseOpenAIService.swift @@ -23,6 +23,9 @@ public class BaseOpenAIService: LLMStreamService { completion: @escaping (EZQueryResult, Error?) -> () ) { Task { + result.isStreamFinished = false + result.isLoading = true + var resultText = "" let queryType = self.queryType(text: text, from: from, to: to) @@ -38,6 +41,7 @@ public class BaseOpenAIService: LLMStreamService { // Get final result text resultText = getFinalResultText(resultText) updateResultText(resultText, queryType: queryType, error: nil, completion: completion) + result.isLoading = false result.isStreamFinished = true } catch { // For stream requests, certain special cases may be normal for the first part of the data transfer, but the final parsing is incorrect. @@ -51,6 +55,7 @@ public class BaseOpenAIService: LLMStreamService { logError(String(describing: error)) } updateResultText(text, queryType: queryType, error: err, completion: completion) + result.isLoading = false result.isStreamFinished = true } } diff --git a/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift b/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift index 4305d90b6..47b463a6f 100644 --- a/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift +++ b/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift @@ -76,7 +76,7 @@ extension LLMStreamService { logInfo("service config changed: \(serviceType().rawValue), windowType: \(windowType.rawValue)") NotificationCenter.default.postServiceUpdateNotification( - serviceType: serviceType(), + serviceType: serviceTypeWithUniqueIdentifier(), windowType: windowType, autoQuery: autoQuery ) @@ -87,10 +87,10 @@ extension LLMStreamService { } func stringDefaultsKey(_ key: StoredKey, defaultValue: String) -> Defaults.Key { - defaultsKey(key, serviceType: serviceType(), defaultValue: defaultValue) + defaultsKey(key, serviceType: serviceType(), id: uuid, defaultValue: defaultValue) } func serviceDefaultsKey(_ key: StoredKey, defaultValue: T) -> Defaults.Key { - defaultsKey(key, serviceType: serviceType(), defaultValue: defaultValue) + defaultsKey(key, serviceType: serviceType(), id: uuid, defaultValue: defaultValue) } } diff --git a/Easydict/Swift/Service/OpenAI/LLMStreamService.swift b/Easydict/Swift/Service/OpenAI/LLMStreamService.swift index ff8e250e8..bf9d11517 100644 --- a/Easydict/Swift/Service/OpenAI/LLMStreamService.swift +++ b/Easydict/Swift/Service/OpenAI/LLMStreamService.swift @@ -323,6 +323,7 @@ extension LLMStreamService { ) { if result.isStreamFinished { cancelStream() + completion(result, error) return } diff --git a/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift b/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift index 87ca45106..fb831d7b7 100644 --- a/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift +++ b/Easydict/Swift/Utility/Extensions/Notification/Notification+Name.swift @@ -32,12 +32,12 @@ extension NSNotification { @objc extension NotificationCenter { func postServiceUpdateNotification( - serviceType: ServiceType = .init(rawValue: ""), + serviceType: String = "", windowType: EZWindowType = .none, autoQuery: Bool = false ) { let userInfo: [String: Any] = [ - EZServiceTypeKey: serviceType.rawValue, + EZServiceTypeKey: serviceType, EZWindowTypeKey: windowType.rawValue, EZAutoQueryKey: autoQuery, ] diff --git a/Easydict/Swift/Utility/GlobalContext.swift b/Easydict/Swift/Utility/GlobalContext.swift index 3cb11eb10..3c6682a98 100644 --- a/Easydict/Swift/Utility/GlobalContext.swift +++ b/Easydict/Swift/Utility/GlobalContext.swift @@ -21,22 +21,6 @@ class GlobalContext: NSObject { updaterDelegate: updaterHelper, userDriverDelegate: userDriverHelper ) - - for service in services { - if let llmService = service as? LLMStreamService { - llmService.setupSubscribers() - } - } - } - - // MARK: Public - - /// Retrieves the service of the specified type. - /// - /// - Parameter type: The type of service to retrieve. - /// - Returns: The service of the specified type. - public func getService(ofType type: ServiceType) -> QueryService? { - services.first(where: { $0.serviceType().rawValue.caseInsensitiveCompare(type.rawValue) == .orderedSame }) } // MARK: Internal @@ -61,6 +45,22 @@ class GlobalContext: NSObject { let updaterController: SPUStandardUpdaterController + // refresh subscribed services after duplicate service + func reloadLLMServicesSubscribers() { + for service in services { + if let llmService = service as? LLMStreamService { + llmService.cancelSubscribers() + } + } + let allServiceTypes = EZLocalStorage.shared().allServiceTypes(EZWindowType.main) + services = ServiceTypes.shared().services(fromTypes: allServiceTypes) + for service in services { + if let llmService = service as? LLMStreamService { + llmService.setupSubscribers() + } + } + } + // MARK: Private private let updaterHelper: SPUUpdaterHelper @@ -75,5 +75,5 @@ class GlobalContext: NSObject { 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. */ - private let services = EZLocalStorage.shared().allServices(.none) + private var services: [QueryService] = [] } diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift index 7888c4bdc..2ddd50b29 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceConfigurationSecretSectionView.swift @@ -41,20 +41,38 @@ struct ServiceConfigurationSecretSectionView: View { } var footer: some View { - Button { - validate() - } label: { - Group { - if viewModel.isValidating { - ProgressView() - .controlSize(.small) - .progressViewStyle(.circular) - } else { - Text("service.configuration.validate") + HStack { + if service.isDuplicatable() { + Button { + service.duplicate() + } label: { + Text("service.configuration.duplicate") + } + + if service.isRemovable(service.windowType) { + Button("service.configuration.remove", role: .destructive) { + service.remove() + } + } + + Spacer() + } + + Button { + validate() + } label: { + Group { + if viewModel.isValidating { + ProgressView() + .controlSize(.small) + .progressViewStyle(.circular) + } else { + Text("service.configuration.validate") + } } } + .disabled(viewModel.isValidateBtnDisabled) } - .disabled(viewModel.isValidateBtnDisabled) } var body: some View { diff --git a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceSecretConfigreValidatable.swift b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceSecretConfigreValidatable.swift index b5cf90faf..a775c7c61 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceSecretConfigreValidatable.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceSecretConfigreValidatable.swift @@ -32,3 +32,48 @@ extension QueryService: ServiceSecretConfigreValidatable { translate("曾经沧海难为水", from: .simplifiedChinese, to: .english, completion: completion) } } + +// MARK: - ServiceSecretConfigreDuplicatable + +protocol ServiceSecretConfigreDuplicatable { + func duplicate() + func remove() +} + +extension ServiceSecretConfigreDuplicatable { + func duplicate() {} + func remove() {} +} + +// MARK: - QueryService + ServiceSecretConfigreDuplicatable + +extension QueryService: ServiceSecretConfigreDuplicatable { + func duplicate() { + let uuid = UUID().uuidString + let newServiceType = "\(serviceType().rawValue)#\(uuid)" + guard let newService = ServiceTypes.shared().service(withTypeId: newServiceType) else { + return + } + newService.enabled = false + newService.resetServiceResult() + for winType in [EZWindowType.fixed, EZWindowType.main, EZWindowType.mini] { + var allServiceTypes = EZLocalStorage.shared().allServiceTypes(winType) + allServiceTypes.append(newServiceType) + newService.windowType = winType + EZLocalStorage.shared().setService(newService, windowType: winType) + EZLocalStorage.shared().setAllServiceTypes(allServiceTypes, windowType: winType) + NotificationCenter.default.postServiceUpdateNotification(windowType: winType) + } + GlobalContext.shared.reloadLLMServicesSubscribers() + } + + func remove() { + for winType in [EZWindowType.fixed, EZWindowType.main, EZWindowType.mini] { + let allServiceTypes = EZLocalStorage.shared().allServiceTypes(winType) + .filter { $0 != serviceTypeWithUniqueIdentifier() } + EZLocalStorage.shared().setAllServiceTypes(allServiceTypes, windowType: winType) + NotificationCenter.default.postServiceUpdateNotification(windowType: winType) + } + GlobalContext.shared.reloadLLMServicesSubscribers() + } +} diff --git a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift index cab82078c..6e013bf93 100644 --- a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift +++ b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift @@ -28,6 +28,9 @@ struct ServiceTab: View { .padding(.bottom) .padding(.horizontal) .frame(minWidth: 260) + .onReceive(serviceHasUpdatedNotification) { _ in + viewModel.updateServices() + } } Group { @@ -55,11 +58,17 @@ struct ServiceTab: View { } .layoutPriority(1) } + .onAppear { + GlobalContext.shared.reloadLLMServicesSubscribers() + } .environmentObject(viewModel) } // MARK: Private + private let serviceHasUpdatedNotification = NotificationCenter.default + .publisher(for: .serviceHasUpdated) + @StateObject private var viewModel: ServiceTabViewModel = .init() } @@ -89,26 +98,23 @@ private class ServiceTabViewModel: ObservableObject { } func updateServices() { - services = getServices() - } + services = EZLocalStorage.shared().allServices(windowType) - func getServices() -> [QueryService] { - EZLocalStorage.shared().allServices(windowType) + let isSelectedExist = services + .contains { $0.serviceTypeWithUniqueIdentifier() == selectedService?.serviceTypeWithUniqueIdentifier() } + if !isSelectedExist { + selectedService = nil + } } func onServiceItemMove(fromOffsets: IndexSet, toOffset: Int) { var services = services - services.move(fromOffsets: fromOffsets, toOffset: toOffset) - let serviceTypes = services.map { service in - service.serviceType() - } - + let serviceTypes = services.map { $0.serviceTypeWithUniqueIdentifier() } EZLocalStorage.shared().setAllServiceTypes(serviceTypes, windowType: windowType) postUpdateServiceNotification() - updateServices() } @@ -136,7 +142,7 @@ private struct ServiceItems: View { private var servicesWithID: [(QueryService, String)] { viewModel.services.map { service in - (service, service.serviceType().rawValue) + (service, service.serviceTypeWithUniqueIdentifier()) } } } diff --git a/Easydict/objc/Service/AudioPlayer/EZAudioPlayer.m b/Easydict/objc/Service/AudioPlayer/EZAudioPlayer.m index 6b4ce226e..8693cdeae 100644 --- a/Easydict/objc/Service/AudioPlayer/EZAudioPlayer.m +++ b/Easydict/objc/Service/AudioPlayer/EZAudioPlayer.m @@ -129,7 +129,7 @@ - (void)setIsPlaying:(BOOL)playing { - (EZQueryService *)defaultTTSService { EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; if (![_defaultTTSService.serviceType isEqualToString:defaultTTSServiceType]) { - EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; + EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithTypeId:defaultTTSServiceType]; _defaultTTSService = defaultTTSService; _defaultTTSService.audioPlayer = self; diff --git a/Easydict/objc/Service/Model/EZQueryResult.h b/Easydict/objc/Service/Model/EZQueryResult.h index 8af3cff89..a073a0ba4 100644 --- a/Easydict/objc/Service/Model/EZQueryResult.h +++ b/Easydict/objc/Service/Model/EZQueryResult.h @@ -101,7 +101,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) EZQueryModel *queryModel; -@property (nonatomic, copy) EZServiceType serviceType; +@property (nonatomic, copy) NSString *serviceTypeWithUniqueIdentifier; @property (nonatomic, weak) EZQueryService *service; @property (assign) BOOL isShowing; diff --git a/Easydict/objc/Service/Model/EZQueryResult.m b/Easydict/objc/Service/Model/EZQueryResult.m index b1cdf09ce..42f87da5d 100644 --- a/Easydict/objc/Service/Model/EZQueryResult.m +++ b/Easydict/objc/Service/Model/EZQueryResult.m @@ -255,7 +255,7 @@ - (void)reset { self.translatedResults = nil; self.wordResult = nil; self.error = nil; - self.serviceType = EZServiceTypeYoudao; + self.serviceTypeWithUniqueIdentifier = EZServiceTypeYoudao; [self.service.audioPlayer stop]; self.service = nil; self.isShowing = NO; diff --git a/Easydict/objc/Service/Model/EZQueryService.h b/Easydict/objc/Service/Model/EZQueryService.h index 57cd8c6f7..fde7627b6 100644 --- a/Easydict/objc/Service/Model/EZQueryService.h +++ b/Easydict/objc/Service/Model/EZQueryService.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(QueryService) @interface EZQueryService : NSObject +@property (nonatomic, strong) NSString *uuid; @property (nonatomic, strong) EZQueryModel *queryModel; /// 翻译结果 @@ -74,9 +75,12 @@ NS_SWIFT_NAME(QueryService) @interface EZQueryService () -/// 服务类型 +/// 服务类型,例如 Google - (EZServiceType)serviceType; +/// 唯一服务类型,默认为 serviceType。如果该服务支持复制,则后面添加 `#+UUID`,例如 Google#E621E1F8-C36C-495A-93FC-0C247A3E6E5F +- (NSString *)serviceTypeWithUniqueIdentifier; + /// 服务名字 - (NSString *)name; @@ -121,6 +125,10 @@ NS_SWIFT_NAME(QueryService) - (BOOL)isStream; +- (BOOL)isDuplicatable; + +- (BOOL)isRemovable:(EZWindowType)type; + /// 获取文本的语言 /// @param text 文本 /// @param completion 回调 diff --git a/Easydict/objc/Service/Model/EZQueryService.m b/Easydict/objc/Service/Model/EZQueryService.m index d5f9815c7..7d7f75ba5 100644 --- a/Easydict/objc/Service/Model/EZQueryService.m +++ b/Easydict/objc/Service/Model/EZQueryService.m @@ -65,7 +65,7 @@ - (EZDetectManager *)detectManager { - (void)setEnabledQuery:(BOOL)enabledQuery { _enabledQuery = enabledQuery; - [[EZLocalStorage shared] setEnabledQuery:enabledQuery serviceType:self.serviceType windowType:self.windowType]; + [[EZLocalStorage shared] setEnabledQuery:enabledQuery serviceType:self.serviceType serviceId:self.uuid windowType:self.windowType]; } - (BOOL)enabledAutoQuery { @@ -89,7 +89,7 @@ - (void)setResult:(EZQueryResult *)translateResult { _result = translateResult; _result.service = self; - _result.serviceType = self.serviceType; + _result.serviceTypeWithUniqueIdentifier = self.serviceTypeWithUniqueIdentifier; _result.queryModel = self.queryModel; } @@ -257,6 +257,10 @@ - (EZServiceType)serviceType { return nil; } +- (NSString *)serviceTypeWithUniqueIdentifier { + return [self serviceType]; +} + - (NSString *)name { MethodNotImplemented(); return nil; @@ -313,6 +317,14 @@ - (BOOL)isStream { return NO; } +- (BOOL)isDuplicatable { + return NO; +} + +- (BOOL)isRemovable:(EZWindowType)type { + return YES; +} + - (void)detectText:(NSString *)text completion:(void (^)(EZLanguage language, NSError *_Nullable error))completion { MethodNotImplemented(); } diff --git a/Easydict/objc/Service/Model/EZServiceTypes.h b/Easydict/objc/Service/Model/EZServiceTypes.h index 3ad5f9136..17450497b 100644 --- a/Easydict/objc/Service/Model/EZServiceTypes.h +++ b/Easydict/objc/Service/Model/EZServiceTypes.h @@ -16,12 +16,13 @@ NS_SWIFT_NAME(ServiceTypes) @interface EZServiceTypes : NSObject @property (nonatomic, copy, readonly) NSArray *allServiceTypes; +@property (nonatomic, copy, readonly) NSArray *allServiceTypeIDs; + (instancetype)shared; -- (nullable EZQueryService *)serviceWithType:(EZServiceType)type; +- (nullable EZQueryService *)serviceWithTypeId:(NSString *)typeIdIfHave; -- (NSArray *)servicesFromTypes:(NSArray *)types; +- (NSArray *)servicesFromTypes:(NSArray *)types; @end diff --git a/Easydict/objc/Service/Model/EZServiceTypes.m b/Easydict/objc/Service/Model/EZServiceTypes.m index 91abbaae9..5086fe7e5 100644 --- a/Easydict/objc/Service/Model/EZServiceTypes.m +++ b/Easydict/objc/Service/Model/EZServiceTypes.m @@ -69,15 +69,26 @@ + (instancetype)allocWithZone:(struct _NSZone *)zone { return allServiceDict; } -- (nullable EZQueryService *)serviceWithType:(EZServiceType)type { +// pass service type with id format like `EZServiceTypeCustomOpenAI#UUID` to support multi instances +- (nullable EZQueryService *)serviceWithTypeId:(NSString *)typeIdIfHave { + NSString *type = typeIdIfHave; + NSString *uuid = @""; + if ([typeIdIfHave containsString:@"#"]) { + NSArray *items = [typeIdIfHave componentsSeparatedByString:@"#"]; + type = items[0]; + uuid = items[1]; + } Class Cls = [[self allServiceDict] objectForKey:type]; - return [Cls new]; + EZQueryService *service = [Cls new]; + service.uuid = uuid; + return service; } -- (NSArray *)servicesFromTypes:(NSArray *)types { + +- (NSArray *)servicesFromTypes:(NSArray *)types { NSMutableArray *services = [NSMutableArray array]; - for (EZServiceType type in types) { - EZQueryService *service = [self serviceWithType:type]; + for (NSString *serviceType in types) { + EZQueryService *service = [self serviceWithTypeId:serviceType]; // Maybe OpenAI has been disabled. if (service) { [services addObject:service]; diff --git a/Easydict/objc/ViewController/Model/EZServiceInfo.h b/Easydict/objc/ViewController/Model/EZServiceInfo.h index 26589f22b..2bffc2141 100644 --- a/Easydict/objc/ViewController/Model/EZServiceInfo.h +++ b/Easydict/objc/ViewController/Model/EZServiceInfo.h @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN @interface EZServiceInfo : NSObject +@property (nonatomic, strong) NSString *uuid; @property (nonatomic, assign) EZServiceType type; @property (nonatomic, assign) BOOL enabled; @property (nonatomic, assign) BOOL enabledQuery; diff --git a/Easydict/objc/ViewController/Model/EZServiceInfo.m b/Easydict/objc/ViewController/Model/EZServiceInfo.m index 04479bb92..5caf14a8f 100644 --- a/Easydict/objc/ViewController/Model/EZServiceInfo.m +++ b/Easydict/objc/ViewController/Model/EZServiceInfo.m @@ -16,7 +16,7 @@ + (instancetype)serviceInfoWithService:(EZQueryService *)service { serviceInfo.enabled = service.enabled; serviceInfo.enabledQuery = service.enabledQuery; serviceInfo.windowType = service.windowType; - + serviceInfo.uuid = service.uuid; return serviceInfo; } diff --git a/Easydict/objc/ViewController/Storage/EZLocalStorage.h b/Easydict/objc/ViewController/Storage/EZLocalStorage.h index 7166f31a6..5c075e6f5 100644 --- a/Easydict/objc/ViewController/Storage/EZLocalStorage.h +++ b/Easydict/objc/ViewController/Storage/EZLocalStorage.h @@ -29,18 +29,19 @@ static NSString *const EZAutoQueryKey = @"EZAutoQueryKey"; + (void)destroySharedInstance; -- (NSArray *)allServiceTypes:(EZWindowType)windowType; -- (void)setAllServiceTypes:(NSArray *)allServiceTypes windowType:(EZWindowType)windowType; +- (NSArray *)allServiceTypes:(EZWindowType)windowType; +- (void)setAllServiceTypes:(NSArray *)allServiceTypes windowType:(EZWindowType)windowType; - (NSArray *)allServices:(EZWindowType)windowType; -- (EZQueryService *)service:(EZServiceType)serviceType windowType:(EZWindowType)windowType; +// pass service type with uuid to support service multi instance +- (EZQueryService *)service:(NSString *)serviceTypeId windowType:(EZWindowType)windowType; -- (nullable EZServiceInfo *)serviceInfoWithType:(EZServiceType)type windowType:(EZWindowType)windowType; +- (nullable EZServiceInfo *)serviceInfoWithType:(EZServiceType)type serviceId:(NSString *)serviceId windowType:(EZWindowType)windowType; - (void)setServiceInfo:(EZServiceInfo *)service windowType:(EZWindowType)windowType; - (void)setService:(EZQueryService *)service windowType:(EZWindowType)windowType; -- (void)setEnabledQuery:(BOOL)enabledQuery serviceType:(EZServiceType)serviceType windowType:(EZWindowType)windowType; +- (void)setEnabledQuery:(BOOL)enabledQuery serviceType:(EZServiceType)serviceType serviceId:(NSString *)serviceId windowType:(EZWindowType)windowType; - (void)increaseQueryCount:(NSString *)queryText; - (NSInteger)queryCount; diff --git a/Easydict/objc/ViewController/Storage/EZLocalStorage.m b/Easydict/objc/ViewController/Storage/EZLocalStorage.m index 1be3136cf..fa5cf0eb3 100644 --- a/Easydict/objc/ViewController/Storage/EZLocalStorage.m +++ b/Easydict/objc/ViewController/Storage/EZLocalStorage.m @@ -54,13 +54,24 @@ + (instancetype)allocWithZone:(struct _NSZone *)zone { // Init data, save all service info - (void)setup { - NSArray *allServiceTypes = [EZServiceTypes.shared allServiceTypes]; NSArray *allWindowTypes = @[ @(EZWindowTypeMini), @(EZWindowTypeFixed), @(EZWindowTypeMain) ]; for (NSNumber *number in allWindowTypes) { EZWindowType windowType = [number integerValue]; - for (EZServiceType serviceType in allServiceTypes) { - EZServiceInfo *serviceInfo = [self serviceInfoWithType:serviceType windowType:windowType]; + NSArray *allServiceTypes = [self allServiceTypes:windowType]; + + for (NSString *serviceType in allServiceTypes) { + NSString *type = @""; + NSString *uuid = @""; + if ([serviceType containsString:@"#"]) { + NSArray *serivceTypeId = [serviceType componentsSeparatedByString:@"#"]; + type = serivceTypeId[0]; + uuid = serivceTypeId[1]; + } else { + type = serviceType; + } + + EZServiceInfo *serviceInfo = [self serviceInfoWithType:type serviceId:uuid windowType:windowType]; // New service. if (!serviceInfo) { @@ -68,6 +79,7 @@ - (void)setup { serviceInfo.type = serviceType; serviceInfo.enabled = YES; serviceInfo.enabledQuery = YES; + serviceInfo.uuid = uuid; /** Fix https://github.com/tisfeng/Easydict/issues/269 and https://github.com/tisfeng/Easydict/issues/372 @@ -97,10 +109,9 @@ - (void)setup { } } -- (NSArray *)allServiceTypes:(EZWindowType)windowType { +- (NSArray *)allServiceTypes:(EZWindowType)windowType { NSString *allServiceTypesKey = [self serviceTypesKeyOfWindowType:windowType]; NSArray *allServiceTypes = EZServiceTypes.shared.allServiceTypes; - NSArray *allStoredServiceTypes = [[NSUserDefaults standardUserDefaults] objectForKey:allServiceTypesKey]; if (!allStoredServiceTypes) { allStoredServiceTypes = allServiceTypes; @@ -116,10 +127,10 @@ - (void)setup { } allStoredServiceTypes = [array copy]; } - return allStoredServiceTypes; } -- (void)setAllServiceTypes:(NSArray *)allServiceTypes windowType:(EZWindowType)windowType { + +- (void)setAllServiceTypes:(NSArray *)allServiceTypes windowType:(EZWindowType)windowType { NSString *allServiceTypesKey = [self serviceTypesKeyOfWindowType:windowType]; [[NSUserDefaults standardUserDefaults] setObject:allServiceTypes forKey:allServiceTypesKey]; } @@ -132,20 +143,24 @@ - (void)setAllServiceTypes:(NSArray *)allServiceTypes windowType: return allServices; } -- (EZQueryService *)service:(EZServiceType)serviceType windowType:(EZWindowType)windowType { - EZQueryService *service = [EZServiceTypes.shared serviceWithType:serviceType]; +- (EZQueryService *)service:(NSString *)serviceTypeId windowType:(EZWindowType)windowType { + EZQueryService *service = [EZServiceTypes.shared serviceWithTypeId:serviceTypeId]; [self updateServiceInfo:service windowType:windowType]; return service; } - (void)updateServiceInfo:(EZQueryService *)service windowType:(EZWindowType)windowType { - EZServiceInfo *serviceInfo = [self serviceInfoWithType:service.serviceType windowType:windowType]; + EZServiceInfo *serviceInfo = [self serviceInfoWithType:service.serviceType serviceId:service.uuid windowType:windowType]; BOOL enabled = YES; BOOL enabledQuery = YES; + NSString *uuid = @""; if (serviceInfo) { enabled = serviceInfo.enabled; enabledQuery = serviceInfo.enabledQuery; + uuid = serviceInfo.uuid; } + // update id + service.uuid = uuid; service.enabled = enabled; service.enabledQuery = enabledQuery; service.windowType = windowType; @@ -154,11 +169,12 @@ - (void)updateServiceInfo:(EZQueryService *)service windowType:(EZWindowType)win - (void)setServiceInfo:(EZServiceInfo *)serviceInfo windowType:(EZWindowType)windowType { // ???: if save EZQueryService, mj_JSONData will dead cycle. NSData *data = [serviceInfo mj_JSONData]; - NSString *serviceInfoKey = [self keyForServiceType:serviceInfo.type windowType:windowType]; + NSString *serviceInfoKey = [self keyForServiceType:serviceInfo.type serviceId:serviceInfo.uuid windowType:windowType]; [[NSUserDefaults standardUserDefaults] setObject:data forKey:serviceInfoKey]; } -- (nullable EZServiceInfo *)serviceInfoWithType:(EZServiceType)type windowType:(EZWindowType)windowType { - NSString *serviceInfoKey = [self keyForServiceType:type windowType:windowType]; + +- (nullable EZServiceInfo *)serviceInfoWithType:(EZServiceType)type serviceId:(NSString *)serviceId windowType:(EZWindowType)windowType { + NSString *serviceInfoKey = [self keyForServiceType:type serviceId:serviceId windowType:windowType]; NSData *data = [[NSUserDefaults standardUserDefaults] objectForKey:serviceInfoKey]; EZServiceInfo *serviceInfo = nil; @@ -174,8 +190,8 @@ - (void)setService:(EZQueryService *)service windowType:(EZWindowType)windowType [self setServiceInfo:serviceInfo windowType:windowType]; } -- (void)setEnabledQuery:(BOOL)enabledQuery serviceType:(EZServiceType)serviceType windowType:(EZWindowType)windowType { - EZServiceInfo *service = [self serviceInfoWithType:serviceType windowType:windowType]; +- (void)setEnabledQuery:(BOOL)enabledQuery serviceType:(EZServiceType)serviceType serviceId:(NSString *)serviceId windowType:(EZWindowType)windowType { + EZServiceInfo *service = [self serviceInfoWithType:serviceType serviceId:serviceId windowType:windowType]; service.enabledQuery = enabledQuery; [self setServiceInfo:service windowType:windowType]; } @@ -345,8 +361,11 @@ - (NSString *)queryLevelTitle:(NSInteger)level chineseFlag:(BOOL)chineseFlag { #pragma mark - Service type key -- (NSString *)keyForServiceType:(EZServiceType)serviceType windowType:(EZWindowType)windowType { - return [NSString stringWithFormat:@"%@-%@-%ld", kServiceInfoStorageKey, serviceType, windowType]; +- (NSString *)keyForServiceType:(EZServiceType)serviceType serviceId: (NSString *)serviceId windowType:(EZWindowType)windowType { + if (!serviceId || [serviceId isEqual:@""]) { + return [NSString stringWithFormat:@"%@-%@-%ld", kServiceInfoStorageKey, serviceType, windowType]; + } + return [NSString stringWithFormat:@"%@-%@-%@-%ld", kServiceInfoStorageKey, serviceType, serviceId, windowType]; } - (NSString *)serviceTypesKeyOfWindowType:(EZWindowType)windowType { diff --git a/Easydict/objc/ViewController/View/ResultView/EZResultView.m b/Easydict/objc/ViewController/View/ResultView/EZResultView.m index e95c96ae1..0f9652e17 100644 --- a/Easydict/objc/ViewController/View/ResultView/EZResultView.m +++ b/Easydict/objc/ViewController/View/ResultView/EZResultView.m @@ -173,7 +173,7 @@ - (void)setup { [stopButton setClickBlock:^(EZButton *_Nonnull button) { mm_strongify(self); - [self.result.queryModel stopServiceRequest:self.result.serviceType]; + [self.result.queryModel stopServiceRequest:self.result.serviceTypeWithUniqueIdentifier]; self.result.isStreamFinished = YES; button.hidden = YES; }]; @@ -260,7 +260,7 @@ - (void)setup { - (void)setResult:(EZQueryResult *)result { _result = result; - EZServiceType serviceType = result.serviceType; + EZServiceType serviceType = result.service.serviceType; self.serviceIcon.image = [NSImage imageNamed:serviceType]; self.serviceNameLabel.attributedStringValue = [NSAttributedString mm_attributedStringWithString:result.service.name font:[NSFont systemFontOfSize:13]]; diff --git a/Easydict/objc/ViewController/View/WordResultView/EZWordResultView.m b/Easydict/objc/ViewController/View/WordResultView/EZWordResultView.m index d9b376898..4849ee4f6 100644 --- a/Easydict/objc/ViewController/View/WordResultView/EZWordResultView.m +++ b/Easydict/objc/ViewController/View/WordResultView/EZWordResultView.m @@ -139,7 +139,7 @@ - (void)refreshWithResult:(EZQueryResult *)result { [self addSubview:resultLabel]; // OpenAI result text has its own paragraph style. - if ([result.serviceType isEqualToString:EZServiceTypeOpenAI]) { + if ([result.serviceTypeWithUniqueIdentifier isEqualToString:EZServiceTypeOpenAI]) { resultLabel.paragraphSpacing = 0; } @@ -770,12 +770,12 @@ - (void)refreshWithResult:(EZQueryResult *)result { // For some special case, copied text language is not the queryTargetLanguage, like 龘, Youdao translate. EZLanguage language = [EZAppleService.shared detectText:text]; - if ([result.serviceType isEqualToString:EZServiceTypeOpenAI]) { + if ([result.serviceTypeWithUniqueIdentifier isEqualToString:EZServiceTypeOpenAI]) { language = result.to; } EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; - EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; + EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithTypeId:defaultTTSServiceType]; [result.service.audioPlayer playTextAudio:text language:language @@ -831,7 +831,7 @@ - (void)refreshWithResult:(EZQueryResult *)result { linkButton.image = linkImage; NSString *toolTip = NSLocalizedString(@"open_web_link", nil); - if (result.serviceType == EZServiceTypeAppleDictionary) { + if (result.serviceTypeWithUniqueIdentifier == EZServiceTypeAppleDictionary) { toolTip = NSLocalizedString(@"open_in_apple_dictionary", nil); } linkButton.toolTip = toolTip; @@ -872,7 +872,7 @@ - (void)refreshWithResult:(EZQueryResult *)result { }]; // webView height need time to calculate, and the value will be called back later. - if (result.serviceType == EZServiceTypeAppleDictionary) { + if (result.serviceTypeWithUniqueIdentifier == EZServiceTypeAppleDictionary) { BOOL hasHTML = result.HTMLString.length > 0; linkButton.enabled = hasHTML; diff --git a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index 0289b88ad..a5091d046 100644 --- a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -58,7 +58,7 @@ @interface EZBaseQueryViewController () *serviceTypes; +@property (nonatomic, strong) NSArray *serviceTypeIds; @property (nonatomic, strong) NSArray *services; @property (nonatomic, strong) EZQueryModel *queryModel; @@ -202,7 +202,7 @@ - (void)modifyLanduage:(NSNotification *)notification { - (void)setupServices:(NSArray *)allServices { - NSMutableArray *serviceTypes = [NSMutableArray array]; + NSMutableArray *serviceTypeIds = [NSMutableArray array]; NSMutableArray *services = [NSMutableArray array]; self.youdaoService = nil; @@ -213,7 +213,7 @@ - (void)setupServices:(NSArray *)allServices { [self resetService:service]; [services addObject:service]; - [serviceTypes addObject:service.serviceType]; + [serviceTypeIds addObject:service.serviceTypeWithUniqueIdentifier]; } EZServiceType serviceType = service.serviceType; @@ -226,7 +226,7 @@ - (void)setupServices:(NSArray *)allServices { } } self.services = services; - self.serviceTypes = serviceTypes; + self.serviceTypeIds = serviceTypeIds; self.audioPlayer = [[EZAudioPlayer alloc] init]; if (!self.youdaoService) { @@ -375,7 +375,7 @@ - (NSString *)queryText { - (EZQueryService *)defaultTTSService { EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; if (![_defaultTTSService.serviceType isEqualToString:defaultTTSServiceType]) { - _defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; + _defaultTTSService = [EZServiceTypes.shared serviceWithTypeId:defaultTTSServiceType]; } return _defaultTTSService; } @@ -749,7 +749,7 @@ - (void)queryAllSerives:(EZQueryModel *)queryModel { for (EZQueryService *service in self.services) { BOOL enableAutoQuery = service.enabledQuery && service.enabledAutoQuery && service.queryTextType != EZQueryTextTypeNone; if (!enableAutoQuery) { - MMLogInfo(@"service disabled: %@", service.serviceType); + MMLogInfo(@"service disabled: %@", service.serviceTypeWithUniqueIdentifier); continue; } @@ -799,7 +799,7 @@ - (void)queryWithModel:(EZQueryModel *)queryModel service:(EZQueryService *)service completion:(nonnull void (^)(EZQueryResult *result, NSError *_Nullable error))completion { if (!service.enabledQuery) { - MMLogWarn(@"service disabled: %@", service.serviceType); + MMLogWarn(@"service disabled: %@", service.serviceTypeWithUniqueIdentifier); return; } if (queryModel.queryText.length == 0) { @@ -1025,7 +1025,7 @@ - (void)updateTableViewRowIndexes:(NSIndexSet *)rowIndexes [NSAnimationContext runAnimationGroup:^(NSAnimationContext *_Nonnull context) { context.duration = duration; context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - + // !!!: Must first notify the update tableView cell height, and then calculate the tableView height. // MMLogInfo(@"noteHeightOfRowsWithIndexesChanged: %@", rowIndexes); [self.tableView noteHeightOfRowsWithIndexesChanged:rowIndexes]; @@ -1109,9 +1109,9 @@ - (nullable NSIndexSet *)indexSetOfResult:(EZQueryResult *)result { /// !!!: Maybe return NSNotFound - (NSUInteger)rowIndexOfResult:(EZQueryResult *)result { - EZServiceType serviceType = result.serviceType; + NSString *serviceTypeWithUniqueIdentifier = result.serviceTypeWithUniqueIdentifier; // Sometimes the query is very slow, and at that time the user may have turned off the service in the settings page. - NSInteger row = [self.serviceTypes indexOfObject:serviceType]; + NSInteger row = [self.serviceTypeIds indexOfObject:serviceTypeWithUniqueIdentifier]; return row; } @@ -1138,23 +1138,23 @@ - (void)resetService:(EZQueryService *)service { service.windowType = self.windowType; } -- (void)updateService:(NSString *)serviceType autoQuery:(BOOL)autoQuery { +- (void)updateService:(NSString *)serviceTypeWithUniqueIdentifier autoQuery:(BOOL)autoQuery { NSMutableArray *newServices = [self.services mutableCopy]; for (EZQueryService *service in self.services) { - if (service.serviceType == serviceType) { + if ([service.serviceTypeWithUniqueIdentifier isEqualToString:serviceTypeWithUniqueIdentifier]) { if (!autoQuery) { [self updateCellWithResult:service.result reloadData:YES completionHandler:nil]; return; } - EZQueryService *updatedService = [EZLocalStorage.shared service:serviceType windowType:self.windowType]; - + EZQueryService *updatedService = [EZLocalStorage.shared service:service.serviceTypeWithUniqueIdentifier windowType:self.windowType]; + // 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. if ([service isKindOfClass:EZLLMStreamService.class]) { [((EZLLMStreamService *)service) cancelSubscribers]; } - - NSInteger index = [self.serviceTypes indexOfObject:serviceType]; + + NSInteger index = [self.serviceTypeIds indexOfObject:serviceTypeWithUniqueIdentifier]; newServices[index] = updatedService; self.services = newServices.copy; @@ -1196,7 +1196,7 @@ - (void)resetQueryAndResults { } - (nullable EZResultView *)resultCellOfResult:(EZQueryResult *)result { - NSInteger index = [self.serviceTypes indexOfObject:result.service.serviceType]; + NSInteger index = [self.serviceTypeIds indexOfObject:result.service.serviceTypeWithUniqueIdentifier]; if (index != NSNotFound) { NSInteger row = index + [self resultCellOffset]; EZResultView *resultCell = [[[self.tableView rowViewAtRow:row makeIfNecessary:NO] subviews] firstObject]; @@ -1501,7 +1501,7 @@ - (EZQueryService *)serviceAtRow:(NSInteger)row { MMLogError(@"error row: %ld, windowType: %ld", row, self.windowType); return nil; } - + EZQueryService *service = self.services[index]; return service; } @@ -1512,8 +1512,8 @@ - (BOOL)isTipsCell:(NSInteger)row { } -- (nullable EZQueryService *)serviceWithType:(EZServiceType)serviceType { - NSInteger index = [self.serviceTypes indexOfObject:serviceType]; +- (nullable EZQueryService *)serviceWithType:(NSString *)serviceTypeId { + NSInteger index = [self.serviceTypeIds indexOfObject:serviceTypeId]; if (index != NSNotFound) { return self.services[index]; }