diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index adc5e75bc..9bb89217d 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -229,7 +229,6 @@ 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 */; }; - 0A057D6F2B499A0B0025C51D /* ServiceItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A057D6E2B499A0B0025C51D /* ServiceItemView.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 */; }; 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */; }; @@ -273,6 +272,9 @@ EA9943EE2B5353AB00EE7B97 /* WindowTypeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */; }; EA9943F02B5354C400EE7B97 /* ShowWindowPositionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943EF2B5354C400EE7B97 /* ShowWindowPositionExtensions.swift */; }; EA9943F22B5358BF00EE7B97 /* LanguageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943F12B5358BF00EE7B97 /* LanguageExtensions.swift */; }; + EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */; }; + EAED41EF2B54B1430005FE0A /* ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */; }; + EAED41F22B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -702,7 +704,6 @@ 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 = ""; }; - 0A057D6E2B499A0B0025C51D /* ServiceItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceItemView.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 = ""; }; 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; @@ -761,6 +762,9 @@ EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTypeExtensions.swift; sourceTree = ""; }; EA9943EF2B5354C400EE7B97 /* ShowWindowPositionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowWindowPositionExtensions.swift; sourceTree = ""; }; EA9943F12B5358BF00EE7B97 /* LanguageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageExtensions.swift; sourceTree = ""; }; + EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceConfigurationSection.swift; sourceTree = ""; }; + EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableService.swift; sourceTree = ""; }; + EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAIService+ConfigurableService.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -2051,7 +2055,6 @@ isa = PBXGroup; children = ( 27FE980A2B3DD5D1000AD654 /* MenuItemView.swift */, - 0A057D6E2B499A0B0025C51D /* ServiceItemView.swift */, 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */, 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */, 27FE98072B3DD52B000AD654 /* SettingView */, @@ -2071,6 +2074,7 @@ 27FE980C2B3DD749000AD654 /* Tabs */ = { isa = PBXGroup; children = ( + EAED41EA2B54A4900005FE0A /* ServiceConfiguration */, 278540332B3DE04F004E9488 /* GeneralTab.swift */, 0A057D6C2B499A000025C51D /* ServiceTab.swift */, 276742042B3DC230002A2C75 /* PrivacyTab.swift */, @@ -2203,6 +2207,7 @@ EA9943DD2B534BAE00EE7B97 /* Utility */ = { isa = PBXGroup; children = ( + EAED41ED2B54B1390005FE0A /* Protocol */, EA9943E62B534D7C00EE7B97 /* Extensions */, ); path = Utility; @@ -2219,6 +2224,7 @@ EA9943E62B534D7C00EE7B97 /* Extensions */ = { isa = PBXGroup; children = ( + EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */, EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */, EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */, EA9943EF2B5354C400EE7B97 /* ShowWindowPositionExtensions.swift */, @@ -2227,6 +2233,30 @@ path = Extensions; sourceTree = ""; }; + EAED41EA2B54A4900005FE0A /* ServiceConfiguration */ = { + isa = PBXGroup; + children = ( + EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */, + ); + path = ServiceConfiguration; + sourceTree = ""; + }; + EAED41ED2B54B1390005FE0A /* Protocol */ = { + isa = PBXGroup; + children = ( + EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */ = { + isa = PBXGroup; + children = ( + EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */, + ); + path = "QueryService+ConfigurableService"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2602,7 +2632,6 @@ 03991166292A8A4400E1B06D /* EZTitleBarMoveView.m in Sources */, 03542A582937CC3200C34C33 /* EZConfiguration.m in Sources */, 27FE98092B3DD536000AD654 /* SettingView.swift in Sources */, - 0A057D6F2B499A0B0025C51D /* ServiceItemView.swift in Sources */, 035E37E72A0953120061DFAF /* EZToast.m in Sources */, 03542A492937B5CF00C34C33 /* EZGoogleTranslate.m in Sources */, 03D0435A2928C4C800E7559E /* EZWindowManager.m in Sources */, @@ -2634,6 +2663,7 @@ 03F14A3B2956016B00CB7379 /* EZVolcanoTranslate.m in Sources */, 03B0230429231FA6001C7E63 /* EZHoverButton.m in Sources */, 0342A9812AD64924002A9F5F /* NSString+EZSplit.m in Sources */, + EAED41EF2B54B1430005FE0A /* ConfigurableService.swift in Sources */, 03BD2825294875AE00F5891A /* EZMyLabel.m in Sources */, 03B0233029231FA6001C7E63 /* MMCrashUncaughtExceptionHandler.m in Sources */, 03D5FCFF2A5EF4E400AD26BE /* EZDeviceSystemInfo.m in Sources */, @@ -2775,6 +2805,7 @@ 039F5508294B6E29004AB940 /* EZAboutViewController.m in Sources */, 03D8A6592A42A1A300D9A968 /* EZAppModel.m in Sources */, 036E7D7B293F4FC8002675DF /* EZOpenLinkButton.m in Sources */, + EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */, 276742092B3DC230002A2C75 /* AboutTab.swift in Sources */, 03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */, DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */, @@ -2790,6 +2821,7 @@ 03008B3F29444B0A0062B821 /* NSView+EZAnimatedHidden.m in Sources */, 03B022FD29231FA6001C7E63 /* EZFixedQueryWindow.m in Sources */, 03B0232C29231FA6001C7E63 /* NSView+MM.m in Sources */, + EAED41F22B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift in Sources */, 033C31002A74CECE0095926A /* EZAppleDictionary.m in Sources */, 03E2BF752A298F2B00E010F3 /* NSString+EZUtils.m in Sources */, 03B022F529231FA6001C7E63 /* EZDetectManager.m in Sources */, diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 8030b13fa..f98fcafec 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -26,3 +26,5 @@ #import "NSString+EZConvenience.h" #import "EZWindowManager.h" #import "NSViewController+EZWindow.h" + +#import "EZOpenAIService.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index ae094d259..2114e3a63 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1,16 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "" - } - } - } - }, "about" : { "comment" : "about", "localizations" : { @@ -505,7 +495,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "[Beta] SwiftUI App模式" } } @@ -1827,7 +1817,14 @@ } }, "none_window" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "ocr_result_is_empty" : { "localizations" : { @@ -2316,6 +2313,92 @@ } } }, + "service.configuration.openai.api_key.footer" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key的一些说明或者加入链接" + } + } + } + }, + "service.configuration.openai.api_key.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenAI API Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenAI API Key" + } + } + } + }, + "service.configuration.openai.api_key.prompt" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + } + } + } + }, + "service.configuration.openai.api_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenAI API Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "OpenAI API Key" + } + } + } + }, + "service.configuration.openai.translation.footer" : { + + }, + "service.configuration.openai.translation.header" : { + + }, + "service.configuration.openai.translation.prompt" : { + + }, + "service.configuration.openai.translation.title" : { + + }, + "service.service_configuration.reset" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reset" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "重置" + } + } + } + }, "setting_general" : { "localizations" : { "en" : { @@ -2780,6 +2863,38 @@ } } }, + "setting.service.detail.no_configuration %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No configuration for %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@没有可供配置的选项" + } + } + } + }, + "setting.service.detail.no_selection" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select a service to show configuration" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "选择服务以查看配置" + } + } + } + }, "setting.tts_service.options.apple" : { "localizations" : { "en" : { @@ -3205,7 +3320,14 @@ } }, "unknown_option" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "unpin" : { "localizations" : { diff --git a/Easydict/Feature/Service/Ali/AliService.swift b/Easydict/Feature/Service/Ali/AliService.swift index 6ba555109..9788fc063 100644 --- a/Easydict/Feature/Service/Ali/AliService.swift +++ b/Easydict/Feature/Service/Ali/AliService.swift @@ -8,6 +8,7 @@ import Alamofire import CryptoKit +import Defaults import Foundation @objc(EZAliService) @@ -76,8 +77,10 @@ class AliService: QueryService { easydict://writeKeyValue?EZAliAccessKeyId= easydict://writeKeyValue?EZAliAccessKeySecret= */ - if let id = UserDefaults.standard.string(forKey: EZAliAccessKeyId), - let secret = UserDefaults.standard.string(forKey: EZAliAccessKeySecret), !id.isEmpty, !secret.isEmpty + if let id = Defaults[.aliAccessKeyId], + let secret = Defaults[.aliAccessKeySecret], + !id.isEmpty, + !secret.isEmpty { requestByAPI(id: id, secret: secret, transType: transType, text: text, from: from, to: to, completion: completion) } else { // use web api diff --git a/Easydict/Feature/Service/Caiyun/CaiyunService.swift b/Easydict/Feature/Service/Caiyun/CaiyunService.swift index 2c6b3dcf3..b36cfbdf8 100644 --- a/Easydict/Feature/Service/Caiyun/CaiyunService.swift +++ b/Easydict/Feature/Service/Caiyun/CaiyunService.swift @@ -7,6 +7,7 @@ // import Alamofire +import Defaults import Foundation @objc(EZCaiyunService) @@ -44,7 +45,7 @@ public final class CaiyunService: QueryService { // easydict://writeKeyValue?EZCaiyunToken= private var token: String { - let token = UserDefaults.standard.string(forKey: EZCaiyunToken) + let token = Defaults[.caiyunToken] if let token, !token.isEmpty { return token } else { diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m b/Easydict/Feature/Service/OpenAI/EZOpenAIService.m index ad01eff55..1123049e4 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m +++ b/Easydict/Feature/Service/OpenAI/EZOpenAIService.m @@ -28,6 +28,7 @@ @interface EZOpenAIService () @end + @implementation EZOpenAIService - (instancetype)init { diff --git a/Easydict/Feature/Service/Tencent/TencentService.swift b/Easydict/Feature/Service/Tencent/TencentService.swift index cc75b7db0..aaa07b9ab 100644 --- a/Easydict/Feature/Service/Tencent/TencentService.swift +++ b/Easydict/Feature/Service/Tencent/TencentService.swift @@ -7,6 +7,7 @@ // import Alamofire +import Defaults import Foundation @objc(EZTencentService) @@ -64,7 +65,7 @@ public final class TencentService: QueryService { // easydict://writeKeyValue?EZTencentSecretId=xxx private var secretId: String { - let secretId = UserDefaults.standard.string(forKey: EZTencentSecretId) + let secretId = Defaults[.tencentSecretId] if let secretId, !secretId.isEmpty { return secretId } else { @@ -74,7 +75,7 @@ public final class TencentService: QueryService { // easydict://writeKeyValue?EZTencentSecretKey=xxx private var secretKey: String { - let secretKey = UserDefaults.standard.string(forKey: EZTencentSecretKey) + let secretKey = Defaults[.tencentSecretKey] if let secretKey, !secretKey.isEmpty { return secretKey } else { diff --git a/Easydict/NewApp/Configuration/Configuration.swift b/Easydict/NewApp/Configuration/Configuration.swift index 976ab72f5..5777aa144 100644 --- a/Easydict/NewApp/Configuration/Configuration.swift +++ b/Easydict/NewApp/Configuration/Configuration.swift @@ -9,6 +9,7 @@ import Defaults import Foundation +// Setting extension Defaults.Keys { // rename `from` static let queryFromLanguage = Key("EZConfiguration_kFromKey", default: .auto) @@ -52,3 +53,37 @@ extension Defaults.Keys { static let appearanceType = Key("EZConfiguration_kApperanceKey", default: .followSystem) static let fontSizeOptionIndex = Key("EZConfiguration_kTranslationControllerFontKey", default: 0) } + +// Service Configuration +extension Defaults.Keys { + // OPENAI + static let openAIAPIKey = Key("EZOpenAIAPIKey") + static let openAITranslation = Key("EZOpenAITranslationKey") + static let openAIDictionary = Key("EZOpenAIDictionaryKey") + static let openAISentence = Key("EZOpenAISentenceKey") + static let openAIServiceUsageStatus = Key("EZOpenAIServiceUsageStatusKey") + static let openAIDomain = Key("EZOpenAIDomainKey") + static let openAIEndPoint = Key("EZOpenAIEndPointKey") + static let openAIModel = Key("EZOpenAIModelKey") + + // DEEPL + static let deepLAuth = Key("EZDeepLAuthKey") + static let deepLTranslateEndPointKey = Key("EZDeepLTranslateEndPointKey") + + // BING + static let bingCookieKey = Key("EZBingCookieKey") + + // niu + static let niuTransAPIKey = Key("EZNiuTransAPIKey") + + // Caiyun + static let caiyunToken = Key("EZCaiyunToken") + + // tencent + static let tencentSecretId = Key("EZTencentSecretId") + static let tencentSecretKey = Key("EZTencentSecretKey") + + // ALI + static let aliAccessKeyId = Key("EZAliAccessKeyId") + static let aliAccessKeySecret = Key("EZAliAccessKeySecret") +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift new file mode 100644 index 000000000..960cada25 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift @@ -0,0 +1,35 @@ +// +// OpenAIService+ConfigurableService.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/14. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 12.0, *) +extension EZOpenAIService: ConfigurableService { + func configurationListItems() -> some View { + ServiceStringConfigurationSection( + textFieldTitleKey: "service.configuration.openai.api_key.header", + headerTitleKey: "service.configuration.openai.api_key.title", + key: .openAIAPIKey, + prompt: "service.configuration.openai.api_key.prompt", + footer: { + Text("service.configuration.openai.api_key.footer") + } + ) + + ServiceStringConfigurationSection( + textFieldTitleKey: "service.configuration.openai.translation.header", + headerTitleKey: "service.configuration.openai.translation.title", + key: .openAITranslation, + prompt: "service.configuration.openai.translation.prompt", + footer: { + Text("service.configuration.openai.translation.footer") + } + ) + } +} diff --git a/Easydict/NewApp/Utility/Protocol/ConfigurableService.swift b/Easydict/NewApp/Utility/Protocol/ConfigurableService.swift new file mode 100644 index 000000000..058151a5c --- /dev/null +++ b/Easydict/NewApp/Utility/Protocol/ConfigurableService.swift @@ -0,0 +1,29 @@ +// +// ConfigurableService.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/14. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +/// A service can provide configuration view in setting +protocol ConfigurableService { + associatedtype T: View + + /// Items in Configuration Form. Use ServiceStringConfigurationSection or other customize view. + @ViewBuilder + func configurationListItems() -> T +} + +@available(macOS 13.0, *) +extension ConfigurableService { + func configurationView() -> some View { + Form { + configurationListItems() + } + .formStyle(.grouped) + } +} diff --git a/Easydict/NewApp/View/ServiceItemView.swift b/Easydict/NewApp/View/ServiceItemView.swift deleted file mode 100644 index d21f17e98..000000000 --- a/Easydict/NewApp/View/ServiceItemView.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// ServiceItemView.swift -// Easydict -// -// Created by phlpsong on 2024/1/6. -// Copyright © 2024 izual. All rights reserved. -// - -import SwiftUI - -@available(macOS 13.0, *) -struct ServiceItemView: View { - @Binding var service: QueryService - - var toggleValueChanged: (Bool) -> Void - - var body: some View { - HStack { - Image(nsImage: NSImage(named: service.serviceType().rawValue) ?? NSImage()) - .resizable() - .frame(maxWidth: 18.0, maxHeight: 18.0) - - Text(service.name()) - - Toggle(isOn: $service.enabled.didSet(execute: { value in - toggleValueChanged(value) - })) {} - .toggleStyle(.switch) - .controlSize(.small) - } - .padding(4.0) - } -} - -@available(macOS 13, *) -#Preview { - let service = EZLocalStorage.shared().allServices(.mini).first ?? QueryService() - return ServiceItemView(service: .constant(service)) { val in - print("toggle value changed: \(val)") - } -} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index 8c4fec008..a7e27c991 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -17,41 +17,48 @@ enum SettingTab: Int { @available(macOS 13, *) struct SettingView: View { - @State private var selection = SettingTab.general.rawValue + @State private var selection = SettingTab.general @State private var window: NSWindow? var body: some View { - TabView(selection: $selection.didSet(execute: { _ in - resizeWindowFrame() - })) { + TabView(selection: $selection) { GeneralTab() .tabItem { Label("setting_general", systemImage: "gear") } - .tag(SettingTab.general.rawValue) + .tag(SettingTab.general) ServiceTab() .tabItem { Label("service", systemImage: "briefcase") } - .tag(SettingTab.service.rawValue) + .tag(SettingTab.service) PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } - .tag(SettingTab.privacy.rawValue) + .tag(SettingTab.privacy) AboutTab() .tabItem { Label("about", systemImage: "info.bubble") } - .tag(SettingTab.about.rawValue) + .tag(SettingTab.about) } - .background(WindowAccessor(window: $window.didSet(execute: { _ in - // reset frame when first launch + .background( + WindowAccessor(window: $window.didSet(execute: { _ in + // reset frame when first launch + resizeWindowFrame() + })) + ) + .onChange(of: selection) { _ in resizeWindowFrame() - }))) + } } func resizeWindowFrame() { guard let window else { return } let originalFrame = window.frame - let newSize = selection == SettingTab.service.rawValue - ? CGSize(width: 360, height: 520) : CGSize(width: 500, height: 400) + let newSize = switch selection { + case .general, .privacy, .about: + CGSize(width: 500, height: 520) + case .service: + CGSize(width: 800, height: 520) + } let newY = originalFrame.origin.y + originalFrame.size.height - newSize.height let newRect = NSRect(origin: CGPoint(x: originalFrame.origin.x, y: newY), size: newSize) diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift new file mode 100644 index 000000000..42c59d0ee --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift @@ -0,0 +1,84 @@ +// +// ServiceConfigurationSection.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/14. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import SwiftUI + +@available(macOS 12.0, *) +struct ServiceStringConfigurationSection: View { + /// Title of text field + let textFieldTitleKey: LocalizedStringKey + /// Header of section. If there is no need to add an header, just leave empty string + let headerTitleKey: LocalizedStringKey + /// Defaults key for configuration. Please refer to `Configuration` - `Configuration` + let key: Defaults.Key + /// Prompt of text field + let prompt: LocalizedStringKey + /// Footer of section. Add comments, footnotes or links to describe the field. + @ViewBuilder let footer: () -> F + + var body: some View { + ServiceConfigurationSection( + headerTitleKey, + key: key, + field: { value in + let value = Binding.init { + value.wrappedValue ?? "" + } set: { newValue in + value.wrappedValue = newValue + } + TextField(textFieldTitleKey, text: value, prompt: Text(prompt)) + }, + footer: footer + ) + } +} + +@available(macOS 12.0, *) +struct ServiceConfigurationSection: View { + @Default var value: T + + init( + _ titleKey: LocalizedStringKey, + key: Defaults.Key, + @ViewBuilder field: @escaping (Binding) -> V, + footer: (() -> F)? + ) { + self.titleKey = titleKey + _value = .init(key) + self.footer = footer + self.field = field + } + + let field: (Binding) -> V + let footer: (() -> F)? + + let titleKey: LocalizedStringKey + + var body: some View { + Section { + field($value) + } header: { + HStack(alignment: .lastTextBaseline) { + Text(titleKey) + Spacer() + Button("service.service_configuration.reset") { + _value.reset() + } + .buttonStyle(.plain) + .foregroundStyle(Color.accentColor) + .font(.footnote) + } + } footer: { + if let footer { + footer() + .font(.footnote) + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift index 3da7ca423..5399f9af7 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift @@ -6,141 +6,203 @@ // Copyright © 2024 izual. All rights reserved. // +import Combine import SwiftUI @available(macOS 13, *) struct ServiceTab: View { - @State private var windowTypeValue = EZWindowType.mini.rawValue - @State private var serviceTypes: [ServiceType] = [] - @State private var services: [QueryService] = [] - @State private var selectedIndex: Int? - // workaround for tap gesture conflict with onMove - @State private var isNeedTapHandler = true - - var segmentCtrl: some View { - Picker("", selection: $windowTypeValue) { - Text("mini_window") - .tag(EZWindowType.mini.rawValue) - - Text("fixed_window") - .tag(EZWindowType.fixed.rawValue) - - Text("main_window") - .tag(EZWindowType.main.rawValue) - } - .padding() - .pickerStyle(.segmented) - .onChange(of: windowTypeValue) { type in - loadService(type: type) - selectedIndex = nil - } + @StateObject private var viewModel: ServiceTabViewModel = .init() + + @Environment(\.colorScheme) private var colorScheme + + var bgColor: Color { + Color(nsColor: colorScheme == .light ? .windowBackgroundColor : .controlBackgroundColor) } - var serviceList: some View { - List { - ForEach(Array(zip(serviceTypes.indices, serviceTypes)), id: \.0) { index, _ in - ServiceItemView( - service: $services[index] - ) { isEnable in - serviceToggled(index: index, isEnable: isEnable) - selectedIndex = nil - isNeedTapHandler = false + var tableColor: Color { + Color(nsColor: colorScheme == .light ? .ez_tableRowViewBgLight() : .ez_tableRowViewBgDark()) + } + + var body: some View { + HStack { + VStack { + WindowTypePicker(windowType: $viewModel.windowType) + .padding() + List { + ServiceItems() } - .frame(height: 30) - .tag(index) - .listRowBackground(selectedIndex == index ? Color("service_cell_highlight") : Color.clear) - .overlay(TapHandler(tapAction: { - if !isNeedTapHandler { - isNeedTapHandler.toggle() - return - } - if selectedIndex == nil || selectedIndex != index { - selectedIndex = index + .scrollContentBackground(.hidden) + .listStyle(.plain) + .scrollIndicators(.never) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .background(bgColor, in: RoundedRectangle(cornerRadius: 10)) + .padding(.bottom) + .padding(.horizontal) + } + .background(bgColor) + Group { + if let service = viewModel.selectedService { + // To provide configuration options for a service, follow these steps + // 1. If the Service is an object of Objc, expose it to Swift. + // 2. Create a new file in the Utility - Extensions - QueryService+ConfigurableService, + // 3. referring to OpenAIService+ConfigurableService, `extension` the Service as `ConfigurableService` to provide the configuration items. + if let service = service as? (any ConfigurableService) { + AnyView(service.configurationView()) } else { - selectedIndex = nil + HStack { + Spacer() + // No configuration for service xxx + Text("setting.service.detail.no_configuration \(service.name())") + Spacer() + } + } + } else { + HStack { + Spacer() + Text("setting.service.detail.no_selection") + Spacer() } - })) + } } - .onMove(perform: { indices, newOffset in - onServiceItemMove(fromOffsets: indices, toOffset: newOffset) - selectedIndex = nil - }) - .listRowSeparator(.hidden) + .layoutPriority(1) } - .scrollIndicators(.hidden) - .listStyle(.plain) - .clipShape(RoundedRectangle(cornerRadius: 8.0)) - .padding([.horizontal, .bottom]) + .environmentObject(viewModel) } +} - var body: some View { - VStack { - segmentCtrl - - serviceList - } - .onAppear { - loadService(type: windowTypeValue) +private class ServiceTabViewModel: ObservableObject { + @Published var windowType = EZWindowType.mini { + didSet { + if oldValue != windowType { + updateServices() + selectedService = nil + } } } - func loadService(type: Int) { - let windowType = EZWindowType(rawValue: type) ?? .none - services = EZLocalStorage.shared().allServices(windowType) - serviceTypes = services.compactMap { $0.serviceType() } - } + @Published var selectedService: QueryService? - func serviceToggled(index: Int, isEnable: Bool) { - let service = services[index] - service.enabled = isEnable - if isEnable { - service.enabledQuery = true - } - let windowType = EZWindowType(rawValue: windowTypeValue) ?? .none - EZLocalStorage.shared().setService(services[index], windowType: windowType) - // refresh service list - loadService(type: windowTypeValue) - postUpdateServiceNotification() + @Published private(set) var services: [QueryService] = EZLocalStorage.shared().allServices(.mini) + + func updateServices() { + services = getServices() } - func enabledServices(in services: [QueryService]) -> [QueryService] { - services.filter(\.enabled) + func getServices() -> [QueryService] { + EZLocalStorage.shared().allServices(windowType) } func onServiceItemMove(fromOffsets: IndexSet, toOffset: Int) { - let oldEnabledServices = enabledServices(in: services) + var services = services services.move(fromOffsets: fromOffsets, toOffset: toOffset) - serviceTypes.move(fromOffsets: fromOffsets, toOffset: toOffset) - let windowType = EZWindowType(rawValue: windowTypeValue) ?? .none + let serviceTypes = services.map { service in + service.serviceType() + } + EZLocalStorage.shared().setAllServiceTypes(serviceTypes, windowType: windowType) - let newServices = EZLocalStorage.shared().allServices(windowType) - let newEnabledServices = enabledServices(in: newServices) - // post notification after enabled services order changed - if isEnabledServicesOrderChanged(source: oldEnabledServices, dest: newEnabledServices) { - postUpdateServiceNotification() - } - } + postUpdateServiceNotification() - func isEnabledServicesOrderChanged( - source: [QueryService], - dest: [QueryService] - ) -> Bool { - !source.elementsEqual(dest) { sItem, dItem in - sItem.serviceType() == dItem.serviceType() && sItem.name() == dItem.name() - } + updateServices() } func postUpdateServiceNotification() { - let userInfo: [String: Any] = [EZWindowTypeKey: windowTypeValue] + let userInfo: [String: Any] = [EZWindowTypeKey: windowType.rawValue] let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo) NotificationCenter.default.post(notification) } } +@available(macOS 13.0, *) +private struct ServiceItems: View { + @EnvironmentObject private var viewModel: ServiceTabViewModel + + private var servicesWithID: [(QueryService, String)] { + viewModel.services.map { service in + (service, service.name()) + } + } + + var body: some View { + ForEach(servicesWithID, id: \.1) { service, _ in + ServiceItemView(service: service) + .tag(service) + } + .onMove(perform: viewModel.onServiceItemMove) + } +} + +@available(macOS 13.0, *) +private struct ServiceItemView: View { + let service: QueryService + + @EnvironmentObject private var viewModel: ServiceTabViewModel + + private var enabled: Binding { + .init { + service.enabled + } set: { newValue in + guard service.enabled != newValue else { return } + service.enabled = newValue + if newValue { + service.enabledQuery = newValue + } + EZLocalStorage.shared().setService(service, windowType: viewModel.windowType) + viewModel.postUpdateServiceNotification() + } + } + + var body: some View { + Toggle(isOn: enabled) { + HStack { + Image(service.serviceType().rawValue) + .resizable() + .scaledToFit() + .frame(width: 20.0, height: 20.0) + Text(service.name()) + .lineLimit(1) + .fixedSize() + } + } + .padding(4.0) + .toggleStyle(.switch) + .controlSize(.small) + .listRowSeparator(.hidden) + .listRowInsets(.init()) + .padding(10) + .listRowBackground(viewModel.selectedService == service ? Color("service_cell_highlight") : tableColor) + .overlay { + TapHandler { + viewModel.selectedService = service + } + } + } + + @Environment(\.colorScheme) private var colorScheme + + private var tableColor: Color { + Color(nsColor: colorScheme == .light ? .ez_tableRowViewBgLight() : .ez_tableRowViewBgDark()) + } +} + @available(macOS 13, *) -#Preview { - ServiceTab() +private struct WindowTypePicker: View { + @Binding var windowType: EZWindowType + + var body: some View { + HStack { + Picker(selection: $windowType) { + ForEach([EZWindowType]([.mini, .fixed, .main]), id: \.rawValue) { windowType in + Text(windowType.localizedStringResource) + .tag(windowType) + } + } label: { + EmptyView() + } + .labelsHidden() + .pickerStyle(.segmented) + } + } }