diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 8590a896e..538001b31 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -5124,6 +5124,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" : { @@ -7864,4 +7896,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift b/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift index d39963fd7..fbc01a826 100644 --- a/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift +++ b/Easydict/Swift/Feature/Configuration/Configuration+Defaults.swift @@ -212,14 +212,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/Service/CustomOpenAI/CustomOpenAIService.swift b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift index 49b799174..d79053aea 100644 --- a/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift +++ b/Easydict/Swift/Service/CustomOpenAI/CustomOpenAIService.swift @@ -24,6 +24,14 @@ class CustomOpenAIService: BaseOpenAIService { // MARK: Internal + 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/LLMStreamService+Configuration.swift b/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift index 4305d90b6..79796720c 100644 --- a/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift +++ b/Easydict/Swift/Service/OpenAI/LLMStreamService+Configuration.swift @@ -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/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..24a20d286 100644 --- a/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceSecretConfigreValidatable.swift +++ b/Easydict/Swift/View/SettingView/Tabs/ServiceConfigurationView/ServiceSecretConfigreValidatable.swift @@ -32,3 +32,39 @@ 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() { + var allServiceTypes = EZLocalStorage.shared().allServiceTypes(windowType) + let uuid = UUID().uuidString + let newServiceType = "\(serviceType().rawValue)#\(uuid)" + allServiceTypes.append(newServiceType) + let newService = self + newService.uuid = uuid + EZLocalStorage.shared().setService(newService, windowType: windowType) + EZLocalStorage.shared().setAllServiceTypes(allServiceTypes, windowType: windowType) + NotificationCenter.default.postServiceUpdateNotification(windowType: windowType) + } + + func remove() { + let allServiceTypes = EZLocalStorage.shared().allServiceTypes(windowType) + .filter { $0 != "\(serviceType().rawValue)#\(uuid)" } + + EZLocalStorage.shared().setAllServiceTypes(allServiceTypes, windowType: windowType) + NotificationCenter.default.postServiceUpdateNotification(windowType: windowType) + } +} diff --git a/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift b/Easydict/Swift/View/SettingView/Tabs/TabView/ServiceTab.swift index cab82078c..7999c169f 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(serviceHasUpdatedPub) { _ in + viewModel.updateServices() + } } Group { @@ -60,6 +63,9 @@ struct ServiceTab: View { // MARK: Private + private let serviceHasUpdatedPub = NotificationCenter.default + .publisher(for: .serviceHasUpdated) + @StateObject private var viewModel: ServiceTabViewModel = .init() } @@ -89,26 +95,25 @@ 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.serviceType() == selectedService?.serviceType() && $0.uuid == selectedService?.uuid } + 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() + "\(service.serviceType())#\(service.uuid)" } - EZLocalStorage.shared().setAllServiceTypes(serviceTypes, windowType: windowType) postUpdateServiceNotification() - updateServices() } @@ -134,9 +139,9 @@ private struct ServiceItems: View { @EnvironmentObject private var viewModel: ServiceTabViewModel - private var servicesWithID: [(QueryService, String)] { - viewModel.services.map { service in - (service, service.serviceType().rawValue) + private var servicesWithID: [(QueryService, Int)] { + viewModel.services.enumerated().map { index, service in + (service, index) } } } diff --git a/Easydict/objc/Service/Model/EZQueryService.h b/Easydict/objc/Service/Model/EZQueryService.h index 57cd8c6f7..cb0964aec 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; /// 翻译结果 @@ -121,6 +122,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..ba1e77c23 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 { @@ -313,6 +313,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..bd44fce65 100644 --- a/Easydict/objc/Service/Model/EZServiceTypes.h +++ b/Easydict/objc/Service/Model/EZServiceTypes.h @@ -16,6 +16,7 @@ NS_SWIFT_NAME(ServiceTypes) @interface EZServiceTypes : NSObject @property (nonatomic, copy, readonly) NSArray *allServiceTypes; +@property (nonatomic, copy, readonly) NSArray *allServiceTypeIDs; + (instancetype)shared; diff --git a/Easydict/objc/Service/Model/EZServiceTypes.m b/Easydict/objc/Service/Model/EZServiceTypes.m index 91abbaae9..82b3b1372 100644 --- a/Easydict/objc/Service/Model/EZServiceTypes.m +++ b/Easydict/objc/Service/Model/EZServiceTypes.m @@ -74,10 +74,18 @@ - (nullable EZQueryService *)serviceWithType:(EZServiceType)type { return [Cls new]; } -- (NSArray *)servicesFromTypes:(NSArray *)types { +- (NSArray *)servicesFromTypes:(NSArray *)types { NSMutableArray *services = [NSMutableArray array]; - for (EZServiceType type in types) { + for (NSString *serviceType in types) { + NSString *type = serviceType; + NSString *uuid = @""; + if ([serviceType containsString:@"#"]) { + NSArray *serivceTypeId = [serviceType componentsSeparatedByString:@"#"]; + type = serivceTypeId[0]; + uuid = serivceTypeId[1]; + } EZQueryService *service = [self serviceWithType:type]; + service.uuid = uuid; // 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..8497c322d 100644 --- a/Easydict/objc/ViewController/Storage/EZLocalStorage.h +++ b/Easydict/objc/ViewController/Storage/EZLocalStorage.h @@ -29,18 +29,18 @@ 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; -- (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..f352e9ce1 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]; } @@ -139,13 +150,17 @@ - (EZQueryService *)service:(EZServiceType)serviceType windowType:(EZWindowType) } - (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 isEqual:@""]) { + return [NSString stringWithFormat:@"%@-%@-%ld", kServiceInfoStorageKey, serviceType, windowType]; + } + return [NSString stringWithFormat:@"%@-%@-%@-%ld", kServiceInfoStorageKey, serviceType, serviceId, windowType]; } - (NSString *)serviceTypesKeyOfWindowType:(EZWindowType)windowType {