From 9ccd9fd9e9f25390dded2497f33c475f1891864b Mon Sep 17 00:00:00 2001 From: Jerry Zhang Date: Sat, 6 Jan 2024 01:58:38 -0800 Subject: [PATCH 01/72] perf(UI): update copyright info (#309) * perf(UI): update copyright info * perf(UI): align copyright info with Finder --- Easydict/App/Info-debug.plist | 2 +- Easydict/App/Info.plist | 2 +- EasydictHelper/Info.plist | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Easydict/App/Info-debug.plist b/Easydict/App/Info-debug.plist index defc09926..d94aeb894 100644 --- a/Easydict/App/Info-debug.plist +++ b/Easydict/App/Info-debug.plist @@ -51,7 +51,7 @@ NSHumanReadableCopyright - Copyright © 2022 tisfeng. All rights reserved. + Copyright © 2023-2024 tisfeng. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/Easydict/App/Info.plist b/Easydict/App/Info.plist index 6e735f833..99a9b1c94 100644 --- a/Easydict/App/Info.plist +++ b/Easydict/App/Info.plist @@ -51,7 +51,7 @@ NSHumanReadableCopyright - Copyright © 2022 tisfeng. All rights reserved. + Copyright © 2023-2024 tisfeng. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass diff --git a/EasydictHelper/Info.plist b/EasydictHelper/Info.plist index d8b9f2dde..9a30b4d8d 100644 --- a/EasydictHelper/Info.plist +++ b/EasydictHelper/Info.plist @@ -27,7 +27,7 @@ LSUIElement NSHumanReadableCopyright - Copyright © 2022 tisfeng. All rights reserved. + Copyright © 2023-2024 tisfeng. All rights reserved. NSMainStoryboardFile Main NSPrincipalClass From fca8c65533bc501aa6bfcee53f7c4c20cf955c01 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sun, 7 Jan 2024 09:50:15 +0800 Subject: [PATCH 02/72] fix: xcode cannot use breakpoint set GCC_GENERATE_DEBUGGING_SYMBOLS = YES https://stackoverflow.com/a/75668788/8378840 --- Easydict.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 00aad8201..56aeb6d3f 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -2754,7 +2754,7 @@ DEVELOPMENT_TEAM = 79NQA2XYHM; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.1; MARKETING_VERSION = 1.0; @@ -2782,7 +2782,7 @@ DEVELOPMENT_TEAM = 79NQA2XYHM; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.1; MARKETING_VERSION = 1.0; @@ -2809,7 +2809,7 @@ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; @@ -2834,7 +2834,7 @@ DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 11.0; @@ -2859,7 +2859,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; INFOPLIST_FILE = EasydictHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -2885,7 +2885,7 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; INFOPLIST_FILE = EasydictHelper/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3044,7 +3044,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_TESTING_SEARCH_PATHS = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; INFOPLIST_FILE = "$(TARGET_NAME)/App/Info-debug.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -3084,7 +3084,7 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_TESTING_SEARCH_PATHS = NO; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; - GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_PREFIX_HEADER = "$(SRCROOT)/Easydict/App/PrefixHeader.pch"; INFOPLIST_FILE = "$(TARGET_NAME)/App/Info.plist"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; From 261b625f3d770365ff1fa0ddabe6a030b0592d08 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sun, 7 Jan 2024 10:19:58 +0800 Subject: [PATCH 03/72] docs: update README --- README.md | 2 +- README_EN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 604284bd4..ce11f5f41 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ 感谢 [BingoKingo](https://github.com/tisfeng/Easydict/issues/1#issuecomment-1445286763) 提供的最初安装版本。 ```bash -brew install easydict +brew install --cask easydict ``` ### 开发者构建 diff --git a/README_EN.md b/README_EN.md index 1a14e34be..75f213731 100644 --- a/README_EN.md +++ b/README_EN.md @@ -117,7 +117,7 @@ You can install it using one of the following two methods. Support macOS 11.0+ Thanks to [BingoKingo](https://github.com/tisfeng/Easydict/issues/1#issuecomment-1445286763) for the initial installation version. ```bash -brew install easydict +brew install --cask easydict ``` ### Developer Build From b336e30b30717576570f3cd8a8886f10776406cb Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sun, 7 Jan 2024 17:13:04 +0800 Subject: [PATCH 04/72] perf: update Localizable.xcstrings --- Easydict/App/Localizable.xcstrings | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 0ee5bae52..160e6409b 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -884,10 +884,24 @@ } }, "Easydict" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "Easydict 🍃" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "enable_beta_new_app" : { "localizations" : { @@ -1420,7 +1434,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "请前往服务官网注册申请个人的 API key。" } } @@ -2440,7 +2454,14 @@ } }, "Tisfeng" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "toggle_languages" : { "localizations" : { From 9433ee31b14e0a9f6e3d6a351e60bf0d57be5156 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:48:58 +0800 Subject: [PATCH 05/72] bugfix: remove increaseMenuItemHeight func (#318) --- .../Feature/StatusItem/EZMenuItemManager.m | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/Easydict/Feature/StatusItem/EZMenuItemManager.m b/Easydict/Feature/StatusItem/EZMenuItemManager.m index ca784ad32..7f87b5103 100644 --- a/Easydict/Feature/StatusItem/EZMenuItemManager.m +++ b/Easydict/Feature/StatusItem/EZMenuItemManager.m @@ -98,7 +98,6 @@ - (void)setup { self.versionItem.title = self.versionTitle; NSArray *items = @[self.versionItem, self.settingsItem, self.checkForUpdateItem, self.helpItem, self.quitItem]; - [self increaseMenuItemsHeight:items lineHeightRatio:kTitleMenuItemHeightRatio]; [self updateVersionItem]; } @@ -308,8 +307,6 @@ - (void)menuWillOpen:(NSMenu *)menu { item.keyEquivalent = @""; item.keyEquivalentModifierMask = 0; } - - [self increaseMenuItemHeight:item lineHeightRatio:kImageMenuItemHeightRatio]; }; configItemShortcut(self.selectionItem, EZSelectionShortcutKey); @@ -319,24 +316,6 @@ - (void)menuWillOpen:(NSMenu *)menu { configItemShortcut(self.screenshotOCRItem, EZScreenshotOCRShortcutKey); } -#pragma mark - - -/// Increase menu item height. Ref: https://stackoverflow.com/questions/18031666/nsmenuitem-height -- (void)increaseMenuItemHeight:(NSMenuItem *)item lineHeightRatio:(CGFloat)lineHeightRatio { - NSFont *font = [NSFont systemFontOfSize:[NSFont systemFontSize]]; - CGFloat fontLineHeight = (font.ascender + fabs(font.descender)); - CGFloat lineHeight = fontLineHeight * lineHeightRatio; - // Ref stackoverflow: https://stackoverflow.com/a/18034142/8378840 - NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(1, lineHeight)]; - [item setImage:image]; -} - -- (void)increaseMenuItemsHeight:(NSArray *)itmes lineHeightRatio:(CGFloat)lineHeightRatio { - for (NSMenuItem *item in itmes) { - [self increaseMenuItemHeight:item lineHeightRatio:lineHeightRatio]; - } -} - #pragma mark - Fetch Github Repo Info - (void)updateVersionItem { @@ -347,7 +326,6 @@ - (void)updateVersionItem { versionTitle = [NSString stringWithFormat:@"%@ (✨ %@)", self.versionTitle, lastestVersion]; } self.versionItem.title = versionTitle; - [self increaseMenuItemHeight:self.versionItem lineHeightRatio:kTitleMenuItemHeightRatio]; }]; } From 08d09df6b368e58019f0d73d3536e6530914d5bd Mon Sep 17 00:00:00 2001 From: tisfeng Date: Tue, 9 Jan 2024 12:46:29 +0800 Subject: [PATCH 06/72] docs: update sponsor list --- README.md | 1 + README_EN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 604284bd4..375d4e6ea 100644 --- a/README.md +++ b/README.md @@ -820,6 +820,7 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2023-12-05 | ㅤ—— | 20 | | | 2023-12-07 | 小逗。🎈 | 5 | | | 2023-12-26 | ㅤ Yee | 5 | 感谢开源 | +| 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! |

diff --git a/README_EN.md b/README_EN.md index 1a14e34be..727d367a0 100644 --- a/README_EN.md +++ b/README_EN.md @@ -817,6 +817,7 @@ If you don't want your username to be displayed in the list, please choose anony | 2023-12-05 | ㅤ—— | 20 | | | 2023-12-07 | 小逗。🎈 | 5 | | | 2023-12-26 | ㅤ Yee | 5 | 感谢开源 | +| 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! |

From d9d9ffe8ac29c55f80797350ee9851c01c7c7a03 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Thu, 11 Jan 2024 09:57:45 +0800 Subject: [PATCH 07/72] fix(crash): substringWithRange Range out of bounds, length < 0 --- .../Utility/EZCategory/NSString/NSString+EZUtils.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m b/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m index df765c4c5..5a119c5df 100644 --- a/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m +++ b/Easydict/Feature/Utility/EZCategory/NSString/NSString+EZUtils.m @@ -515,8 +515,14 @@ - (BOOL)isStartAndEndWith:(NSString *)start end:(NSString *)end { } - (NSString *)removeStartAndEndWith:(NSString *)start end:(NSString *)end { - if ([self isStartAndEndWith:start end:end]) { - return [self substringWithRange:NSMakeRange(start.length, self.length - start.length - end.length)]; + /** + Fix crash + + SIGABRT: -[NSTaggedPointerString substringWithRange:]: Range {2, 18446744073709551614} out of bounds; string length 2 + */ + NSInteger substringLength = self.length - start.length - end.length; + if ([self isStartAndEndWith:start end:end] && substringLength > 0) { + return [self substringWithRange:NSMakeRange(start.length, substringLength)]; } return self; } From 899e6d8a6e101b9c4374ed53dbd8d48bccdd3c20 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Mon, 8 Jan 2024 20:30:28 +0800 Subject: [PATCH 08/72] perf: remove unused variables --- Easydict/Feature/StatusItem/EZMenuItemManager.m | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Easydict/Feature/StatusItem/EZMenuItemManager.m b/Easydict/Feature/StatusItem/EZMenuItemManager.m index 7f87b5103..1d47cb1d6 100644 --- a/Easydict/Feature/StatusItem/EZMenuItemManager.m +++ b/Easydict/Feature/StatusItem/EZMenuItemManager.m @@ -16,9 +16,6 @@ #import "EZConfiguration.h" #import "Easydict-Swift.h" -static CGFloat const kImageMenuItemHeightRatio = 1.4; -static CGFloat const kTitleMenuItemHeightRatio = 1.2; - @interface EZMenuItemManager () @property (weak) IBOutlet NSMenu *menu; @@ -96,9 +93,7 @@ - (void)setup { self.appVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; self.versionItem.title = self.versionTitle; - - NSArray *items = @[self.versionItem, self.settingsItem, self.checkForUpdateItem, self.helpItem, self.quitItem]; - + [self updateVersionItem]; } From 70466d8af3bd590fcd893834908dab9f8806e7f7 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 12 Jan 2024 23:56:28 +0800 Subject: [PATCH 09/72] fix: add fallback privacy image on macOS 12- --- .../toolbar_privacy.imageset/Contents.json | 1 - .../setting/toolbar_privacy.imageset/Privacy.png | Bin 948 -> 0 bytes .../PerferenceWindow/EZPrivacyViewController.m | 12 +++++++----- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Privacy.png diff --git a/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json b/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json index d6c01d3c7..4d3912c0e 100644 --- a/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json +++ b/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Contents.json @@ -10,7 +10,6 @@ "scale" : "2x" }, { - "filename" : "Privacy.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Privacy.png b/Easydict/App/Assets.xcassets/setting/toolbar_privacy.imageset/Privacy.png deleted file mode 100644 index 12ccf9bf3d6329d4e61e0ea5f870148c184f3c54..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 948 zcmV;l155mgP)Px&Zb?KzRA@uxn>%vbFc5~p8zjAp=m_cD*-Gy&2gwZ*-5>`^r`}dNcR7Mmq)%?3 z%#YzxD+(a64~pQJGt%gR*T;Vsj|JF;8L$2E^cX_G3)cgyihet=-+uO=4^Nwp|L)qt z&b71-4g`=%`*mCps(5i-ZN^8z;k-vD%c1I*wm-~~Y4z{S8l?2D&g z8OjnXE&|>kPv}6agpsRr!g{2!`4P6DiKb~O1G>D0iP5V3|Yod_~Xl`vclXd zklMk&ub{7hJ=ARo_^K020h`oK`RRfRqv{KlFMvJNZ4mfEM=QSRqCh zfz?BjWLTob{CLfaGSEnZ5O<>gt`O#!P^zLg(qIxDX36|lvZ=T_N6-5hXpDl1`!o|(Hv?gT(H(AthF zceus

ZKOYn7ck$urBbB@gI1V3R&P9#$;#Y=GnVku;8V|KWVhNjS{e4yI9M7HBGi z&TQd3ZUVm$v5c?&ESC_Hk{4mC6lTz-$cPf6Hk Date: Sat, 13 Jan 2024 09:43:49 +0800 Subject: [PATCH 10/72] fix: crash SIGABRT: *** -[EZTextView replaceCharactersInRange:withString:]: nil NSString given --- Easydict/Feature/ViewController/View/TextView/EZTextView.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Easydict/Feature/ViewController/View/TextView/EZTextView.m b/Easydict/Feature/ViewController/View/TextView/EZTextView.m index ba8edbc24..fcc3dc36e 100644 --- a/Easydict/Feature/ViewController/View/TextView/EZTextView.m +++ b/Easydict/Feature/ViewController/View/TextView/EZTextView.m @@ -246,6 +246,11 @@ - (void)updatePlaceholderVisibility { } - (void)setString:(NSString *)string { + // Fix: SIGABRT: *** -[EZTextView replaceCharactersInRange:withString:]: nil NSString given. + if (!string) { + string = @""; + } + [super setString:string]; [self updatePlaceholderVisibility]; } From bec66dad6df7427a9abff7af1419644c73cfde62 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sun, 14 Jan 2024 11:48:51 +0800 Subject: [PATCH 11/72] chore: auto add .git for Alamofire --- Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4516ef259..bc7e5cc78 100644 --- a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -12,7 +12,7 @@ { "identity" : "alamofire", "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire", + "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", "version" : "5.8.1" From 35222c2e150bebe2fb200b7c0fbb5236e8229cb5 Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Sun, 14 Jan 2024 01:25:22 -0800 Subject: [PATCH 12/72] Introduce `Defaults` package into project and implement Setting - General with SwiftUI (#323) * import Defaults package * refactor EZConfiguration using Defaults package and Implement Boolean options in SwiftUI Setting View * complete enum options in `GeneralTab` of `Setting` * Font Size and Appearance setting * rename EZConfiguration file in swift to Configuration * rename `TTSService` with `TTSServiceType` * renaming TTSService to TTSServiceType and remove EZ prefix of some file * maintain the order of languages * filter classical Chinese in first/second language options * `Defaults ` package use version instead of branch --- Easydict.xcodeproj/project.pbxproj | 73 +++ .../xcshareddata/swiftpm/Package.resolved | 9 + Easydict/App/Localizable.xcstrings | 532 +++++++++++++++++- .../Feature/Configuration/Appearance.swift | 3 +- .../Feature/Configuration/EZConfiguration.h | 1 + .../Tencent/TencentTranslateType.swift | 14 - .../NewApp/Configuration/Configuration.swift | 55 ++ Easydict/NewApp/Model/TTSServiceType.swift | 80 +++ .../LanguageDetectOptimizeExtensions.swift | 32 ++ .../Extensions/LanguageExtensions.swift | 78 +++ .../ShowWindowPositionExtensions.swift | 34 ++ .../Extensions/WindowTypeExtensions.swift | 34 ++ .../View/SettingView/Tabs/GeneralTab.swift | 268 ++++++++- 13 files changed, 1176 insertions(+), 37 deletions(-) create mode 100644 Easydict/NewApp/Configuration/Configuration.swift create mode 100644 Easydict/NewApp/Model/TTSServiceType.swift create mode 100644 Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift create mode 100644 Easydict/NewApp/Utility/Extensions/LanguageExtensions.swift create mode 100644 Easydict/NewApp/Utility/Extensions/ShowWindowPositionExtensions.swift create mode 100644 Easydict/NewApp/Utility/Extensions/WindowTypeExtensions.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 56aeb6d3f..ec44cd1ab 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -260,6 +260,13 @@ DC3C643F2B187119008EEDD8 /* ChangeFontSizeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3C643E2B187119008EEDD8 /* ChangeFontSizeView.swift */; }; DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */; }; DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C882B3969510055EFFC /* Appearance.swift */; }; + EA3B81F92B5254AA004C0E8B /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration.swift */; }; + EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = EA3B81FB2B52555C004C0E8B /* Defaults */; }; + EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */; }; + EA9943E82B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */; }; + 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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -737,6 +744,12 @@ DC3C643E2B187119008EEDD8 /* ChangeFontSizeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeFontSizeView.swift; sourceTree = ""; }; DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeHintView.swift; sourceTree = ""; }; DC6D9C882B3969510055EFFC /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; + EA3B81F82B5254AA004C0E8B /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSServiceType.swift; sourceTree = ""; }; + EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageDetectOptimizeExtensions.swift; sourceTree = ""; }; + 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -778,6 +791,7 @@ 03A830922B4073E700112834 /* AppCenterCrashes in Frameworks */, 03022F1C2B35DEBA00B63209 /* Hue in Frameworks */, 03A830952B4076FC00112834 /* FirebaseAnalyticsSwift in Frameworks */, + EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */, 03CF27FE2B3DA7D500E19B57 /* Realm in Frameworks */, 03A830902B4073E700112834 /* AppCenterAnalytics in Frameworks */, 03B63ABF2A86967800E155ED /* CoreServices.framework in Frameworks */, @@ -1994,6 +2008,9 @@ 27FE98032B3DCA9F000AD654 /* NewApp */ = { isa = PBXGroup; children = ( + EA9943E12B534C2900EE7B97 /* Model */, + EA9943DD2B534BAE00EE7B97 /* Utility */, + EA3B81F72B52549B004C0E8B /* Configuration */, 27FE95262B3DC55F000AD654 /* EasydictApp.swift */, 27FE98042B3DCB09000AD654 /* NewAppManager.swift */, 27FE98062B3DD525000AD654 /* View */, @@ -2143,6 +2160,41 @@ path = ChangeFontSizeView; sourceTree = ""; }; + EA3B81F72B52549B004C0E8B /* Configuration */ = { + isa = PBXGroup; + children = ( + EA3B81F82B5254AA004C0E8B /* Configuration.swift */, + ); + path = Configuration; + sourceTree = ""; + }; + EA9943DD2B534BAE00EE7B97 /* Utility */ = { + isa = PBXGroup; + children = ( + EA9943E62B534D7C00EE7B97 /* Extensions */, + ); + path = Utility; + sourceTree = ""; + }; + EA9943E12B534C2900EE7B97 /* Model */ = { + isa = PBXGroup; + children = ( + EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */, + ); + path = Model; + sourceTree = ""; + }; + EA9943E62B534D7C00EE7B97 /* Extensions */ = { + isa = PBXGroup; + children = ( + EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */, + EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */, + EA9943EF2B5354C400EE7B97 /* ShowWindowPositionExtensions.swift */, + EA9943F12B5358BF00EE7B97 /* LanguageExtensions.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2234,6 +2286,7 @@ 038030962B4106800009230C /* CocoaLumberjackSwift */, 038EA1A92B41169C008A6DD1 /* ZipArchive */, 038EA1AC2B41282F008A6DD1 /* MJExtension */, + EA3B81FB2B52555C004C0E8B /* Defaults */, ); productName = Bob; productReference = C99EEB182385796700FEE666 /* Easydict-debug.app */; @@ -2292,6 +2345,7 @@ 038030932B4106800009230C /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, 038EA1A82B41169C008A6DD1 /* XCRemoteSwiftPackageReference "ZipArchive" */, 038EA1AB2B41282F008A6DD1 /* XCRemoteSwiftPackageReference "MJExtension" */, + EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */, ); productRefGroup = C99EEB192385796700FEE666 /* Products */; projectDirPath = ""; @@ -2493,6 +2547,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + EA9943EE2B5353AB00EE7B97 /* WindowTypeExtensions.swift in Sources */, 030570E22ADB919900C9905E /* EZAppleScriptManager.m in Sources */, 03991158292927E000E1B06D /* EZTitlebar.m in Sources */, 03D8A65C2A433B4100D9A968 /* EZConfiguration+EZUserData.m in Sources */, @@ -2591,6 +2646,7 @@ 03B0233229231FA6001C7E63 /* MMLog.swift in Sources */, 03DC7C5E2A3ABE28000BF7C9 /* EZConstKey.m in Sources */, 62E2BF4C2B4082BA00E42D38 /* AliTranslateType.swift in Sources */, + EA3B81F92B5254AA004C0E8B /* Configuration.swift in Sources */, 03E3E7C22ADE318800812C84 /* EZQueryMenuTextView.m in Sources */, 03B0231829231FA6001C7E63 /* SnipWindowController.m in Sources */, 03542A342936F70F00C34C33 /* EZLanguageManager.m in Sources */, @@ -2627,6 +2683,7 @@ 0333FDA62A035D5700891515 /* NSString+EZChineseText.m in Sources */, 03E02A222924E77100A10260 /* EZMenuItemManager.m in Sources */, 039D119929D5E26300C93F46 /* EZAudioUtils.m in Sources */, + EA9943F22B5358BF00EE7B97 /* LanguageExtensions.swift in Sources */, 03B0231729231FA6001C7E63 /* Snip.m in Sources */, 03BFFC6E295FE59C004E033E /* EZQueryResult+EZYoudaoDictModel.m in Sources */, DC3C643F2B187119008EEDD8 /* ChangeFontSizeView.swift in Sources */, @@ -2644,10 +2701,12 @@ 0320C5872B29F35700861B3D /* QueryServiceRecord.swift in Sources */, 03FC699A2B39D13A0035D2DA /* EZOpenAIChatResponse.m in Sources */, 03B022FA29231FA6001C7E63 /* EZServiceTypes.m in Sources */, + EA9943F02B5354C400EE7B97 /* ShowWindowPositionExtensions.swift in Sources */, 03B0233129231FA6001C7E63 /* MMCrash.m in Sources */, 03B0232629231FA6001C7E63 /* NSAttributedString+MM.m in Sources */, 03542A402937B3C900C34C33 /* EZOCRResult.m in Sources */, C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */, + EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */, 036A0DBB2AD941F9006E6D4F /* EZReplaceTextButton.m in Sources */, 03DC7C662A3CA465000BF7C9 /* HWSegmentedControl.m in Sources */, 037E006D2B3DC098006491C6 /* EZOpenAIService+EZPromptMessages.m in Sources */, @@ -2683,6 +2742,7 @@ DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */, 03B0232429231FA6001C7E63 /* NSUserDefaults+MM.m in Sources */, 03542A5E2938F05B00C34C33 /* EZLanguageModel.m in Sources */, + EA9943E82B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift in Sources */, 03F639952AA6CFBB009B9914 /* EZBingConfig.m in Sources */, 03D2A3E329F4C6F50035CED4 /* EZNetworkManager.m in Sources */, 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */, @@ -3251,6 +3311,14 @@ minimumVersion = 5.8.1; }; }; + EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Defaults.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.3.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3329,6 +3397,11 @@ package = 2721E4CE2AFE920700A059AC /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; + EA3B81FB2B52555C004C0E8B /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C99EEB102385796700FEE666 /* Project object */; diff --git a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved index bc7e5cc78..9c63ea7f5 100644 --- a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -54,6 +54,15 @@ "version" : "1.8.0" } }, + { + "identity" : "defaults", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/Defaults.git", + "state" : { + "revision" : "3efef5a28ebdbbe922d4a2049493733ed14475a6", + "version" : "7.3.1" + } + }, { "identity" : "firebase-ios-sdk", "kind" : "remoteSourceControl", diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 160e6409b..aa35e533b 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -845,7 +845,7 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "划词内容为空时生效" + "value" : "划词内容为空时禁用复制提示音" } } } @@ -907,7 +907,7 @@ "localizations" : { "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "以 SwiftUI App模式启动,重启App生效" } } @@ -1675,6 +1675,9 @@ } } } + }, + "none_window" : { + }, "ocr_result_is_empty" : { "localizations" : { @@ -2173,6 +2176,528 @@ } } }, + "setting.general.advance.default_tts_service" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Default TTS Service" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认 TTS 服务" + } + } + } + }, + "setting.general.advance.enable_beta_feature" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable Beta features" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用 Beta 特性" + } + } + } + }, + "setting.general.advance.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advance" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高级" + } + } + } + }, + "setting.general.appearance.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appearance" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "外观" + } + } + } + }, + "setting.general.appearance.light_dark_appearance" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Appearance" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "外观" + } + } + } + }, + "setting.general.auto_copy.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto Copy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动复制" + } + } + } + }, + "setting.general.auto_query.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto Query" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动查询" + } + } + } + }, + "setting.general.font.font_size.label" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Font Size" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字体大小" + } + } + } + }, + "setting.general.font.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Font" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "字体" + } + } + } + }, + "setting.general.input.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Input Field" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入框" + } + } + } + }, + "setting.general.language.duplicated_alert %@%@%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The first language should not be the same as the second language (%1$@). The %2$@ was replaced with %3$@." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一语言不能与第二语言相同(%1$@)。%2$@已被替换为%3$@。" + } + } + } + }, + "setting.general.language.duplicated_alert.field.first" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "first language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第二语言" + } + } + } + }, + "setting.general.language.duplicated_alert.field.second" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "second language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一语言" + } + } + } + }, + "setting.general.language.duplicated_alert.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language Duplicated" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语言重复" + } + } + } + }, + "setting.general.language.first_language" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "First Language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第一语言" + } + } + } + }, + "setting.general.language.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语言" + } + } + } + }, + "setting.general.language.language_detect_optimize" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Language Detection" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "语种识别" + } + } + } + }, + "setting.general.language.second_language" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Second Language" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "第二语言" + } + } + } + }, + "setting.general.mouse_query.adjust_pop_button_origin" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adjust Query Icon Position to avoid conflict with PopClip" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "调整查询图标位置(避免和 PopClip 显示冲突)" + } + } + } + }, + "setting.general.mouse_query.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Query with Mouse" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠标查询" + } + } + } + }, + "setting.general.quick_link.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quick Link" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷功能" + } + } + } + }, + "setting.general.voice.auto_play_word_audio" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查询英语单词后自动播放发音" + } + } + } + }, + "setting.general.voice.disable_empty_copy_beep_msg" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Disable 'beep' when selected text is empty" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "划词内容为空时" + } + } + } + }, + "setting.general.voice.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sound" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "声音" + } + } + } + }, + "setting.general.window.fixed_window_position" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Floating Window Position" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "侧悬浮窗口位置" + } + } + } + }, + "setting.general.window.mouse_select_translate_window_type" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mouse Window Type" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "鼠标划词窗口类型" + } + } + } + }, + "setting.general.window.shortcut_select_translate_window_type" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut Window Type" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷键划词窗口类型" + } + } + } + }, + "setting.general.windows.header" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Window" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "窗口" + } + } + } + }, + "setting.tts_service.options.apple" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "苹果" + } + } + } + }, + "setting.tts_service.options.baidu" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Baidu" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "百度" + } + } + } + }, + "setting.tts_service.options.bing" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bing" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bing" + } + } + } + }, + "setting.tts_service.options.google" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Google" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Google" + } + } + } + }, + "setting.tts_service.options.youdao" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Youdao" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "有道" + } + } + } + }, "Settings..." : { "localizations" : { "zh-Hans" : { @@ -2516,6 +3041,9 @@ } } } + }, + "unknown_option" : { + }, "unpin" : { "localizations" : { diff --git a/Easydict/Feature/Configuration/Appearance.swift b/Easydict/Feature/Configuration/Appearance.swift index 3a59f1124..eb49593cc 100644 --- a/Easydict/Feature/Configuration/Appearance.swift +++ b/Easydict/Feature/Configuration/Appearance.swift @@ -6,9 +6,10 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import Foundation -@objc enum AppearenceType: Int, CaseIterable { +@objc enum AppearenceType: Int, CaseIterable, Defaults.Serializable { case followSystem = 0 case light case dark diff --git a/Easydict/Feature/Configuration/EZConfiguration.h b/Easydict/Feature/Configuration/EZConfiguration.h index bc07911ee..310f47031 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.h +++ b/Easydict/Feature/Configuration/EZConfiguration.h @@ -23,6 +23,7 @@ static NSString *const EZFontSizeUpdateNotification = @"EZFontSizeUpdateNotifica static NSString *const EZIntelligentQueryModeKey = @"IntelligentQueryMode"; + typedef NS_ENUM(NSUInteger, EZLanguageDetectOptimize) { EZLanguageDetectOptimizeNone = 0, EZLanguageDetectOptimizeBaidu = 1, diff --git a/Easydict/Feature/Service/Tencent/TencentTranslateType.swift b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift index e82753154..84ac8b712 100644 --- a/Easydict/Feature/Service/Tencent/TencentTranslateType.swift +++ b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift @@ -75,17 +75,3 @@ struct TencentTranslateType: Equatable { return TencentTranslateType(sourceLanguage: fromLanguage, targetLanguage: toLanguage) } } - -extension [Language] { - /// Contains Chinese language, - func containsChinese() -> Bool { - contains { $0.isKindOfChinese() } - } -} - -extension Language { - /// Is kind of Chinese language, means it is simplifiedChinese or traditionalChinese. - func isKindOfChinese() -> Bool { - self == .simplifiedChinese || self == .traditionalChinese - } -} diff --git a/Easydict/NewApp/Configuration/Configuration.swift b/Easydict/NewApp/Configuration/Configuration.swift new file mode 100644 index 000000000..9ecdff830 --- /dev/null +++ b/Easydict/NewApp/Configuration/Configuration.swift @@ -0,0 +1,55 @@ +// +// Configuration.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/12. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension Defaults.Keys { + // rename `from` + static let queryFromLanguage = Key("EZConfiguration_kFromKey", default: .auto) + // rename `to` + static let queryToLanguage = Key("EZConfiguration_kToKey", default: .auto) + + static let firstLanguage = Key("EZConfiguration_kFirstLanguageKey", default: EZLanguageManager.shared().systemPreferredTwoLanguages[0]) + static let secondLanguage = Key("EZConfiguration_kSecondLanguageKey", default: EZLanguageManager.shared().systemPreferredTwoLanguages[1]) + + static let autoSelectText = Key("EZConfiguration_kAutoSelectTextKey", default: true) + static let forceAutoGetSelectedText = Key("EZConfiguration_kForceAutoGetSelectedText", default: false) + + static let disableEmptyCopyBeep = Key("EZConfiguration_kDisableEmptyCopyBeepKey", default: true) + static let clickQuery = Key("EZConfiguration_kClickQueryKey", default: false) + static let autoPlayAudio = Key("EZConfiguration_kAutoPlayAudioKey", default: true) + static let launchAtStartup = Key("EZConfiguration_kLaunchAtStartupKey", default: false) + static let hideMainWindow = Key("EZConfiguration_kHideMainWindowKey", default: true) + static let autoQueryOCRText = Key("EZConfiguration_kAutoQueryOCTTextKey", default: true) + static let autoQuerySelectedText = Key("EZConfiguration_kAutoQuerySelectedTextKey", default: true) + static let autoQueryPastedText = Key("EZConfiguration_kAutoQueryPastedTextKey", default: false) + static let autoCopyOCRText = Key("EZConfiguration_kAutoCopyOCRTextKey", default: false) + static let autoCopySelectedText = Key("EZConfiguration_kAutoCopySelectedTextKey", default: false) + static let autoCopyFirstTranslatedText = Key("EZConfiguration_kAutoCopyFirstTranslatedTextKey", default: false) + static let languageDetectOptimize = Key("EZConfiguration_kLanguageDetectOptimizeTypeKey", default: EZLanguageDetectOptimize.none) + @available(macOS 13, *) + static let defaultTTSServiceType = Key("EZConfiguration_kDefaultTTSServiceTypeKey", default: TTSServiceType.youdao) + static let showGoogleQuickLink = Key("EZConfiguration_kShowGoogleLinkKey", default: true) + static let showEudicQuickLink = Key("EZConfiguration_kShowEudicLinkKey", default: true) + static let showAppleDictionaryQuickLink = Key("EZConfiguration_kShowAppleDictionaryLinkKey", default: true) + static let hideMenuBarIcon = Key("EZConfiguration_kHideMenuBarIconKey", default: false) + static let fixedWindowPosition = Key("EZConfiguration_kShowFixedWindowPositionKey", default: .right) + static let mouseSelectTranslateWindowType = Key("EZConfiguration_kMouseSelectTranslateWindowTypeKey", default: .mini) + static let shortcutSelectTranslateWindowType = Key("EZConfiguration_kShortcutSelectTranslateWindowTypeKey", default: .fixed) + static let adjustPopButtonOrigin = Key("EZConfiguration_kAdjustPopButtomOriginKey", default: false) + static let allowCrashLog = Key("EZConfiguration_kAllowCrashLogKey", default: true) + static let allowAnalytics = Key("EZConfiguration_kAllowAnalyticsKey", default: true) + static let clearInput = Key("EZConfiguration_kClearInputKey", default: true) + static let enableBetaNewApp = Key("EZConfiguration_kEnableBetaNewAppKey", default: false) + + static let enableBetaFeature = Key("EZBetaFeatureKey", default: false) + + static let appearanceType = Key("EZConfiguration_kApperanceKey", default: .followSystem) + static let fontSizeOptionIndex = Key("EZConfiguration_kTranslationControllerFontKey", default: 0) +} diff --git a/Easydict/NewApp/Model/TTSServiceType.swift b/Easydict/NewApp/Model/TTSServiceType.swift new file mode 100644 index 000000000..6eda5cf16 --- /dev/null +++ b/Easydict/NewApp/Model/TTSServiceType.swift @@ -0,0 +1,80 @@ +// +// TTSServiceType.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +@available(macOS 13, *) +enum TTSServiceType: String, CaseIterable, CustomLocalizedStringResourceConvertible { + var localizedStringResource: LocalizedStringResource { + switch self { + case .youdao: + "setting.tts_service.options.youdao" + case .bing: + "setting.tts_service.options.bing" + case .google: + "setting.tts_service.options.google" + case .baidu: + "setting.tts_service.options.baidu" + case .apple: + "setting.tts_service.options.apple" + } + } + + case youdao + case bing + case google + case baidu + case apple +} + +@available(macOS 13, *) +extension TTSServiceType: Defaults.Serializable { + // while in the future, ServiceType was deleted, then you can safely delete this struct and `bridge` + struct TTSServiceTypeBridge: Defaults.Bridge { + func serialize(_ value: TTSServiceType?) -> String? { + guard let value else { return nil } + switch value { + case .youdao: + return ServiceType.youdao.rawValue + case .bing: + return ServiceType.bing.rawValue + case .google: + return ServiceType.google.rawValue + case .baidu: + return ServiceType.baidu.rawValue + case .apple: + return ServiceType.apple.rawValue + } + } + + func deserialize(_ object: String?) -> TTSServiceType? { + guard let object else { return nil } + switch object { + case "Youdao": + return .youdao + case "Bing": + return .bing + case "Google": + return .google + case "Baidu": + return .baidu + case "Apple": + return .apple + default: + return nil + } + } + + typealias Value = TTSServiceType + + typealias Serializable = String + } + + static let bridge = TTSServiceTypeBridge() +} diff --git a/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift b/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift new file mode 100644 index 000000000..82ec281a1 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift @@ -0,0 +1,32 @@ +// +// LanguageDetectOptimizeExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension EZLanguageDetectOptimize: Defaults.Serializable {} + +extension EZLanguageDetectOptimize: CaseIterable { + public static let allCases: [EZLanguageDetectOptimize] = [.none, .baidu, .google] +} + +@available(macOS 13, *) +extension EZLanguageDetectOptimize: CustomLocalizedStringResourceConvertible { + public var localizedStringResource: LocalizedStringResource { + switch self { + case .none: + "language_detect_optimize_none" + case .google: + "language_detect_optimize_google" + case .baidu: + "language_detect_optimize_baidu" + @unknown default: + "unknown_option" + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/LanguageExtensions.swift b/Easydict/NewApp/Utility/Extensions/LanguageExtensions.swift new file mode 100644 index 000000000..b7cc84eae --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/LanguageExtensions.swift @@ -0,0 +1,78 @@ +// +// LanguageExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension Language: Defaults.Serializable {} + +extension Language: CaseIterable { + public static let allCases: [Language] = EZLanguageModel.allLanguagesDict().sortedKeys().map { rawValue in + Language(rawValue: rawValue as String) + } + + public static let allAvailableOptions: [Language] = allCases.filter { language in + language != .auto && language != .classicalChinese + } +} + +public extension Language { + var model: EZLanguageModel { + EZLanguageModel.allLanguagesDict().object(forKey: rawValue as NSString) + } + + var chineseName: String { + model.chineseName + } + + var englishName: String { + model.englishName.rawValue + } + + var localName: String { + model.localName + } + + var flagEmoji: String { + model.flagEmoji + } + + var voiceName: String { + model.voiceName + } + + var localeIdentifier: String { + model.localeIdentifier + } + + var localizedName: String { + if EZLanguageManager.shared().isSystemChineseFirstLanguage() { + chineseName + } else { + if self == .auto { + "Auto" + } else { + englishName + } + } + } +} + +extension [Language] { + /// Contains Chinese language, + func containsChinese() -> Bool { + contains { $0.isKindOfChinese() } + } +} + +extension Language { + /// Is kind of Chinese language, means it is simplifiedChinese or traditionalChinese. + func isKindOfChinese() -> Bool { + self == .simplifiedChinese || self == .traditionalChinese + } +} diff --git a/Easydict/NewApp/Utility/Extensions/ShowWindowPositionExtensions.swift b/Easydict/NewApp/Utility/Extensions/ShowWindowPositionExtensions.swift new file mode 100644 index 000000000..e531d64c1 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/ShowWindowPositionExtensions.swift @@ -0,0 +1,34 @@ +// +// ShowWindowPositionExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension EZShowWindowPosition: Defaults.Serializable {} + +extension EZShowWindowPosition: CaseIterable { + public static let allCases: [EZShowWindowPosition] = [.right, .mouse, .former, .center] +} + +@available(macOS 13, *) +extension EZShowWindowPosition: CustomLocalizedStringResourceConvertible { + public var localizedStringResource: LocalizedStringResource { + switch self { + case .right: + "fixed_window_position_right" + case .mouse: + "fixed_window_position_mouse" + case .former: + "fixed_window_position_former" + case .center: + "fixed_window_position_center" + @unknown default: + "unknown_option" + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/WindowTypeExtensions.swift b/Easydict/NewApp/Utility/Extensions/WindowTypeExtensions.swift new file mode 100644 index 000000000..34b60c049 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/WindowTypeExtensions.swift @@ -0,0 +1,34 @@ +// +// WindowTypeExtensions.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/13. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation + +extension EZWindowType: Defaults.Serializable {} + +public extension EZWindowType { + static let availableOptions: [EZWindowType] = [.mini, .fixed] +} + +@available(macOS 13, *) +extension EZWindowType: CustomLocalizedStringResourceConvertible { + public var localizedStringResource: LocalizedStringResource { + switch self { + case .fixed: + "fixed_window" + case .main: + "main_window" + case .mini: + "mini_window" + case .none: + "none_window" + @unknown default: + "unknown_option" + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift index e9368eaea..852631707 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import SwiftUI @available(macOS 13, *) @@ -13,44 +14,271 @@ struct GeneralTab: View { var body: some View { Form { Section { - HStack { - Text("show_main_window") - Toggle(isOn: $hideMainWindow) { - Text("hide_main_window") + Picker("setting.general.appearance.light_dark_appearance", selection: $appearanceType) { + ForEach(AppearenceType.allCases, id: \.rawValue) { option in + Text(option.title) + .tag(option) } } - HStack { - Text("launch") - Toggle(isOn: $launchAtStartup) { - Text("launch_at_startup") + } header: { + Text("setting.general.appearance.header") + } + Section { + FirstAndSecondLanguageSettingView() + Picker("setting.general.language.language_detect_optimize", selection: $languageDetectOptimize) { + ForEach(EZLanguageDetectOptimize.allCases, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) + } + } + } header: { + Text("setting.general.language.header") + } + + Section { + Toggle("auto_show_query_icon", isOn: $autoSelectText) + Toggle("force_auto_get_selected_text", isOn: $forceAutoGetSelectedText) + Toggle("click_icon_query_info", isOn: $clickQuery) + Toggle("setting.general.mouse_query.adjust_pop_button_origin", isOn: $adjustPopButtonOrigin) // 调整查询图标位置: + } header: { + Text("setting.general.mouse_query.header") + } + + Section { + Toggle("setting.general.voice.disable_empty_copy_beep_msg", isOn: $disableEmptyCopyBeep) // 禁用提示音:划词内容为空时生效 + Toggle("setting.general.voice.auto_play_word_audio", isOn: $autoPlayAudio) // 查询英语单词后自动播放发音 + } header: { + Text("setting.general.voice.header") + } + + Section { + Picker("setting.general.window.mouse_select_translate_window_type", selection: $mouseSelectTranslateWindowType) { + ForEach(EZWindowType.availableOptions, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) } } - HStack { - Text("menu_bar_icon") - Toggle(isOn: $hideMenuBarIcon) { - Text("hide_menu_bar_icon") + Picker("setting.general.window.shortcut_select_translate_window_type", selection: $shortcutSelectTranslateWindowType) { + ForEach(EZWindowType.availableOptions, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) } } - HStack { - Text("beta_new_app") - Toggle(isOn: $enableBetaNewApp) { - Text("enable_beta_new_app") + Picker("setting.general.window.fixed_window_position", selection: $fixedWindowPosition) { + ForEach(EZShowWindowPosition.allCases, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) } } + } header: { + Text("setting.general.windows.header") + } + + Section { + Toggle("clear_input_when_translating", isOn: $clearInput) + } header: { + Text("setting.general.input.header") + } + + Section { + Toggle("auto_query_ocr_text", isOn: $autoQueryOCRText) + Toggle("auto_query_selected_text", isOn: $autoQuerySelectedText) + Toggle("auto_query_pasted_text", isOn: $autoQueryPastedText) + } header: { + Text("setting.general.auto_query.header") + } + + Section { + Toggle("auto_copy_ocr_text", isOn: $autoCopyOCRText) + Toggle("auto_copy_selected_text", isOn: $autoCopySelectedText) + Toggle("auto_copy_first_translated_text", isOn: $autoCopyFirstTranslatedText) + } header: { + Text("setting.general.auto_copy.header") + } + + Section { + Toggle("show_google_quick_link", isOn: $showGoogleQuickLink) + Toggle("show_eudic_quick_link", isOn: $showEudicQuickLink) + Toggle("show_apple_dictionary_quick_link", isOn: $showAppleDictionaryQuickLink) + } header: { + Text("setting.general.quick_link.header") + } + + Section { + let bindingFontSize = Binding(get: { + Double(fontSizeOptionIndex) + }, set: { newValue in + fontSizeOptionIndex = UInt(newValue) + }) + Slider(value: bindingFontSize, in: 0.0 ... 4.0, step: 1) { + Text("setting.general.font.font_size.label") + } minimumValueLabel: { + Text("small") + .font(.system(size: 10)) + } maximumValueLabel: { + Text("large") + .font(.system(size: 14)) + } + } header: { + Text("setting.general.font.header") + } footer: { + Text("hints_keyboard_shortcuts_font_size") + .font(.footnote) + } + + Section { + Toggle(isOn: $launchAtStartup) { + Text("launch_at_startup") + } + Toggle(isOn: $hideMainWindow) { + Text("hide_main_window") + } + Toggle(isOn: $hideMenuBarIcon) { + Text("hide_menu_bar_icon") + } } header: { Text("other") } + + Section { + Picker("setting.general.advance.default_tts_service", selection: $defaultTTSServiceType) { + ForEach(TTSServiceType.allCases, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) + } + } + Toggle("setting.general.advance.enable_beta_feature", isOn: $enableBetaFeature) + Toggle(isOn: $enableBetaNewApp) { + Text("enable_beta_new_app") + } + } header: { + Text("setting.general.advance.header") + } } .formStyle(.grouped) } - @AppStorage(kHideMainWindowKey) private var hideMainWindow = false - @AppStorage(kLaunchAtStartupKey) private var launchAtStartup = false - @AppStorage(kHideMenuBarIconKey) private var hideMenuBarIcon = false - @AppStorage(kEnableBetaNewAppKey) private var enableBetaNewApp = false + @Default(.autoSelectText) private var autoSelectText + @Default(.forceAutoGetSelectedText) private var forceAutoGetSelectedText + @Default(.clickQuery) private var clickQuery + @Default(.adjustPopButtonOrigin) private var adjustPopButtonOrigin + + @Default(.clearInput) private var clearInput + + @Default(.disableEmptyCopyBeep) private var disableEmptyCopyBeep + @Default(.autoPlayAudio) private var autoPlayAudio + + @Default(.autoQueryOCRText) private var autoQueryOCRText + @Default(.autoQuerySelectedText) private var autoQuerySelectedText + @Default(.autoQueryPastedText) private var autoQueryPastedText + + @Default(.autoCopyOCRText) private var autoCopyOCRText + @Default(.autoCopySelectedText) private var autoCopySelectedText + @Default(.autoCopyFirstTranslatedText) private var autoCopyFirstTranslatedText + + @Default(.showGoogleQuickLink) private var showGoogleQuickLink + @Default(.showEudicQuickLink) private var showEudicQuickLink + @Default(.showAppleDictionaryQuickLink) private var showAppleDictionaryQuickLink + + @Default(.hideMainWindow) private var hideMainWindow + @Default(.launchAtStartup) private var launchAtStartup + @Default(.hideMenuBarIcon) private var hideMenuBarIcon + @Default(.enableBetaNewApp) private var enableBetaNewApp + + @Default(.languageDetectOptimize) private var languageDetectOptimize + @Default(.defaultTTSServiceType) private var defaultTTSServiceType + + @Default(.fixedWindowPosition) private var fixedWindowPosition + @Default(.mouseSelectTranslateWindowType) private var mouseSelectTranslateWindowType + @Default(.shortcutSelectTranslateWindowType) private var shortcutSelectTranslateWindowType + @Default(.enableBetaFeature) private var enableBetaFeature + @Default(.appearanceType) private var appearanceType + + @Default(.fontSizeOptionIndex) private var fontSizeOptionIndex } @available(macOS 13, *) #Preview { GeneralTab() } + +@available(macOS 13, *) +private struct FirstAndSecondLanguageSettingView: View { + var body: some View { + Group { + Picker("setting.general.language.first_language", selection: $firstLanguage) { + ForEach(Language.allAvailableOptions, id: \.rawValue) { option in + Text(verbatim: "\(option.flagEmoji) \(option.localizedName)") + .tag(option) + } + } + Picker("setting.general.language.second_language", selection: $secondLanguage) { + ForEach(Language.allAvailableOptions, id: \.rawValue) { option in + Text(verbatim: "\(option.flagEmoji) \(option.localizedName)") + .tag(option) + } + } + } + .onChange(of: firstLanguage) { [firstLanguage] newValue in + let oldValue = firstLanguage + if newValue == secondLanguage { + secondLanguage = oldValue + languageDuplicatedAlert = .init(duplicatedLanguage: newValue, setField: .second, setLanguage: oldValue) + } + } + .onChange(of: secondLanguage) { [secondLanguage] newValue in + let oldValue = secondLanguage + if newValue == firstLanguage { + firstLanguage = oldValue + languageDuplicatedAlert = .init(duplicatedLanguage: newValue, setField: .first, setLanguage: oldValue) + } + } + .alert("setting.general.language.duplicated_alert.title", isPresented: showLanguageDuplicatedAlert, presenting: languageDuplicatedAlert) { _ in + + } message: { alert in + Text(alert.description) + } + } + + @Default(.firstLanguage) private var firstLanguage + @Default(.secondLanguage) private var secondLanguage + + private struct LanguageDuplicateAlert: CustomStringConvertible { + var description: String { + // First language should not be same as second language. (\(duplicatedLanguage)) + // \(setField) is replaced with \(setLanguage). + String(localized: "setting.general.language.duplicated_alert \(duplicatedLanguage.localizedName)\(String(localized: setField.localizedStringResource))\(setLanguage.localizedName)") + } + + let duplicatedLanguage: Language + + let setField: Field + + let setLanguage: Language + + enum Field: CustomLocalizedStringResourceConvertible { + var localizedStringResource: LocalizedStringResource { + switch self { + case .first: + "setting.general.language.duplicated_alert.field.first" + case .second: + "setting.general.language.duplicated_alert.field.second" + } + } + + case first + case second + } + } + + @State private var languageDuplicatedAlert: LanguageDuplicateAlert? + private var showLanguageDuplicatedAlert: Binding { + .init { + languageDuplicatedAlert != nil + } set: { newValue in + if !newValue { + languageDuplicatedAlert = nil + } + } + } +} From 39b7730e9a21793da9e2dfe5e8cb7f71d2d16208 Mon Sep 17 00:00:00 2001 From: liyafly Date: Sun, 14 Jan 2024 17:56:52 +0800 Subject: [PATCH 13/72] Refactor settings menu in swiftui only UI (#316) * refactor: refactor the Settings page in SwiftUI * refactor: add Settings page some localizable * refactor: Add functionality to list options on menu pages * Fix Xcode nullable warning * refactor: renames the repeatedly defined properties (cherry picked from commit ca75f5a8bc83e2d9b2db6def8c964010cfdfdf00) * refactor: Perfect SPUStandardUpdaterController delegate implementation --------- Co-authored-by: Kyle Co-authored-by: liyafei Co-authored-by: Tisfeng --- Easydict/App/Easydict-Bridging-Header.h | 2 + Easydict/App/Localizable.xcstrings | 122 +++++++++++++++ .../EZCategory/NSViewController+EZWindow.h | 2 +- .../EZCategory/NSViewController+EZWindow.m | 2 +- .../EZBaseQueryViewController.h | 2 +- .../EZBaseQueryViewController.m | 6 +- .../BaseQueryWindow/EZBaseQueryWindow.m | 2 +- Easydict/NewApp/EasydictApp.swift | 33 ++++- Easydict/NewApp/View/MenuItemView.swift | 140 +++++++++++++++++- 9 files changed, 299 insertions(+), 12 deletions(-) diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 2747a587f..3753f8f67 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -23,3 +23,5 @@ #import "EZConfiguration.h" #import "NSString+EZConvenience.h" +#import "EZWindowManager.h" +#import "NSViewController+EZWindow.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index aa35e533b..924386b73 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1058,6 +1058,16 @@ } } }, + "Export Log" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "导出日志" + } + } + } + }, "Failed to allocate memory" : { "comment" : "Error reason", "localizations" : { @@ -1069,6 +1079,16 @@ } } }, + "Feedback" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "反馈问题" + } + } + } + }, "first_language" : { "localizations" : { "en" : { @@ -1283,6 +1303,16 @@ } } }, + "Help" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "帮助" + } + } + } + }, "hide" : { "localizations" : { "en" : { @@ -1569,6 +1599,16 @@ } } }, + "Log Directory" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "日志目录" + } + } + } + }, "main_window" : { "localizations" : { "en" : { @@ -1612,6 +1652,88 @@ } } }, + "menu_input_translate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Input Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "输入翻译" + } + } + } + }, + "menu_screenshot_Translate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Screenshot Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "截图翻译" + } + } + } + }, + "menu_selectWord_Translate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select Translate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "划词翻译" + } + } + } + }, + "menu_show_mini_window" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show Mini Window" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "显示迷你窗口" + } + } + } + }, + "menu_silent_screenshot_OCR" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Silent Screenshot OCR" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "静默截图 OCR" + } + } + } + }, "mini_window" : { "localizations" : { "en" : { diff --git a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h index de32711b0..9890d7b89 100644 --- a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h +++ b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.h @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @interface NSViewController (EZWindow) -- (NSWindow *)window; +- (nullable NSWindow *)window; @end diff --git a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m index a73e9e2aa..a46341981 100644 --- a/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m +++ b/Easydict/Feature/Utility/EZCategory/NSViewController+EZWindow.m @@ -10,7 +10,7 @@ @implementation NSViewController (EZWindow) -- (NSWindow *)window { +- (nullable NSWindow *)window { NSResponder *responder = self; while ((responder = [responder nextResponder])) { if ([responder isKindOfClass:[NSWindow class]]) { diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h index e407b1360..e7301b5c8 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) NSString *inputText; @property (nonatomic, assign) EZWindowType windowType; -@property (nonatomic, weak) EZBaseQueryWindow *window; +@property (nullable, nonatomic, weak) EZBaseQueryWindow *baseQueryWindow; @property (nonatomic, strong, readonly) NSArray *services; diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index fd28c7928..58d48a8a3 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -522,11 +522,11 @@ - (void)retryQuery { - (void)focusInputTextView { // Fix ⚠️: ERROR: Setting as the first responder for window , but it is in a different window ((null))! This would eventually crash when the view is freed. The first responder will be set to nil. - if (self.queryView.window == self.window) { + if (self.queryView.window == self.baseQueryWindow) { // Need to activate the current application first. [NSApp activateIgnoringOtherApps:YES]; - [self.window makeFirstResponder:self.queryView.textView]; + [self.baseQueryWindow makeFirstResponder:self.queryView.textView]; } } @@ -1420,7 +1420,7 @@ - (void)updateWindowViewHeightWithLock:(BOOL)lockFlag // ???: why set window frame will change tableView height? // ???: why this window animation will block cell rendering? // [self.window setFrame:safeFrame display:NO animate:animateFlag]; - [self.window setFrame:safeFrame display:NO]; + [self.baseQueryWindow setFrame:safeFrame display:NO]; // Restore tableView height. self.tableView.height = tableViewHeight; diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m index 1e16d79e8..b2b56b8bd 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryWindow.m @@ -70,7 +70,7 @@ - (void)setWindowType:(EZWindowType)windowType { - (void)setQueryViewController:(EZBaseQueryViewController *)viewController { _queryViewController = viewController; - viewController.window = self; + viewController.baseQueryWindow = self; self.contentViewController = viewController; } diff --git a/Easydict/NewApp/EasydictApp.swift b/Easydict/NewApp/EasydictApp.swift index ae385f4e7..c81ba83f4 100644 --- a/Easydict/NewApp/EasydictApp.swift +++ b/Easydict/NewApp/EasydictApp.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import Sparkle import SwiftUI @main @@ -20,6 +21,22 @@ enum EasydictCmpatibilityEntry { } } +class SPUUpdaterHelper: NSObject, SPUUpdaterDelegate { + func feedURLString(for _: SPUUpdater) -> String? { + var feedURLString = "https://raw.githubusercontent.com/tisfeng/Easydict/main/appcast.xml" + #if DEBUG + feedURLString = "http://localhost:8000/appcast.xml" + #endif + return feedURLString + } +} + +class SPUUserDriverHelper: NSObject, SPUStandardUserDriverDelegate { + var supportsGentleScheduledUpdateReminders: Bool { + true + } +} + struct EasydictApp: App { @NSApplicationDelegateAdaptor var delegate: AppDelegate @@ -35,10 +52,22 @@ struct EasydictApp: App { #endif } + let userDriverHelper = SPUUserDriverHelper() + let upadterHelper = SPUUpdaterHelper() + + private let updaterController: SPUStandardUpdaterController + + init() { + // 参考 https://sparkle-project.org/documentation/programmatic-setup/ + // If you want to start the updater manually, pass false to startingUpdater and call .startUpdater() later + // This is where you can also pass an updater delegate if you need one + updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: upadterHelper, userDriverDelegate: userDriverHelper) + } + var body: some Scene { if #available(macOS 13, *) { MenuBarExtra(isInserted: $hideMenuBar.toggledValue) { - MenuItemView() + MenuItemView(updater: updaterController.updater) } label: { Label { Text("Easydict") @@ -49,7 +78,7 @@ struct EasydictApp: App { .scaledToFit() } .help("Easydict 🍃") - } + }.menuBarExtraStyle(.menu) Settings { SettingView() } diff --git a/Easydict/NewApp/View/MenuItemView.swift b/Easydict/NewApp/View/MenuItemView.swift index 806abc807..999bd03b3 100644 --- a/Easydict/NewApp/View/MenuItemView.swift +++ b/Easydict/NewApp/View/MenuItemView.swift @@ -8,16 +8,44 @@ import Sparkle import SwiftUI +import ZipArchive + +@available(macOS 13, *) +final class MenuItemStore: ObservableObject { + @Published var canCheckForUpdates = false + var updater: SPUUpdater + init(updater: SPUUpdater) { + self.updater = updater + self.updater.publisher(for: \.canCheckForUpdates) + .assign(to: &$canCheckForUpdates) + } +} @available(macOS 13, *) struct MenuItemView: View { + @ObservedObject var store: MenuItemStore + + init(updater: SPUUpdater) { + store = MenuItemStore(updater: updater) + } + var body: some View { + // ️.menuBarExtraStyle为 .menu 时某些控件可能会失效 ,只能显示内容(按照菜单项高度、图像以 template 方式渲染)无法交互 ,比如 Stepper、Slider 等,像基本的 Button、Text、Divider、Image 等还是能正常显示的。 + // Button 和Label的systemImage是不会渲染的 Group { versionItem Divider() + inputItem + screenshotItem + selectWordItem + miniWindowItem + Divider() + ocrItem + Divider() settingItem .keyboardShortcut(.init(",")) checkUpdateItem + helpItem Divider() quitItem .keyboardShortcut(.init("q")) @@ -72,12 +100,81 @@ struct MenuItemView: View { } } + // MARK: - List of functions + + @ViewBuilder + private var inputItem: some View { + Button { + NSLog("输入翻译") + EZWindowManager.shared().inputTranslate() + } label: { + HStack { + Image(systemName: "keyboard") + Text("menu_input_translate") + } + } + } + + @ViewBuilder + private var screenshotItem: some View { + Button { + NSLog("截图翻译") + EZWindowManager.shared().snipTranslate() + } label: { + HStack { + Image(systemName: "camera.viewfinder") + Text("menu_screenshot_Translate") + } + } + } + + @ViewBuilder + private var selectWordItem: some View { + Button { + NSLog("划词翻译") + EZWindowManager.shared().selectTextTranslate() + } label: { + HStack { + Image(systemName: "highlighter") + Text("menu_selectWord_Translate") + } + } + } + + @ViewBuilder + private var miniWindowItem: some View { + Button { + NSLog("显示迷你窗口") + EZWindowManager.shared().showMiniFloatingWindow() + } label: { + HStack { + Image(systemName: "dock.rectangle") + Text("menu_show_mini_window") + } + } + } + + @ViewBuilder + private var ocrItem: some View { + Button { + NSLog("静默截图OCR") + EZWindowManager.shared().screenshotOCR() + } label: { + HStack { + Image(systemName: "camera.metering.spot") + Text("menu_silent_screenshot_OCR") + } + } + } + + // MARK: - Setting + @ViewBuilder private var checkUpdateItem: some View { Button("check_updates") { NSLog("检查更新") - SPUStandardUpdaterController(updaterDelegate: nil, userDriverDelegate: nil).checkForUpdates(nil) - } + store.updater.checkForUpdates() + }.disabled(!store.canCheckForUpdates) } @ViewBuilder @@ -87,9 +184,46 @@ struct MenuItemView: View { NSApplication.shared.terminate(nil) } } + + @ViewBuilder + private var helpItem: some View { + Menu("Help") { + Button("Feedback") { + guard let versionURL = URL(string: "\(EZGithubRepoEasydictURL)/issues") else { + return + } + openURL(versionURL) + } + Button("Export Log") { + exportLogAction() + } + Button("Log Directory") { + NSLog("日志目录") + let logPath = MMManagerForLog.rootLogDirectory() ?? "" + let directoryURL = URL(fileURLWithPath: logPath) + NSWorkspace.shared.open(directoryURL) + } + } + } + + private func exportLogAction() { + NSLog("导出日志") + let logPath = MMManagerForLog.rootLogDirectory() ?? "" + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH-mm-ss-SSS" + let dataString = dateFormatter.string(from: Date()) + let downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] + let zipPath = downloadDirectory.appendingPathComponent("Easydict log \(dataString).zip").path(percentEncoded: false) + let success = SSZipArchive.createZipFile(atPath: zipPath, withContentsOfDirectory: logPath, keepParentDirectory: false) + if success { + NSWorkspace.shared.selectFile(zipPath, inFileViewerRootedAtPath: "") + } else { + MMLogInfo("导出日志失败") + } + } } @available(macOS 13, *) #Preview { - MenuItemView() + MenuItemView(updater: SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil).updater) } From baca1fdef1d0b8fe57e633f518ee8d2b2cd759bf Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Sun, 14 Jan 2024 18:34:01 +0800 Subject: [PATCH 14/72] Add service tab in settings use SwiftUI (#311) * feat: add service tab screen use SwiftUI * feat: add service list move function * fix: Removed a file change * fix: service list order issue and UI change * style: format code * fix: change service tab frame size * feat: update the style of service list * style: auto update Localizable.xcstrings * perf: remove unused variables * fix: service list index of bounds issue and tap issue * fix: serviceTypes count is greater than services count, cause crash * perf: adjust service rowHeight to 30 * perf: hide scrollIndicators * fix: reset service list selection * feat: optimize tab selection window transition * fix: window frame not reset issue * fix: onTap conflict with onMove in service tab * fix: remove tap event print --------- Co-authored-by: tisfeng --- Easydict.xcodeproj/project.pbxproj | 40 +++++ .../Contents.json | 38 +++++ Easydict/App/Easydict-Bridging-Header.h | 1 + Easydict/App/Localizable.xcstrings | 3 + .../Swift/Binding/Binding+DidSet.swift | 23 +++ .../Notification/Notification+Name.swift | 17 ++ Easydict/NewApp/View/ServiceItemView.swift | 41 +++++ .../NewApp/View/SettingView/SettingView.swift | 43 +++++- .../View/SettingView/Tabs/AboutTab.swift | 2 + .../View/SettingView/Tabs/ServiceTab.swift | 146 ++++++++++++++++++ Easydict/NewApp/View/TapHandlerView.swift | 42 +++++ Easydict/NewApp/View/WindowAccessor.swift | 23 +++ 12 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json create mode 100644 Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift create mode 100644 Easydict/Feature/Utility/Swift/Notification/Notification+Name.swift create mode 100644 Easydict/NewApp/View/ServiceItemView.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift create mode 100644 Easydict/NewApp/View/TapHandlerView.swift create mode 100644 Easydict/NewApp/View/WindowAccessor.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index ec44cd1ab..7438afb68 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -228,6 +228,12 @@ 03FC699A2B39D13A0035D2DA /* EZOpenAIChatResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 03FC69992B39D13A0035D2DA /* EZOpenAIChatResponse.m */; }; 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 */; }; + 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */; }; 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */; }; 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF62B0DFF9000A7D372 /* EZNiuTransTranslate.m */; }; 2721E4D02AFE920700A059AC /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 2721E4CF2AFE920700A059AC /* Alamofire */; }; @@ -696,6 +702,12 @@ 03FC69992B39D13A0035D2DA /* EZOpenAIChatResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EZOpenAIChatResponse.m; sourceTree = ""; }; 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+EncryptAES.swift"; sourceTree = ""; }; 06E15747A7BD34D510ADC6A8 /* Pods-Easydict.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.debug.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.debug.xcconfig"; sourceTree = ""; }; + 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 = ""; }; + 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapHandlerView.swift; sourceTree = ""; }; 17BCAEF32B0DFF9000A7D372 /* EZNiuTransTranslateResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslateResponse.h; sourceTree = ""; }; 17BCAEF42B0DFF9000A7D372 /* EZNiuTransTranslate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslate.h; sourceTree = ""; }; 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EZNiuTransTranslateResponse.m; sourceTree = ""; }; @@ -1806,6 +1818,8 @@ 03CF88602B137ECB0030C199 /* Swift */ = { isa = PBXGroup; children = ( + 0A2BA9622B4A3CBB002872A4 /* Notification */, + 0A2BA95E2B49A967002872A4 /* Binding */, 03FD68BC2B1E14B500FD388E /* String */, 03CF88612B137ED60030C199 /* Array */, ); @@ -1984,6 +1998,22 @@ path = String; sourceTree = ""; }; + 0A2BA95E2B49A967002872A4 /* Binding */ = { + isa = PBXGroup; + children = ( + 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */, + ); + path = Binding; + sourceTree = ""; + }; + 0A2BA9622B4A3CBB002872A4 /* Notification */ = { + isa = PBXGroup; + children = ( + 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */, + ); + path = Notification; + sourceTree = ""; + }; 17BCAEF22B0DFF9000A7D372 /* Niutrans */ = { isa = PBXGroup; children = ( @@ -2022,6 +2052,9 @@ isa = PBXGroup; children = ( 27FE980A2B3DD5D1000AD654 /* MenuItemView.swift */, + 0A057D6E2B499A0B0025C51D /* ServiceItemView.swift */, + 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */, + 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */, 27FE98072B3DD52B000AD654 /* SettingView */, ); path = View; @@ -2040,6 +2073,7 @@ isa = PBXGroup; children = ( 278540332B3DE04F004E9488 /* GeneralTab.swift */, + 0A057D6C2B499A000025C51D /* ServiceTab.swift */, 276742042B3DC230002A2C75 /* PrivacyTab.swift */, 276742052B3DC230002A2C75 /* AboutTab.swift */, ); @@ -2571,6 +2605,7 @@ 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 */, @@ -2608,6 +2643,7 @@ 27FE95272B3DC55F000AD654 /* EasydictApp.swift in Sources */, 03882F9129D95044005B5A52 /* CTCommon.m in Sources */, 276742082B3DC230002A2C75 /* PrivacyTab.swift in Sources */, + 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */, 03882F8F29D95044005B5A52 /* CTScreen.m in Sources */, 27FE980B2B3DD5D1000AD654 /* MenuItemView.swift in Sources */, 03DC7C6A2A3CA852000BF7C9 /* EZAppCell.m in Sources */, @@ -2619,6 +2655,7 @@ 033B7134293CE2430096E2DF /* EZWebViewTranslator.m in Sources */, 03CF88632B137F650030C199 /* Array+Convenience.swift in Sources */, 03B0231229231FA6001C7E63 /* NSObject+DarkMode.m in Sources */, + 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */, 03B0233829231FA6001C7E63 /* MMOrderedDictionary.m in Sources */, 278540342B3DE04F004E9488 /* GeneralTab.swift in Sources */, 03BDA7BC2A26DA280079D04F /* XPMArgumentSignature.m in Sources */, @@ -2637,6 +2674,7 @@ 03B0230529231FA6001C7E63 /* EZButton.m in Sources */, 03B0232329231FA6001C7E63 /* NSString+MM.m in Sources */, 036196772A000F5900806370 /* NSData+CommonCrypto.m in Sources */, + 0A057D6D2B499A000025C51D /* ServiceTab.swift in Sources */, 03882F8D29D95044005B5A52 /* CTView.m in Sources */, 278322602B0FB0EA0026644C /* CaiyunResponse.swift in Sources */, 03B3B8B52925DD3D00168E8D /* EZPopButtonViewController.m in Sources */, @@ -2662,6 +2700,7 @@ 039CC90D292F664E0037B91E /* NSObject+EZWindowType.m in Sources */, 03B0232229231FA6001C7E63 /* NSImage+MM.m in Sources */, 03BB2DEF29F59C8A00447EDD /* EZSymbolImageButton.m in Sources */, + 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */, 62A2D03F2A82967F007EEB01 /* EZBingRequest.m in Sources */, 03BDA7BE2A26DA280079D04F /* XPMCountedArgument.m in Sources */, 03D35DAA2AA6C49B00B023FE /* NSString+EZRegex.m in Sources */, @@ -2706,6 +2745,7 @@ 03B0232629231FA6001C7E63 /* NSAttributedString+MM.m in Sources */, 03542A402937B3C900C34C33 /* EZOCRResult.m in Sources */, C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */, + 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */, EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */, 036A0DBB2AD941F9006E6D4F /* EZReplaceTextButton.m in Sources */, 03DC7C662A3CA465000BF7C9 /* HWSegmentedControl.m in Sources */, diff --git a/Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json b/Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json new file mode 100644 index 000000000..f6b481345 --- /dev/null +++ b/Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.847", + "red" : "0.706" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.251", + "green" : "0.251", + "red" : "0.251" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 3753f8f67..8030b13fa 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -21,6 +21,7 @@ #import "entry.h" #import "AppDelegate.h" #import "EZConfiguration.h" +#import "EZLocalStorage.h" #import "NSString+EZConvenience.h" #import "EZWindowManager.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 924386b73..c75689b1e 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, "about" : { "comment" : "about", "localizations" : { diff --git a/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift new file mode 100644 index 000000000..244b735cb --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift @@ -0,0 +1,23 @@ +// +// Binding+DidSet.swift +// Easydict +// +// Created by phlpsong on 2024/1/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +// Ref https://stackoverflow.com/a/62871938 +// Toggle onChange not trigger issue +extension Binding { + func didSet(execute: @escaping (Value) -> Void) -> Binding { + Binding( + get: { self.wrappedValue }, + set: { + self.wrappedValue = $0 + execute($0) + } + ) + } +} diff --git a/Easydict/Feature/Utility/Swift/Notification/Notification+Name.swift b/Easydict/Feature/Utility/Swift/Notification/Notification+Name.swift new file mode 100644 index 000000000..71c773f81 --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Notification/Notification+Name.swift @@ -0,0 +1,17 @@ +// +// Notification+Name.swift +// Easydict +// +// Created by phlpsong on 2024/1/7. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Notification.Name { + static let serviceHasUpdated = Notification.Name(EZServiceHasUpdatedNotification) +} + +@objc public extension NSNotification { + static let serviceHasUpdated = Notification.Name.serviceHasUpdated +} diff --git a/Easydict/NewApp/View/ServiceItemView.swift b/Easydict/NewApp/View/ServiceItemView.swift new file mode 100644 index 000000000..d21f17e98 --- /dev/null +++ b/Easydict/NewApp/View/ServiceItemView.swift @@ -0,0 +1,41 @@ +// +// 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 2e5da7bf3..8c4fec008 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -8,20 +8,55 @@ import SwiftUI +enum SettingTab: Int { + case general + case service + case privacy + case about +} + @available(macOS 13, *) struct SettingView: View { + @State private var selection = SettingTab.general.rawValue + @State private var window: NSWindow? + var body: some View { - TabView { + TabView(selection: $selection.didSet(execute: { _ in + resizeWindowFrame() + })) { GeneralTab() .tabItem { Label("setting_general", systemImage: "gear") } - .frame(width: 500, height: 400) + .tag(SettingTab.general.rawValue) + + ServiceTab() + .tabItem { Label("service", systemImage: "briefcase") } + .tag(SettingTab.service.rawValue) + PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } - .frame(width: 500, height: 400) + .tag(SettingTab.privacy.rawValue) + AboutTab() .tabItem { Label("about", systemImage: "info.bubble") } - .frame(width: 500, height: 400) + .tag(SettingTab.about.rawValue) } + .background(WindowAccessor(window: $window.didSet(execute: { _ in + // reset frame when first launch + 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 newY = originalFrame.origin.y + originalFrame.size.height - newSize.height + let newRect = NSRect(origin: CGPoint(x: originalFrame.origin.x, y: newY), size: newSize) + + window.setFrame(newRect, display: true, animate: true) } } diff --git a/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift b/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift index 92de82c24..aa6f6a4e4 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift @@ -34,7 +34,9 @@ struct AboutTab: View { } .padding(.horizontal, 50) .padding(.vertical, 30) + .frame(maxWidth: .infinity) } + .scrollIndicators(.hidden) .task { let version = await EZMenuItemManager.shared().fetchRepoLatestVersion(EZGithubRepoEasydict) await MainActor.run { diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift new file mode 100644 index 000000000..3da7ca423 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift @@ -0,0 +1,146 @@ +// +// ServiceTab.swift +// Easydict +// +// Created by phlpsong on 2024/1/6. +// Copyright © 2024 izual. All rights reserved. +// + +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 + } + } + + 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 + } + .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 + } else { + selectedIndex = nil + } + })) + } + .onMove(perform: { indices, newOffset in + onServiceItemMove(fromOffsets: indices, toOffset: newOffset) + selectedIndex = nil + }) + .listRowSeparator(.hidden) + } + .scrollIndicators(.hidden) + .listStyle(.plain) + .clipShape(RoundedRectangle(cornerRadius: 8.0)) + .padding([.horizontal, .bottom]) + } + + var body: some View { + VStack { + segmentCtrl + + serviceList + } + .onAppear { + loadService(type: windowTypeValue) + } + } + + func loadService(type: Int) { + let windowType = EZWindowType(rawValue: type) ?? .none + services = EZLocalStorage.shared().allServices(windowType) + serviceTypes = services.compactMap { $0.serviceType() } + } + + 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() + } + + func enabledServices(in services: [QueryService]) -> [QueryService] { + services.filter(\.enabled) + } + + func onServiceItemMove(fromOffsets: IndexSet, toOffset: Int) { + let oldEnabledServices = enabledServices(in: services) + + services.move(fromOffsets: fromOffsets, toOffset: toOffset) + serviceTypes.move(fromOffsets: fromOffsets, toOffset: toOffset) + + let windowType = EZWindowType(rawValue: windowTypeValue) ?? .none + 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() + } + } + + func isEnabledServicesOrderChanged( + source: [QueryService], + dest: [QueryService] + ) -> Bool { + !source.elementsEqual(dest) { sItem, dItem in + sItem.serviceType() == dItem.serviceType() && sItem.name() == dItem.name() + } + } + + func postUpdateServiceNotification() { + let userInfo: [String: Any] = [EZWindowTypeKey: windowTypeValue] + let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo) + NotificationCenter.default.post(notification) + } +} + +@available(macOS 13, *) +#Preview { + ServiceTab() +} diff --git a/Easydict/NewApp/View/TapHandlerView.swift b/Easydict/NewApp/View/TapHandlerView.swift new file mode 100644 index 000000000..39bdff50f --- /dev/null +++ b/Easydict/NewApp/View/TapHandlerView.swift @@ -0,0 +1,42 @@ +// +// TapHandlerView.swift +// Easydict +// +// Created by phlpsong on 2024/1/10. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +// Ref: https://stackoverflow.com/a/64194868/8378840 +// Fix conflicts between onTap and onMove modifier +class TapHandlerView: NSView { + var tapAction: () -> Void + + init(_ block: @escaping () -> Void) { + tapAction = block + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + tapAction() + } +} + +struct TapHandler: NSViewRepresentable { + let tapAction: () -> Void + + func makeNSView(context _: Context) -> TapHandlerView { + TapHandlerView(tapAction) + } + + func updateNSView(_ nsView: TapHandlerView, context _: Context) { + nsView.tapAction = tapAction + } +} diff --git a/Easydict/NewApp/View/WindowAccessor.swift b/Easydict/NewApp/View/WindowAccessor.swift new file mode 100644 index 000000000..b748023cb --- /dev/null +++ b/Easydict/NewApp/View/WindowAccessor.swift @@ -0,0 +1,23 @@ +// +// WindowAccessor.swift +// Easydict +// +// Created by phlpsong on 2024/1/9. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +struct WindowAccessor: NSViewRepresentable { + @Binding var window: NSWindow? + + func makeNSView(context _: Context) -> NSView { + let view = NSView() + DispatchQueue.main.async { + window = view.window + } + return view + } + + func updateNSView(_: NSView, context _: Context) {} +} From 068203e9914e371c3ea0adc89588924130ae56a2 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sun, 14 Jan 2024 21:23:40 +0800 Subject: [PATCH 15/72] perf: adjust App name in menu item --- Easydict/NewApp/View/MenuItemView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict/NewApp/View/MenuItemView.swift b/Easydict/NewApp/View/MenuItemView.swift index 999bd03b3..5f25919b1 100644 --- a/Easydict/NewApp/View/MenuItemView.swift +++ b/Easydict/NewApp/View/MenuItemView.swift @@ -78,7 +78,7 @@ struct MenuItemView: View { } private var versionString: String { - let defaultLabel = "Easydict \(currentVersion)" + let defaultLabel = "Easydict \(currentVersion)" if let latestVersion, currentVersion.compare(latestVersion, options: .numeric) == .orderedAscending { From f2bcfa07a2244b74e34feb0a90812fc6b78ab026 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Mon, 15 Jan 2024 10:04:54 +0800 Subject: [PATCH 16/72] Revert "chore: auto add .git for Alamofire" This reverts commit bec66dad6df7427a9abff7af1419644c73cfde62. --- Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9c63ea7f5..38cf97c19 100644 --- a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -12,7 +12,7 @@ { "identity" : "alamofire", "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", + "location" : "https://github.com/Alamofire/Alamofire", "state" : { "revision" : "3dc6a42c7727c49bf26508e29b0a0b35f9c7e1ad", "version" : "5.8.1" From 89b6a872f184e59101124a0d2804ab9630efb7cb Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:10:41 +0800 Subject: [PATCH 17/72] fix: swift format issue while enable SwiftUI preview (#328) * fix: swift format while enable swiftui preview * Update project.pbxproj Co-authored-by: Tisfeng --------- Co-authored-by: Tisfeng --- Easydict.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 7438afb68..a1e5d3696 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -2467,7 +2467,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\" \"$SRCROOT/$TARGET_NAME\" --swiftversion 5.7\n"; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ \"${ENABLE_PREVIEWS}\" = \"YES\" ]; then\n echo \"SwiftFormat skipped for Xcode Previews.\"\n exit 0;\nfi\n\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\" \"$SRCROOT/$TARGET_NAME\" --swiftversion 5.7\n"; }; 124D690EE7236D6430CF945E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; From 2ae4e7e16388c571a6acb7c4da466b6b5bd928c0 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Tue, 16 Jan 2024 17:56:09 +0800 Subject: [PATCH 18/72] fix: add some missing English translations (#327) * fix: add some missing English translations * fix: remove needs_review label * fix: update label in localized strings * perf: update Localizable.xcstrings --------- Co-authored-by: tisfeng --- Easydict/App/Localizable.xcstrings | 55 +++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index c75689b1e..ae094d259 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -2,7 +2,14 @@ "sourceLanguage" : "en", "strings" : { "" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "about" : { "comment" : "about", @@ -490,9 +497,15 @@ }, "beta_new_app" : { "localizations" : { - "zh-Hans" : { + "en" : { "stringUnit" : { "state" : "translated", + "value" : "[Beta] SwiftUI App mode" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", "value" : "[Beta] SwiftUI App模式" } } @@ -560,6 +573,12 @@ }, "check_updates" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Check for Updates" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -908,9 +927,15 @@ }, "enable_beta_new_app" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launching in SwiftUI App mode, changes take effect upon restarting the app." + } + }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "以 SwiftUI App模式启动,重启App生效" } } @@ -1666,7 +1691,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "输入翻译" } } @@ -1683,7 +1708,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "截图翻译" } } @@ -1699,7 +1724,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "划词翻译" } } @@ -1715,7 +1740,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "显示迷你窗口" } } @@ -1731,7 +1756,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "静默截图 OCR" } } @@ -2180,6 +2205,12 @@ }, "quit" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quit" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2599,7 +2630,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "调整查询图标位置(避免和 PopClip 显示冲突)" } } @@ -2639,6 +2670,12 @@ }, "setting.general.voice.auto_play_word_audio" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto play pronunciation after querying English words" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", From 3c46e75e70a7b197ab1d83c39f8f3d068662469a Mon Sep 17 00:00:00 2001 From: tisfeng Date: Tue, 16 Jan 2024 21:55:34 +0800 Subject: [PATCH 19/72] docs: update sponsor list --- README.md | 2 ++ README_EN.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 375d4e6ea..1645ba8af 100644 --- a/README.md +++ b/README.md @@ -821,6 +821,8 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2023-12-07 | 小逗。🎈 | 5 | | | 2023-12-26 | ㅤ Yee | 5 | 感谢开源 | | 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! | +| 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | +| 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 |

diff --git a/README_EN.md b/README_EN.md index 727d367a0..026b259d3 100644 --- a/README_EN.md +++ b/README_EN.md @@ -818,6 +818,8 @@ If you don't want your username to be displayed in the list, please choose anony | 2023-12-07 | 小逗。🎈 | 5 | | | 2023-12-26 | ㅤ Yee | 5 | 感谢开源 | | 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! | +| 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | +| 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 |

From 029a7d8d81f073e90a843a3f771977b3b54add91 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:05:10 +0800 Subject: [PATCH 20/72] fix: update README (#333) * fix: update README * Update README.md Co-authored-by: Tisfeng --------- Co-authored-by: Tisfeng --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1645ba8af..2f0a8a952 100644 --- a/README.md +++ b/README.md @@ -734,7 +734,7 @@ Easydict 有一些应用内快捷键,方便你在使用过程中更加高效 ## 致谢 -- /是以 [Bob (GPL-3.0)](https://github.com/1xiaocainiao/Bob) 为基础开发。Easydict 在原项目上进行了许多改进和优化,很多功能和 UI 都参考了 Bob。 +- 这个项目的灵感来自 [saladict](https://github.com/crimx/ext-saladict) 和 [Bob](https://github.com/ripperhe/Bob),且初始版本是以 [Bob (GPL-3.0)](https://github.com/1xiaocainiao/Bob) 为基础开发。Easydict 在原项目上进行了许多改进和优化,很多功能和 UI 都参考了 Bob。 - 截图功能是基于 [isee15](https://github.com/isee15) 的 [Capture-Screen-For-Multi-Screens-On-Mac](https://github.com/isee15/Capture-Screen-For-Multi-Screens-On-Mac),并在此基础上进行了优化。 - 鼠标划词功能参考了 [PopClip](https://pilotmoon.com/popclip/)。 From 63729d6d4e5bc2094ee2cb1d0b126549632ee687 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Tue, 16 Jan 2024 22:46:52 +0800 Subject: [PATCH 21/72] refactor: rewrite BingLanguageVoice with Swift by Jerry23011 --- Easydict.xcodeproj/project.pbxproj | 10 +++---- .../Service/Bing/BingLanguageVoice.swift | 26 +++++++++++++++++++ .../Service/Bing/EZBingLanguageVoice.h | 24 ----------------- .../Service/Bing/EZBingLanguageVoice.m | 20 -------------- Easydict/Feature/Service/Bing/EZBingRequest.m | 2 +- 5 files changed, 31 insertions(+), 51 deletions(-) create mode 100644 Easydict/Feature/Service/Bing/BingLanguageVoice.swift delete mode 100644 Easydict/Feature/Service/Bing/EZBingLanguageVoice.h delete mode 100644 Easydict/Feature/Service/Bing/EZBingLanguageVoice.m diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index a1e5d3696..adc5e75bc 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1EC292B439A00AFB76A /* EZTextView.m */; }; 0309E1F0292B4A5E00AFB76A /* NSView+EZGetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1EF292B4A5E00AFB76A /* NSView+EZGetViewController.m */; }; 0309E1F4292BD6A100AFB76A /* EZQueryModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1F3292BD6A100AFB76A /* EZQueryModel.m */; }; + 030DB2612B56CC6500E27DEA /* BingLanguageVoice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */; }; 0310C8272A94F5DF00B1D81E /* apple-dictionary.html in Resources */ = {isa = PBXBuildFile; fileRef = 0310C8262A94EFA100B1D81E /* apple-dictionary.html */; }; 0313F8702AD5577400A5CFB0 /* EasydictTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0313F86F2AD5577400A5CFB0 /* EasydictTests.m */; }; 031DBD792AE01E130071CF85 /* easydict in Resources */ = {isa = PBXBuildFile; fileRef = 031DBD782AE01E130071CF85 /* easydict */; }; @@ -66,7 +67,6 @@ 037852B329583F5200D0E2CF /* EZServiceCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 037852B229583F5200D0E2CF /* EZServiceCell.m */; }; 037852B629588EDE00D0E2CF /* EZCustomTableRowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 037852B529588EDE00D0E2CF /* EZCustomTableRowView.m */; }; 037852B9295D49F900D0E2CF /* EZTableRowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 037852B8295D49F900D0E2CF /* EZTableRowView.m */; }; - 037BEFCD2A98FDF700D0F17F /* EZBingLanguageVoice.m in Sources */ = {isa = PBXBuildFile; fileRef = 037BEFCC2A98FDF700D0F17F /* EZBingLanguageVoice.m */; }; 037E006D2B3DC098006491C6 /* EZOpenAIService+EZPromptMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 03CF27FA2B3A787900E19B57 /* EZOpenAIService+EZPromptMessages.m */; }; 038030952B4106800009230C /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 038030942B4106800009230C /* CocoaLumberjack */; }; 038030972B4106800009230C /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 038030962B4106800009230C /* CocoaLumberjackSwift */; }; @@ -325,6 +325,7 @@ 0309E1EF292B4A5E00AFB76A /* NSView+EZGetViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSView+EZGetViewController.m"; sourceTree = ""; }; 0309E1F2292BD6A100AFB76A /* EZQueryModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZQueryModel.h; sourceTree = ""; }; 0309E1F3292BD6A100AFB76A /* EZQueryModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZQueryModel.m; sourceTree = ""; }; + 030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BingLanguageVoice.swift; sourceTree = ""; }; 0310C8262A94EFA100B1D81E /* apple-dictionary.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "apple-dictionary.html"; sourceTree = ""; }; 0313F86D2AD5577400A5CFB0 /* EasydictTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EasydictTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0313F86F2AD5577400A5CFB0 /* EasydictTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EasydictTests.m; sourceTree = ""; }; @@ -414,8 +415,6 @@ 037852B529588EDE00D0E2CF /* EZCustomTableRowView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZCustomTableRowView.m; sourceTree = ""; }; 037852B7295D49F900D0E2CF /* EZTableRowView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZTableRowView.h; sourceTree = ""; }; 037852B8295D49F900D0E2CF /* EZTableRowView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZTableRowView.m; sourceTree = ""; }; - 037BEFCB2A98FDF700D0F17F /* EZBingLanguageVoice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZBingLanguageVoice.h; sourceTree = ""; }; - 037BEFCC2A98FDF700D0F17F /* EZBingLanguageVoice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZBingLanguageVoice.m; sourceTree = ""; }; 03839141292FBE120009828C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 03839142292FBE120009828C /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 03839143292FBE120009828C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -2091,8 +2090,7 @@ 6295DE302A84D82E006145F4 /* EZBingTranslateModel.m */, 6295DE322A84EF76006145F4 /* EZBingLookupModel.h */, 6295DE332A84EF76006145F4 /* EZBingLookupModel.m */, - 037BEFCB2A98FDF700D0F17F /* EZBingLanguageVoice.h */, - 037BEFCC2A98FDF700D0F17F /* EZBingLanguageVoice.m */, + 030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */, 03F639932AA6CFBB009B9914 /* EZBingConfig.h */, 03F639942AA6CFBB009B9914 /* EZBingConfig.m */, ); @@ -2586,7 +2584,6 @@ 03991158292927E000E1B06D /* EZTitlebar.m in Sources */, 03D8A65C2A433B4100D9A968 /* EZConfiguration+EZUserData.m in Sources */, 03BD282229486CF200F5891A /* EZBlueTextButton.m in Sources */, - 037BEFCD2A98FDF700D0F17F /* EZBingLanguageVoice.m in Sources */, 03BDA7C22A26DA280079D04F /* NSString+Indenter.m in Sources */, 03542A462937B4C300C34C33 /* EZBaiduTranslateResponse.m in Sources */, 0309E1F0292B4A5E00AFB76A /* NSView+EZGetViewController.m in Sources */, @@ -2722,6 +2719,7 @@ 0333FDA62A035D5700891515 /* NSString+EZChineseText.m in Sources */, 03E02A222924E77100A10260 /* EZMenuItemManager.m in Sources */, 039D119929D5E26300C93F46 /* EZAudioUtils.m in Sources */, + 030DB2612B56CC6500E27DEA /* BingLanguageVoice.swift in Sources */, EA9943F22B5358BF00EE7B97 /* LanguageExtensions.swift in Sources */, 03B0231729231FA6001C7E63 /* Snip.m in Sources */, 03BFFC6E295FE59C004E033E /* EZQueryResult+EZYoudaoDictModel.m in Sources */, diff --git a/Easydict/Feature/Service/Bing/BingLanguageVoice.swift b/Easydict/Feature/Service/Bing/BingLanguageVoice.swift new file mode 100644 index 000000000..3751e9433 --- /dev/null +++ b/Easydict/Feature/Service/Bing/BingLanguageVoice.swift @@ -0,0 +1,26 @@ +// +// BingLanguageVoice.swift +// Easydict +// +// Created by Jerry on 2023-10-14. +// Copyright © 2023 izual. All rights reserved. +// + +import Foundation + +// Docs: https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/language-support?tabs=tts + +@objc(EZBingLanguageVoice) +class BingLanguageVoice: NSObject { + @objc var lang: String // BCP-47, en-US + @objc var voiceName: String // en-US-JennyNeural + + init(lang: String, voiceName: String) { + self.lang = lang + self.voiceName = voiceName + } + + @objc class func voice(withLanguage language: String, voiceName: String) -> BingLanguageVoice { + BingLanguageVoice(lang: language, voiceName: voiceName) + } +} diff --git a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.h b/Easydict/Feature/Service/Bing/EZBingLanguageVoice.h deleted file mode 100644 index 2a193cbbd..000000000 --- a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// EZBingTTSVoice.h -// Easydict -// -// Created by tisfeng on 2023/8/25. -// Copyright © 2023 izual. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -// Docs: https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/language-support?tabs=tts - -@interface EZBingLanguageVoice : NSObject - -@property (nonatomic, copy) NSString *lang; // BCP-47, en-US -@property (nonatomic, copy) NSString *voiceName; // en-US-JennyNeural - -+ (instancetype)voiceWithLanguage:(NSString *)langauge voiceName:(NSString *)voiceName; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.m b/Easydict/Feature/Service/Bing/EZBingLanguageVoice.m deleted file mode 100644 index 4ba73a88d..000000000 --- a/Easydict/Feature/Service/Bing/EZBingLanguageVoice.m +++ /dev/null @@ -1,20 +0,0 @@ -// -// EZBingTTSVoice.m -// Easydict -// -// Created by tisfeng on 2023/8/25. -// Copyright © 2023 izual. All rights reserved. -// - -#import "EZBingLanguageVoice.h" - -@implementation EZBingLanguageVoice - -+ (instancetype)voiceWithLanguage:(NSString *)langauge voiceName:(NSString *)voiceName { - EZBingLanguageVoice *voice = [[EZBingLanguageVoice alloc] init]; - voice.lang = langauge; - voice.voiceName = voiceName; - return voice; -} - -@end diff --git a/Easydict/Feature/Service/Bing/EZBingRequest.m b/Easydict/Feature/Service/Bing/EZBingRequest.m index 3d6849b35..512e31b87 100644 --- a/Easydict/Feature/Service/Bing/EZBingRequest.m +++ b/Easydict/Feature/Service/Bing/EZBingRequest.m @@ -8,8 +8,8 @@ #import "EZBingRequest.h" #import "EZError.h" -#import "EZBingLanguageVoice.h" #import "NSString+EZRegex.h" +#import "Easydict-Swift.h" static NSString *const kAudioMIMEType = @"audio/mpeg"; static NSString *const kBingConfigKey = @"kBingConfigKey"; From 2df80b922865103b24d19a22b541b89c3218bf3e Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:05:10 +0800 Subject: [PATCH 22/72] fix: update README (#333) * fix: update README * Update README.md Co-authored-by: Tisfeng --------- Co-authored-by: Tisfeng --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce11f5f41..91ab698d1 100644 --- a/README.md +++ b/README.md @@ -734,7 +734,7 @@ Easydict 有一些应用内快捷键,方便你在使用过程中更加高效 ## 致谢 -- /是以 [Bob (GPL-3.0)](https://github.com/1xiaocainiao/Bob) 为基础开发。Easydict 在原项目上进行了许多改进和优化,很多功能和 UI 都参考了 Bob。 +- 这个项目的灵感来自 [saladict](https://github.com/crimx/ext-saladict) 和 [Bob](https://github.com/ripperhe/Bob),且初始版本是以 [Bob (GPL-3.0)](https://github.com/1xiaocainiao/Bob) 为基础开发。Easydict 在原项目上进行了许多改进和优化,很多功能和 UI 都参考了 Bob。 - 截图功能是基于 [isee15](https://github.com/isee15) 的 [Capture-Screen-For-Multi-Screens-On-Mac](https://github.com/isee15/Capture-Screen-For-Multi-Screens-On-Mac),并在此基础上进行了优化。 - 鼠标划词功能参考了 [PopClip](https://pilotmoon.com/popclip/)。 From a0cb61e70f40249853e10301b583a92e74d274e4 Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Wed, 17 Jan 2024 03:33:54 -0800 Subject: [PATCH 23/72] TTSServiceType get rid of limitation to more than macOS 13 (#336) --- .../NewApp/Configuration/Configuration.swift | 1 - Easydict/NewApp/Model/TTSServiceType.swift | 17 +++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Easydict/NewApp/Configuration/Configuration.swift b/Easydict/NewApp/Configuration/Configuration.swift index 9ecdff830..976ab72f5 100644 --- a/Easydict/NewApp/Configuration/Configuration.swift +++ b/Easydict/NewApp/Configuration/Configuration.swift @@ -33,7 +33,6 @@ extension Defaults.Keys { static let autoCopySelectedText = Key("EZConfiguration_kAutoCopySelectedTextKey", default: false) static let autoCopyFirstTranslatedText = Key("EZConfiguration_kAutoCopyFirstTranslatedTextKey", default: false) static let languageDetectOptimize = Key("EZConfiguration_kLanguageDetectOptimizeTypeKey", default: EZLanguageDetectOptimize.none) - @available(macOS 13, *) static let defaultTTSServiceType = Key("EZConfiguration_kDefaultTTSServiceTypeKey", default: TTSServiceType.youdao) static let showGoogleQuickLink = Key("EZConfiguration_kShowGoogleLinkKey", default: true) static let showEudicQuickLink = Key("EZConfiguration_kShowEudicLinkKey", default: true) diff --git a/Easydict/NewApp/Model/TTSServiceType.swift b/Easydict/NewApp/Model/TTSServiceType.swift index 6eda5cf16..c28ef5938 100644 --- a/Easydict/NewApp/Model/TTSServiceType.swift +++ b/Easydict/NewApp/Model/TTSServiceType.swift @@ -9,8 +9,16 @@ import Defaults import Foundation +enum TTSServiceType: String, CaseIterable { + case youdao + case bing + case google + case baidu + case apple +} + @available(macOS 13, *) -enum TTSServiceType: String, CaseIterable, CustomLocalizedStringResourceConvertible { +extension TTSServiceType: CustomLocalizedStringResourceConvertible { var localizedStringResource: LocalizedStringResource { switch self { case .youdao: @@ -25,15 +33,8 @@ enum TTSServiceType: String, CaseIterable, CustomLocalizedStringResourceConverti "setting.tts_service.options.apple" } } - - case youdao - case bing - case google - case baidu - case apple } -@available(macOS 13, *) extension TTSServiceType: Defaults.Serializable { // while in the future, ServiceType was deleted, then you can safely delete this struct and `bridge` struct TTSServiceTypeBridge: Defaults.Bridge { From 8edb419954d47a4ba10096ef9fdcb26a53be9a16 Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Wed, 17 Jan 2024 05:26:49 -0800 Subject: [PATCH 24/72] Refactor Setting - Service and provide service configuration view (#326) * service tab refactor * service configuration view * add comment for ServiceStringConfigurationSection * add comments for ConfigurableService * rename openAIAPI with openAIAPIKey * UI optimization * revert schema * fix: service setting in dark mode * fix: cannot move and scroll position error * introduce a view model in service tab * delete unused code * fix: do not post update notification if service enabled is not changed * fix: resizing windows animation in service view * small refactor on viewmodels * refactor: ServiceItems * reset selection after window type changes * perf: update Localizable.xcstrings --------- Co-authored-by: tisfeng Co-authored-by: phlpsong <103433299+phlpsong@users.noreply.github.com> --- Easydict.xcodeproj/project.pbxproj | 40 ++- Easydict/App/Easydict-Bridging-Header.h | 2 + Easydict/App/Localizable.xcstrings | 148 +++++++++- Easydict/Feature/Service/Ali/AliService.swift | 7 +- .../Service/Caiyun/CaiyunService.swift | 3 +- .../Feature/Service/OpenAI/EZOpenAIService.m | 1 + .../Service/Tencent/TencentService.swift | 5 +- .../NewApp/Configuration/Configuration.swift | 35 +++ .../OpenAIService+ConfigurableService.swift | 35 +++ .../Protocol/ConfigurableService.swift | 29 ++ Easydict/NewApp/View/ServiceItemView.swift | 41 --- .../NewApp/View/SettingView/SettingView.swift | 33 ++- .../ServiceConfigurationSection.swift | 84 ++++++ .../View/SettingView/Tabs/ServiceTab.swift | 262 +++++++++++------- 14 files changed, 549 insertions(+), 176 deletions(-) create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Protocol/ConfigurableService.swift delete mode 100644 Easydict/NewApp/View/ServiceItemView.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift 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) + } + } } From 51b9c810f411dcee1c6bc733542d8270d613ed82 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 19 Jan 2024 10:08:41 +0800 Subject: [PATCH 25/72] docs: update Longman dict link --- docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md | 2 +- docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md b/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md index 42a3be4c6..9cfe6d240 100644 --- a/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md +++ b/docs/How-to-use-macOS-system-dictionary-in-Easydict-en.md @@ -43,7 +43,7 @@ Longman, Collins, and Oxford are three substantial yet outstanding dictionaries. | Concise English-Chinese dictionary | Chinese-English | [GitHub](https://github.com/skywind3000/ECDICT) | https://drive.google.com/file/d/1-RoulJykOmcADGRHSmUjX2SkwiyLTHP1/view?usp=sharing | | Youdao Words Analysis | Chinese-English | [freemdict](https://downloads.freemdict.com/%E5%B0%9A%E6%9C%AA%E6%95%B4%E7%90%86/%E5%85%B1%E4%BA%AB2020.5.11/qwjs/39_%E6%9C%89%E9%81%93%E8%AF%8D%E8%AF%AD%E8%BE%A8%E6%9E%90/) | https://drive.google.com/file/d/1-HGanRhQDRR0OSMLb19or07lPwn_R0cn/view?usp=sharing | | Great Cictionary | Chinese-English | [mdict](https://mdict.org/post/dacihai/) | https://drive.google.com/file/d/1-8cBLcuA_N4PAjIMn_-d03ELv4uVrmIr/view?usp=sharing | -| Longman Dictionary of Contemporary Advanced English | Chinese-English | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1-7g-hDiwqAFtweL1qePKSRcGFJvruu97/view?usp=share_link | +| Longman Dictionary of Contemporary Advanced English | Chinese-English | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1scunXbe2JppVuKxNvn2uOidTbAZpiktk/view?usp=drive_link | | Collins Advanced English-Chinese Dictionary | Chinese-English | [《柯林斯双解》for macOS](https://placeless.net/blog/macos-dictionaries) | https://drive.google.com/file/d/1-KQmILchx71L2rFqhIZMtusIcemIlM01/view?usp=share_link | | Oxford Advanced Learner's English-Chinese Dictionary (8th Edition) | Chinese-English | [Jianshu](https://www.jianshu.com/p/e279d4a979fa) | https://drive.google.com/file/d/1-N0kiXmfTHREcBtumAmNn4sUM5poyiC7/view?usp=share_link | | Oxford Advanced Learner's English-Chinese Dictionary (8) | Chinese-English | Source unknown, I modified the css myself | https://drive.google.com/file/d/1-SigzdPPjQlycPwBHICgQSUOHpR8mMf7/view?usp=share_link | diff --git a/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md b/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md index d990d6210..5faddb4c0 100644 --- a/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md +++ b/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md @@ -43,9 +43,9 @@ Easydict 自动支持词典 App 中系统自带的词典,如牛津英汉汉英 | 简明英汉字典 | 中英 | [GitHub](https://github.com/skywind3000/ECDICT) | https://drive.google.com/file/d/1-RoulJykOmcADGRHSmUjX2SkwiyLTHP1/view?usp=sharing | | 有道词语辨析 | 中英 | [freemdict](https://downloads.freemdict.com/%E5%B0%9A%E6%9C%AA%E6%95%B4%E7%90%86/%E5%85%B1%E4%BA%AB2020.5.11/qwjs/39_%E6%9C%89%E9%81%93%E8%AF%8D%E8%AF%AD%E8%BE%A8%E6%9E%90/) | https://drive.google.com/file/d/1-HGanRhQDRR0OSMLb19or07lPwn_R0cn/view?usp=sharing | | 大辞海 | 中文 | [mdict](https://mdict.org/post/dacihai/) | https://drive.google.com/file/d/1-8cBLcuA_N4PAjIMn_-d03ELv4uVrmIr/view?usp=sharing | -| 朗文当代高级英语辞典 | 中英 | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1-7g-hDiwqAFtweL1qePKSRcGFJvruu97/view?usp=share_link | +| 朗文当代高级英语辞典 | 中英 | [v2ex](https://www.v2ex.com/t/907272) | https://drive.google.com/file/d/1scunXbe2JppVuKxNvn2uOidTbAZpiktk/view?usp=drive_link | | 柯林斯高阶英汉双解 | 中英 | [《柯林斯双解》for macOS](https://placeless.net/blog/macos-dictionaries) | https://drive.google.com/file/d/1-KQmILchx71L2rFqhIZMtusIcemIlM01/view?usp=share_link | -| 牛津高阶英汉双解词典(第8版) | 中英 | [简书](https://www.jianshu.com/p/e279d4a979fa) | https://drive.google.com/file/d/1-N0kiXmfTHREcBtumAmNn4sUM5poyiC7/view?usp=share_link | +| 牛津高阶英汉双解词典(第 8 版) | 中英 | [简书](https://www.jianshu.com/p/e279d4a979fa) | https://drive.google.com/file/d/1-N0kiXmfTHREcBtumAmNn4sUM5poyiC7/view?usp=share_link | | 牛津高阶英汉双解词典(8) | 中英 | 来源不详,我自己修改的 css | https://drive.google.com/file/d/1-SigzdPPjQlycPwBHICgQSUOHpR8mMf7/view?usp=share_link | ### 简明英汉字典 @@ -71,7 +71,7 @@ Easydict 自动支持词典 App 中系统自带的词典,如牛津英汉汉英 ![image-20231001184454574](https://raw.githubusercontent.com/tisfeng/ImageBed/main/uPic/image-20231001184454574-1696157094.png) -### 牛津高阶英汉双解词典(第8版) +### 牛津高阶英汉双解词典(第 8 版) ![image-20231001185812289](https://raw.githubusercontent.com/tisfeng/ImageBed/main/uPic/image-20231001185812289-1696157892.png) From 95ea1dba96ea83090bca03c933ed7c15714ef635 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 19 Jan 2024 18:03:38 +0800 Subject: [PATCH 26/72] fix: do not check Beta class, since it is Bool in Swift --- Easydict/Feature/Configuration/EZConfiguration.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict/Feature/Configuration/EZConfiguration.m b/Easydict/Feature/Configuration/EZConfiguration.m index 939744f59..b15c3184d 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.m +++ b/Easydict/Feature/Configuration/EZConfiguration.m @@ -665,7 +665,7 @@ - (void)setBeta:(BOOL)beta { [NSUserDefaults mm_write:stringValue forKey:EZBetaFeatureKey]; } - (BOOL)isBeta { - NSString *stringValue = [NSUserDefaults mm_readString:EZBetaFeatureKey defaultValue:@"0"]; + NSString *stringValue = [NSUserDefaults mm_read:EZBetaFeatureKey]; BOOL isBeta = [stringValue boolValue]; return isBeta; } From 25b651c7513511aef0acc9d2fda75406d376ce8a Mon Sep 17 00:00:00 2001 From: Tisfeng Date: Sat, 20 Jan 2024 12:24:50 +0800 Subject: [PATCH 27/72] perf: adjust setting UI (#341) * perf: set all setting pages to the same * perf: disable zoom button in Settings --- .../NewApp/View/SettingView/SettingView.swift | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index a7e27c991..c6446538c 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -51,15 +51,26 @@ struct SettingView: View { func resizeWindowFrame() { guard let window else { return } + + // Disable zoom button, ref: https://stackoverflow.com/a/66039864/8378840 + window.standardWindowButton(.zoomButton)?.isEnabled = false - let originalFrame = window.frame - let newSize = switch selection { - case .general, .privacy, .about: - CGSize(width: 500, height: 520) + // Keep the settings page windows all the same width to avoid strange animations. + let maxWidth = 650 + let height = switch selection { + case .general: + maxWidth case .service: - CGSize(width: 800, height: 520) + 500 + case .privacy: + 320 + case .about: + 450 } + let newSize = CGSize(width: maxWidth, height: height) + + let originalFrame = window.frame let newY = originalFrame.origin.y + originalFrame.size.height - newSize.height let newRect = NSRect(origin: CGPoint(x: originalFrame.origin.x, y: newY), size: newSize) From 1f6fdc27ce25060057d6b0aca71eb7719e521f24 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:32:43 +0800 Subject: [PATCH 28/72] add disabled app list tab (#340) * feat: add disabled app tab * fix: refactor app list issues * fix: refactor disabled app tab * fix: remove list row bg color * fix: bundle application name issue * fix: update code for review comments * fix: update app item view model * fix: remove item view model * fix: refactor disable app item view * perf: adjust add & minus bg color * perf: add a border for list * perf: adjust custom color name * perf: show minus image color according to enable state * perf: adjust disabled tab UI * fix: add disabled title horizontal padding * fix: code refactor * fix: disabled environment * fix: disable issue --------- Co-authored-by: tisfeng --- Easydict.xcodeproj/project.pbxproj | 23 +- .../App/Assets.xcassets/Colors/Contents.json | 6 + .../add_minus_bg_color.colorset/Contents.json | 38 +++ .../list_border_color.colorset/Contents.json | 38 +++ .../Contents.json | 0 Easydict/App/Easydict-Bridging-Header.h | 1 + Easydict/App/Localizable.xcstrings | 20 ++ .../Utility/Swift/Bundle/Bundle+AppInfo.swift | 23 ++ .../NewApp/View/SettingView/SettingView.swift | 9 +- .../SettingView/Tabs/DisabledAppTab.swift | 231 ++++++++++++++++++ .../View/SettingView/Tabs/ServiceTab.swift | 2 +- 11 files changed, 385 insertions(+), 6 deletions(-) create mode 100644 Easydict/App/Assets.xcassets/Colors/Contents.json create mode 100644 Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json create mode 100644 Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json rename Easydict/App/Assets.xcassets/{service_cell_highlight.colorset => Colors/service_cell_highlight_color.colorset}/Contents.json (100%) create mode 100644 Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 9bb89217d..c1c23e80f 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -229,8 +229,10 @@ 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 */; }; + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */; }; 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */; }; 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */; }; + 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8685C72B552A590022534F /* DisabledAppTab.swift */; }; 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */; }; 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */; }; 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */; }; @@ -704,8 +706,10 @@ 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 = ""; }; + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppInfo.swift"; sourceTree = ""; }; 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+DidSet.swift"; sourceTree = ""; }; 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name.swift"; sourceTree = ""; }; + 0A8685C72B552A590022534F /* DisabledAppTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppTab.swift; sourceTree = ""; }; 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapHandlerView.swift; sourceTree = ""; }; 17BCAEF32B0DFF9000A7D372 /* EZNiuTransTranslateResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslateResponse.h; sourceTree = ""; }; @@ -1821,6 +1825,7 @@ 03CF88602B137ECB0030C199 /* Swift */ = { isa = PBXGroup; children = ( + 0A2A05A42B59755F00EEA142 /* Bundle */, 0A2BA9622B4A3CBB002872A4 /* Notification */, 0A2BA95E2B49A967002872A4 /* Binding */, 03FD68BC2B1E14B500FD388E /* String */, @@ -2001,6 +2006,14 @@ path = String; sourceTree = ""; }; + 0A2A05A42B59755F00EEA142 /* Bundle */ = { + isa = PBXGroup; + children = ( + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */, + ); + path = Bundle; + sourceTree = ""; + }; 0A2BA95E2B49A967002872A4 /* Binding */ = { isa = PBXGroup; children = ( @@ -2077,6 +2090,7 @@ EAED41EA2B54A4900005FE0A /* ServiceConfiguration */, 278540332B3DE04F004E9488 /* GeneralTab.swift */, 0A057D6C2B499A000025C51D /* ServiceTab.swift */, + 0A8685C72B552A590022534F /* DisabledAppTab.swift */, 276742042B3DC230002A2C75 /* PrivacyTab.swift */, 276742052B3DC230002A2C75 /* AboutTab.swift */, ); @@ -2320,7 +2334,7 @@ buildConfigurationList = C99EEB2C2385796900FEE666 /* Build configuration list for PBXNativeTarget "Easydict" */; buildPhases = ( 21D768ECC6D11E109E6EB73A /* [CP] Check Pods Manifest.lock */, - 03B04B582B2D4B8E00E30823 /* ShellScript */, + 03B04B582B2D4B8E00E30823 /* Run Script */, C99EEB142385796700FEE666 /* Sources */, C99EEB152385796700FEE666 /* Frameworks */, C99EEB162385796700FEE666 /* Resources */, @@ -2479,16 +2493,17 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 03B04B582B2D4B8E00E30823 /* ShellScript */ = { + 03B04B582B2D4B8E00E30823 /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 2147483647; + buildActionMask = 12; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); + name = "Run Script"; outputFileListPaths = ( ); outputPaths = ( @@ -2652,6 +2667,7 @@ 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */, 03BDA7BF2A26DA280079D04F /* NSScanner+EscapedScanning.m in Sources */, 03542A4C2937B5F100C34C33 /* EZYoudaoTranslate.m in Sources */, + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */, 037852B329583F5200D0E2CF /* EZServiceCell.m in Sources */, 03247E362968158B00AFCD67 /* EZScriptExecutor.m in Sources */, 03882F8E29D95044005B5A52 /* ToastWindowController.m in Sources */, @@ -2691,6 +2707,7 @@ 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */, 039F5506294B6E29004AB940 /* EZSettingViewController.m in Sources */, 03BD281E29481C0400F5891A /* EZAudioPlayer.m in Sources */, + 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */, 03E02A2629250D1D00A10260 /* EZEventMonitor.m in Sources */, 03B0233429231FA6001C7E63 /* MMConsoleLogFormatter.m in Sources */, 037852B9295D49F900D0E2CF /* EZTableRowView.m in Sources */, diff --git a/Easydict/App/Assets.xcassets/Colors/Contents.json b/Easydict/App/Assets.xcassets/Colors/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json new file mode 100644 index 000000000..a83dbf8e7 --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE9", + "green" : "0xE9", + "red" : "0xEA" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3A", + "green" : "0x39", + "red" : "0x3B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json new file mode 100644 index 000000000..d60e3bb57 --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDE", + "green" : "0xDD", + "red" : "0xDE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0x50", + "red" : "0x50" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json similarity index 100% rename from Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json rename to Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index f98fcafec..06670f99e 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -21,6 +21,7 @@ #import "entry.h" #import "AppDelegate.h" #import "EZConfiguration.h" +#import "EZAppModel.h" #import "EZLocalStorage.h" #import "NSString+EZConvenience.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 2114e3a63..aacf0329e 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, "about" : { "comment" : "about", "localizations" : { @@ -2415,6 +2418,23 @@ } } }, + "setting.disabled.import_app_error.message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to add Application" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法添加应用" + } + } + } + }, "setting.general.advance.default_tts_service" : { "localizations" : { "en" : { diff --git a/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift new file mode 100644 index 000000000..c095084cb --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift @@ -0,0 +1,23 @@ +// +// Bundle+AppInfo.swift +// Easydict +// +// Created by phlpsong on 2024/1/18. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Bundle { + var applicationName: String { + if let displayName: String = object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { + return displayName + } else if let name: String = object(forInfoDictionaryKey: "CFBundleName") as? String { + return name + } + if let executableURL { + return executableURL.deletingLastPathComponent().lastPathComponent + } + return "" + } +} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index c6446538c..f64f70efc 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -11,6 +11,7 @@ import SwiftUI enum SettingTab: Int { case general case service + case disabled case privacy case about } @@ -30,6 +31,10 @@ struct SettingView: View { .tabItem { Label("service", systemImage: "briefcase") } .tag(SettingTab.service) + DisabledAppTab() + .tabItem { Label("disabled_app_list", systemImage: "nosign") } + .tag(SettingTab.disabled) + PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } .tag(SettingTab.privacy) @@ -51,7 +56,7 @@ struct SettingView: View { func resizeWindowFrame() { guard let window else { return } - + // Disable zoom button, ref: https://stackoverflow.com/a/66039864/8378840 window.standardWindowButton(.zoomButton)?.isEnabled = false @@ -60,7 +65,7 @@ struct SettingView: View { let height = switch selection { case .general: maxWidth - case .service: + case .service, .disabled: 500 case .privacy: 320 diff --git a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift new file mode 100644 index 000000000..a121652c7 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift @@ -0,0 +1,231 @@ +// +// DisabledAppTab.swift +// Easydict +// +// Created by phlpsong on 2024/1/15. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import SwiftUI + +private class DisabledAppViewModel: ObservableObject { + @Published var appModelList: [EZAppModel] = [] + @Published var selectedAppModels: Set = [] + @Published var isImporting = false + @Published var isShowImportErrorAlert = false + + init() { + fetchDisabledApps() + } + + func fetchDisabledApps() { + appModelList = EZLocalStorage.shared().selectTextTypeAppModelList + } + + func saveDisabledApps() { + EZLocalStorage.shared().selectTextTypeAppModelList = appModelList + } + + func removeDisabledApp() { + appModelList = appModelList.filter { !selectedAppModels.contains($0) } + saveDisabledApps() + selectedAppModels = [] + } + + func newAppURLsSelected(from urls: [URL]) { + urls.forEach { url in + let gotAccess = url.startAccessingSecurityScopedResource() + if !gotAccess { return } + appendNewDisabledApp(for: url) + url.stopAccessingSecurityScopedResource() + } + } + + func appendNewDisabledApp(for url: URL) { + guard let selectAppModel = disabledAppModel(from: url) else { return } + guard !appModelList.contains(selectAppModel) else { return } + appModelList.append(selectAppModel) + saveDisabledApps() + } + + func disabledAppModel(from url: URL) -> EZAppModel? { + let appModel = EZAppModel() + guard let bundle = Bundle(url: url) else { return nil } + appModel.appBundleID = bundle.bundleIdentifier ?? "" + appModel.triggerType = [] + return appModel + } +} + +@available(macOS 13.0, *) +struct DisabledAppTab: View { + @StateObject private var disabledAppViewModel = DisabledAppViewModel() + + var listToolbar: some View { + ListToolbar() + .fileImporter( + isPresented: $disabledAppViewModel.isImporting, + allowedContentTypes: [.application], + allowsMultipleSelection: true + ) { result in + switch result { + case let .success(urls): + disabledAppViewModel.newAppURLsSelected(from: urls) + case let .failure(error): + print("fileImporter error: \(error)") + disabledAppViewModel.isShowImportErrorAlert.toggle() + } + } + .alert(isPresented: $disabledAppViewModel.isShowImportErrorAlert) { + Alert(title: Text(""), message: Text("setting.disabled.import_app_error.message"), dismissButton: .default(Text("ok"))) + } + } + + var appListView: some View { + List(selection: $disabledAppViewModel.selectedAppModels) { + ForEach(disabledAppViewModel.appModelList, id: \.self) { app in + BlockAppItemView(with: app) + .tag(app) + } + .listRowSeparator(.hidden) + } + .listStyle(.plain) + .scrollIndicators(.never) + } + + var appListViewWithToolbar: some View { + VStack(spacing: 0) { + appListView + + listToolbar + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay(content: { + RoundedRectangle(cornerRadius: 8) + .stroke(Color("list_border_color"), lineWidth: 0.5) + }) + .padding(.horizontal, 25) + .padding(.bottom, 25) + .onTapGesture { + disabledAppViewModel.selectedAppModels = [] + } + } + + var body: some View { + VStack { + Text("disabled_title") + .padding(.horizontal) + .padding(.top, 18) + .padding(.bottom, 8) + + appListViewWithToolbar + } + .environmentObject(disabledAppViewModel) + } +} + +@available(macOS 13.0, *) +private struct ListToolbar: View { + @EnvironmentObject private var disabledAppViewModel: DisabledAppViewModel + + var body: some View { + VStack(spacing: 0) { + Divider() + HStack(spacing: 0) { + ListButton(systemName: "plus") { + disabledAppViewModel.isImporting.toggle() + } + .disabled(false) + Divider() + .padding(.vertical, 1) + ListButton(systemName: "minus") { + disabledAppViewModel.removeDisabledApp() + } + .disabled(disabledAppViewModel.selectedAppModels.isEmpty) + Spacer() + } + .padding(2) + } + .frame(height: 28) + .background(Color("add_minus_bg_color")) + } +} + +@available(macOS 13.0, *) +private struct ListButton: View { + @Environment(\.isEnabled) private var isEnabled: Bool + var systemName: String + var action: () -> Void + + var body: some View { + Button(action: { + action() + }) { + Image(systemName: systemName) + .resizable() + .scaledToFit() + .frame(width: 10, height: 10) + .padding(.horizontal, 8) + .contentShape(Rectangle()) + .foregroundStyle(isEnabled ? Color(.secondaryLabelColor) : Color(.tertiaryLabelColor)) + .font(.system(size: 14, weight: .semibold)) + } + .buttonStyle(BorderlessButtonStyle()) + } +} + +@available(macOS 13.0, *) +private struct BlockAppItemView: View { + @EnvironmentObject var disabledAppViewModel: DisabledAppViewModel + + let appIcon: NSImage + let appName: String + + init(with appModel: EZAppModel) { + let appBundleId = appModel.appBundleID + let workspace = NSWorkspace.shared + let appURL = workspace.urlForApplication(withBundleIdentifier: appBundleId) + guard let appURL else { + appIcon = .init() + appName = "" + return + } + + let appPath = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appBundleId) + guard let appPath else { + appIcon = .init() + appName = "" + return + } + appIcon = workspace.icon(forFile: appPath.path(percentEncoded: false)) + + guard let appBundle = Bundle(url: appURL) else { + appName = "" + return + } + appName = appBundle.applicationName + } + + var body: some View { + HStack(alignment: .center) { + Image(nsImage: appIcon) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + + Text(appName) + + Spacer() + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .padding(.vertical, 4) + .padding(.leading, 6) + } +} + +@available(macOS 13.0, *) +#Preview { + DisabledAppTab() +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift index 5399f9af7..83a5f6398 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift @@ -172,7 +172,7 @@ private struct ServiceItemView: View { .listRowSeparator(.hidden) .listRowInsets(.init()) .padding(10) - .listRowBackground(viewModel.selectedService == service ? Color("service_cell_highlight") : tableColor) + .listRowBackground(viewModel.selectedService == service ? Color("service_cell_highlight_color") : tableColor) .overlay { TapHandler { viewModel.selectedService = service From 5384b39a05cad484b1f383d2a8668cf36220ae0e Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Sun, 21 Jan 2024 09:37:12 +0800 Subject: [PATCH 29/72] fix: disable app list lag after add too many apps (#347) --- .../SettingView/Tabs/DisabledAppTab.swift | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift index a121652c7..7d70402ff 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift @@ -179,42 +179,20 @@ private struct ListButton: View { private struct BlockAppItemView: View { @EnvironmentObject var disabledAppViewModel: DisabledAppViewModel - let appIcon: NSImage - let appName: String + @StateObject private var appItemViewModel: AppItemViewModel init(with appModel: EZAppModel) { - let appBundleId = appModel.appBundleID - let workspace = NSWorkspace.shared - let appURL = workspace.urlForApplication(withBundleIdentifier: appBundleId) - guard let appURL else { - appIcon = .init() - appName = "" - return - } - - let appPath = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appBundleId) - guard let appPath else { - appIcon = .init() - appName = "" - return - } - appIcon = workspace.icon(forFile: appPath.path(percentEncoded: false)) - - guard let appBundle = Bundle(url: appURL) else { - appName = "" - return - } - appName = appBundle.applicationName + _appItemViewModel = StateObject(wrappedValue: AppItemViewModel(appModel: appModel)) } var body: some View { HStack(alignment: .center) { - Image(nsImage: appIcon) + Image(nsImage: appItemViewModel.appIcon) .resizable() .scaledToFit() .frame(width: 24, height: 24) - Text(appName) + Text(appItemViewModel.appName) Spacer() } @@ -225,6 +203,34 @@ private struct BlockAppItemView: View { } } +@available(macOS 13.0, *) +private class AppItemViewModel: ObservableObject { + @Published var appIcon = NSImage() + + @Published var appName = "" + + var appModel: EZAppModel + + init(appModel: EZAppModel) { + self.appModel = appModel + getAppBundleInfo() + } + + func getAppBundleInfo() { + let appBundleId = appModel.appBundleID + let workspace = NSWorkspace.shared + let appURL = workspace.urlForApplication(withBundleIdentifier: appBundleId) + guard let appURL else { return } + + let appPath = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appBundleId) + guard let appPath else { return } + appIcon = workspace.icon(forFile: appPath.path(percentEncoded: false)) + + guard let appBundle = Bundle(url: appURL) else { return } + appName = appBundle.applicationName + } +} + @available(macOS 13.0, *) #Preview { DisabledAppTab() From 18ae512d90b684208d8a1235de49ad40ed467eab Mon Sep 17 00:00:00 2001 From: NeverAgain11 Date: Tue, 23 Jan 2024 17:08:43 +0800 Subject: [PATCH 30/72] [Refactor] rewrite EZConfiguration using Swift (#335) * rewrite EZConfiguration using swift * add nullable to the error in EZScriptExecutor * use DefaultsWrapper to replace Defaults * refactor EZConfiguration+EZUserData using swift * Remove the limitation to the defaultTTSServiceType * add missing quotation * Replace EZConfiguration with Configuration * fix: observe Configuration keys when init cause dead cycle * fixed Simultaneous accesses * make the rawValue of TTSServiceType equivalent to ServiceType * fix: updateDarkMode missing parameter * fix: key changes trigger the observer multi times * fixed Simultaneous accesses * fix input error service type * remove unused code * remove unused code Co-authored-by: Tisfeng * rename files * remove #import "EZConfiguration.h" from Easydict-Bridging-Header.h * Remove unnecessary code --------- Co-authored-by: tisfeng Co-authored-by: Lava <34743145+CanglongCl@users.noreply.github.com> --- Easydict.xcodeproj/project.pbxproj | 16 +- Easydict/App/AppDelegate+EZURLScheme.m | 6 +- Easydict/App/AppDelegate.m | 2 +- Easydict/App/Easydict-Bridging-Header.h | 7 +- .../Configuration+UserData.swift | 63 ++ .../Feature/Configuration/Configuration.swift | 701 ++++++++++++++++++ .../Feature/Configuration/EZConfiguration.m | 2 +- Easydict/Feature/DarkMode/DarkModeManager.h | 5 +- Easydict/Feature/DarkMode/DarkModeManager.m | 10 +- .../Feature/EventMonitor/EZEventMonitor.m | 11 +- .../EZDisableAutoSelectTextViewController.m | 5 +- .../PerferenceWindow/EZAboutViewController.m | 5 +- .../EZPrivacyViewController.m | 9 +- .../EZSettingViewController.m | 6 +- .../Feature/Service/Apple/EZAppleService.m | 3 +- .../Feature/Service/Apple/EZScriptExecutor.h | 8 +- .../Feature/Service/Apple/EZScriptExecutor.m | 2 +- .../Service/AudioPlayer/EZAudioPlayer.m | 3 +- .../Feature/Service/Baidu/EZBaiduTranslate.m | 6 +- Easydict/Feature/Service/Bing/EZBingService.m | 3 +- .../Service/Google/EZGoogleTranslate.m | 3 +- .../Service/Language/EZLanguageManager.m | 5 +- .../Feature/Service/Model/EZDetectManager.m | 7 +- .../Feature/Service/Model/EZQueryService.m | 3 +- .../OpenAI/EZOpenAIService+EZPromptMessages.m | 5 +- .../Feature/Service/OpenAI/EZOpenAIService.m | 4 +- .../Service/Youdao/EZYoudaoTranslate.m | 2 +- .../Feature/StatusItem/EZMenuItemManager.m | 6 +- .../AppleScript/EZAppleScriptManager.m | 3 +- .../Utility/EZLinkParser/EZSchemeParser.m | 15 +- Easydict/Feature/Utility/EZLog/EZLog.m | 2 +- .../Cell/EZSelectLanguageCell.m | 15 +- .../ViewController/Model/EZQueryModel.m | 7 +- .../EZQueryMenuTextView/EZQueryMenuTextView.m | 7 +- .../View/QueryView/EZQueryView.m | 4 +- .../ViewController/View/Titlebar/EZTitlebar.m | 7 +- .../View/WordResultView/EZWebViewManager.m | 3 +- .../View/WordResultView/EZWordResultView.m | 8 +- .../EZBaseQueryViewController.m | 14 +- .../Window/WindowManager/EZLayoutManager.m | 5 +- .../Window/WindowManager/EZWindowManager.m | 29 +- ...ion.swift => Configuration+Defaults.swift} | 78 +- Easydict/NewApp/Model/TTSServiceType.swift | 10 +- .../LanguageDetectOptimizeExtensions.swift | 8 +- .../View/SettingView/Tabs/GeneralTab.swift | 2 +- 45 files changed, 1000 insertions(+), 125 deletions(-) create mode 100644 Easydict/Feature/Configuration/Configuration+UserData.swift create mode 100644 Easydict/Feature/Configuration/Configuration.swift rename Easydict/NewApp/Configuration/{Configuration.swift => Configuration+Defaults.swift} (68%) diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index c1c23e80f..43ca6f7b7 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -265,9 +265,11 @@ C4DE3D6D2AC00EB500C2B85D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = C4DE3D6C2AC00EB500C2B85D /* Localizable.xcstrings */; }; C98CAE75239F4619005F7DCA /* EasydictHelper.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = C90BE309239F38EB00ADE88B /* EasydictHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DC3C643F2B187119008EEDD8 /* ChangeFontSizeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3C643E2B187119008EEDD8 /* ChangeFontSizeView.swift */; }; + DC46DF802B4417B900DEAE3E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC46DF7F2B4417B900DEAE3E /* Configuration.swift */; }; DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */; }; DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C882B3969510055EFFC /* Appearance.swift */; }; - EA3B81F92B5254AA004C0E8B /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration.swift */; }; + DCF176F22B57CED700CA6026 /* Configuration+UserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */; }; + EA3B81F92B5254AA004C0E8B /* Configuration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */; }; EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = EA3B81FB2B52555C004C0E8B /* Defaults */; }; EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */; }; EA9943E82B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */; }; @@ -758,9 +760,11 @@ C90BE309239F38EB00ADE88B /* EasydictHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EasydictHelper.app; sourceTree = BUILT_PRODUCTS_DIR; }; C99EEB182385796700FEE666 /* Easydict-debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Easydict-debug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; DC3C643E2B187119008EEDD8 /* ChangeFontSizeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeFontSizeView.swift; sourceTree = ""; }; + DC46DF7F2B4417B900DEAE3E /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeHintView.swift; sourceTree = ""; }; DC6D9C882B3969510055EFFC /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; - EA3B81F82B5254AA004C0E8B /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+UserData.swift"; sourceTree = ""; }; + EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+Defaults.swift"; sourceTree = ""; }; EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSServiceType.swift; sourceTree = ""; }; EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageDetectOptimizeExtensions.swift; sourceTree = ""; }; EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTypeExtensions.swift; sourceTree = ""; }; @@ -1298,6 +1302,8 @@ 03542A572937CC3200C34C33 /* EZConfiguration.m */, 03D8A65A2A433B4100D9A968 /* EZConfiguration+EZUserData.h */, 03D8A65B2A433B4100D9A968 /* EZConfiguration+EZUserData.m */, + DC46DF7F2B4417B900DEAE3E /* Configuration.swift */, + DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */, DC6D9C882B3969510055EFFC /* Appearance.swift */, ); path = Configuration; @@ -2213,7 +2219,7 @@ EA3B81F72B52549B004C0E8B /* Configuration */ = { isa = PBXGroup; children = ( - EA3B81F82B5254AA004C0E8B /* Configuration.swift */, + EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */, ); path = Configuration; sourceTree = ""; @@ -2654,6 +2660,7 @@ 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */, 03B0230729231FA6001C7E63 /* EZCommonView.m in Sources */, 03B0233329231FA6001C7E63 /* MMLog.m in Sources */, + DCF176F22B57CED700CA6026 /* Configuration+UserData.swift in Sources */, 0309E1F4292BD6A100AFB76A /* EZQueryModel.m in Sources */, 03BFFC7129612E10004E033E /* NSString+EZConvenience.m in Sources */, 03BDA7BD2A26DA280079D04F /* XPMArguments_Coalescer_Internal.m in Sources */, @@ -2728,7 +2735,7 @@ 03B0233229231FA6001C7E63 /* MMLog.swift in Sources */, 03DC7C5E2A3ABE28000BF7C9 /* EZConstKey.m in Sources */, 62E2BF4C2B4082BA00E42D38 /* AliTranslateType.swift in Sources */, - EA3B81F92B5254AA004C0E8B /* Configuration.swift in Sources */, + EA3B81F92B5254AA004C0E8B /* Configuration+Defaults.swift in Sources */, 03E3E7C22ADE318800812C84 /* EZQueryMenuTextView.m in Sources */, 03B0231829231FA6001C7E63 /* SnipWindowController.m in Sources */, 03542A342936F70F00C34C33 /* EZLanguageManager.m in Sources */, @@ -2821,6 +2828,7 @@ 6220AD5B2A82812300BBFB52 /* EZBingService.m in Sources */, 039F5508294B6E29004AB940 /* EZAboutViewController.m in Sources */, 03D8A6592A42A1A300D9A968 /* EZAppModel.m in Sources */, + DC46DF802B4417B900DEAE3E /* Configuration.swift in Sources */, 036E7D7B293F4FC8002675DF /* EZOpenLinkButton.m in Sources */, EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */, 276742092B3DC230002A2C75 /* AboutTab.swift in Sources */, diff --git a/Easydict/App/AppDelegate+EZURLScheme.m b/Easydict/App/AppDelegate+EZURLScheme.m index eef2674c5..f3ce651bd 100644 --- a/Easydict/App/AppDelegate+EZURLScheme.m +++ b/Easydict/App/AppDelegate+EZURLScheme.m @@ -10,7 +10,7 @@ #import #import "EZWindowManager.h" #import "EZSchemeParser.h" -#import "EZConfiguration.h" +#import "Easydict-Swift.h" @implementation AppDelegate (EZURLScheme) @@ -70,8 +70,8 @@ - (void)registerRouters { - (void)showFloatingWindowAndAutoQueryText:(NSString *)text { EZWindowManager *windowManager = [EZWindowManager shared]; - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; - + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; + [windowManager showFloatingWindowType:windowType queryText:text.trim autoQuery:YES diff --git a/Easydict/App/AppDelegate.m b/Easydict/App/AppDelegate.m index 8aa9e5f27..6e6efd49c 100644 --- a/Easydict/App/AppDelegate.m +++ b/Easydict/App/AppDelegate.m @@ -12,7 +12,6 @@ #import "MMCrash.h" #import "EZWindowManager.h" #import "EZLanguageManager.h" -#import "EZConfiguration.h" #import "EZLog.h" #import "EZSchemeParser.h" #import "AppDelegate+EZURLScheme.h" @@ -31,6 +30,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // Capturing crash logs must be placed first. [MMCrash registerHandler]; + [EZLog setupCrashLogService]; [EZLog logAppInfo]; diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 06670f99e..7bcde5af5 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -20,12 +20,15 @@ #import "entry.h" #import "AppDelegate.h" -#import "EZConfiguration.h" + #import "EZAppModel.h" #import "EZLocalStorage.h" #import "NSString+EZConvenience.h" #import "EZWindowManager.h" #import "NSViewController+EZWindow.h" - +#import "EZLog.h" +#import "EZLanguageManager.h" +#import "DarkModeManager.h" +#import "EZScriptExecutor.h" #import "EZOpenAIService.h" diff --git a/Easydict/Feature/Configuration/Configuration+UserData.swift b/Easydict/Feature/Configuration/Configuration+UserData.swift new file mode 100644 index 000000000..0b6784aac --- /dev/null +++ b/Easydict/Feature/Configuration/Configuration+UserData.swift @@ -0,0 +1,63 @@ +// +// Configuration+UserData.swift +// Easydict +// +// Created by ljk on 2024/1/17. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Configuration { + var userDefaultsData: [String: Any] { + let userDefaults = UserDefaults.standard + + var userConfigDict = [String: Any]() + if let bundleIdentifier = Bundle.main.bundleIdentifier, let appUserDefaultsData = userDefaults.persistentDomain(forName: bundleIdentifier) { + for (key, value) in appUserDefaultsData { + if !key.hasPrefix("MASPreferences"), !(value is Data) { + userConfigDict[key] = value + } + } + } + + return userConfigDict + } + + func saveUserDefaultsDataToDownloadFolder() { + writeDictToDownloadFolder(userDefaultsData) + } + + func resetUserDefaultsData() { + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { return } + UserDefaults.standard.removePersistentDomain(forName: bundleIdentifier) + } + + func writeDictToDownloadFolder(_ dict: [String: Any]) { + let downloadPath = downloadPath + let name = ProcessInfo.processInfo.processName + let date = currentDate + let fileName = "\(name)_\(date).plist" + let plistPath = (downloadPath as NSString?)?.appendingPathComponent(fileName) + guard let path = plistPath else { return } + + let plistData = try? PropertyListSerialization.data(fromPropertyList: dict, format: .binary, options: 0) + try? plistData?.write(to: URL(fileURLWithPath: path)) + } + + var downloadPath: String? { + NSSearchPathForDirectoriesInDomains(.downloadsDirectory, .userDomainMask, true).first + } + + var currentDate: String { + let currentDate = Date() + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + + let formattedDate = formatter.string(from: currentDate) + print("Formatted Date: ", formattedDate) + + return formattedDate + } +} diff --git a/Easydict/Feature/Configuration/Configuration.swift b/Easydict/Feature/Configuration/Configuration.swift new file mode 100644 index 000000000..f888889c2 --- /dev/null +++ b/Easydict/Feature/Configuration/Configuration.swift @@ -0,0 +1,701 @@ +// +// Configuration.swift +// Easydict +// +// Created by ljk on 2024/1/2. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import Defaults +import Foundation + +@objc enum LanguageDetectOptimize: Int { + case none = 0 + case baidu = 1 + case google = 2 +} + +let kEnableBetaNewAppKey = "EZConfiguration_kEnableBetaNewAppKey" +let kHideMenuBarIconKey = "EZConfiguration_kHideMenuBarIconKey" + +@objcMembers class Configuration: NSObject { + private(set) static var shared = Configuration() + + override private init() { + super.init() + DispatchQueue.main.async { [weak self] in + guard let self else { return } + observeKeys() + } + } + + var appDelegate = NSApp.delegate as? AppDelegate + + var updater: SPUUpdater? { + appDelegate?.updaterController.updater + } + + @DefaultsWrapper(.firstLanguage) + var firstLanguage: Language + + @DefaultsWrapper(.secondLanguage) + var secondLanguage: Language + + @DefaultsWrapper(.queryFromLanguage) + var fromLanguage: Language + + @DefaultsWrapper(.queryToLanguage) + var toLanguage: Language + + @DefaultsWrapper(.autoSelectText) + var autoSelectText: Bool + + @DefaultsWrapper(.forceAutoGetSelectedText) + var forceAutoGetSelectedText: Bool + + @DefaultsWrapper(.disableEmptyCopyBeep) + var disableEmptyCopyBeep: Bool // Some apps will beep when empty copy. + + @DefaultsWrapper(.clickQuery) + var clickQuery: Bool + + @DefaultsWrapper(.launchAtStartup) + var launchAtStartup: Bool + + var automaticallyChecksForUpdates: Bool { + get { + updater?.automaticallyChecksForUpdates ?? false + } + set { + updater?.automaticallyChecksForUpdates = newValue + logSettings(["automatically_checks_for_updates": newValue]) + } + } + + @DefaultsWrapper(.hideMainWindow) + var hideMainWindow: Bool + + @DefaultsWrapper(.autoQueryOCRText) + var autoQueryOCRText: Bool + + @DefaultsWrapper(.autoQuerySelectedText) + var autoQuerySelectedText: Bool + + @DefaultsWrapper(.autoQueryPastedText) + var autoQueryPastedText: Bool + + @DefaultsWrapper(.autoPlayAudio) + var autoPlayAudio: Bool + + @DefaultsWrapper(.autoCopySelectedText) + var autoCopySelectedText: Bool + + @DefaultsWrapper(.autoCopyOCRText) + var autoCopyOCRText: Bool + + @DefaultsWrapper(.autoCopyFirstTranslatedText) + var autoCopyFirstTranslatedText: Bool + + @DefaultsWrapper(.languageDetectOptimize) + var languageDetectOptimize: LanguageDetectOptimize + + var defaultTTSServiceType: ServiceType { + get { + ServiceType(rawValue: Defaults[.defaultTTSServiceType].rawValue) + } + set { + Defaults[.defaultTTSServiceType] = TTSServiceType(rawValue: newValue.rawValue) ?? .youdao + } + } + + @DefaultsWrapper(.showGoogleQuickLink) + var showGoogleQuickLink: Bool + + @DefaultsWrapper(.showEudicQuickLink) + var showEudicQuickLink: Bool + + @DefaultsWrapper(.showAppleDictionaryQuickLink) + var showAppleDictionaryQuickLink: Bool + + @DefaultsWrapper(.hideMenuBarIcon) + var hideMenuBarIcon: Bool + + @DefaultsWrapper(.enableBetaNewApp) + var enableBetaNewApp: Bool + + @DefaultsWrapper(.fixedWindowPosition) + var fixedWindowPosition: EZShowWindowPosition + + @DefaultsWrapper(.mouseSelectTranslateWindowType) + var mouseSelectTranslateWindowType: EZWindowType + + @DefaultsWrapper(.shortcutSelectTranslateWindowType) + var shortcutSelectTranslateWindowType: EZWindowType + + @DefaultsWrapper(.adjustPopButtonOrigin) + var adjustPopButtomOrigin: Bool + + @DefaultsWrapper(.allowCrashLog) + var allowCrashLog: Bool + + @DefaultsWrapper(.allowAnalytics) + var allowAnalytics: Bool + + @DefaultsWrapper(.clearInput) + var clearInput: Bool + + var disabledAutoSelect: Bool = false + + var isRecordingSelectTextShortcutKey: Bool = false + + let fontSizes: [CGFloat] = [1, 1.1, 1.2, 1.3, 1.4] + + var fontSizeRatio: CGFloat { + fontSizes[Int(fontSizeIndex)] + } + + @DefaultsWrapper(.fontSizeOptionIndex) + var fontSizeIndex: UInt + + @DefaultsWrapper(.appearanceType) + var appearance: AppearenceType + + @DefaultsWrapper(.enableBetaFeature) + private(set) var beta: Bool + + static func destroySharedInstance() { + shared = Configuration() + } + + private func observeKeys() { + cancellables.append( + Defaults.publisher(.firstLanguage) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetFirstLanguage() + } + ) + + cancellables.append( + Defaults.publisher(.secondLanguage) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetSecondLanguage() + } + ) + + cancellables.append( + Defaults.publisher(.autoSelectText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoSelectText() + } + ) + + cancellables.append( + Defaults.publisher(.forceAutoGetSelectedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetForceAutoGetSelectedText() + } + ) + + cancellables.append( + Defaults.publisher(.disableEmptyCopyBeep) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetDisableEmptyCopyBeep() + } + ) + + cancellables.append( + Defaults.publisher(.clickQuery) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetClickQuery() + } + ) + + cancellables.append( + Defaults.publisher(.launchAtStartup) + .removeDuplicates() + .sink { [weak self] change in + self?.didSetLaunchAtStartup(change.oldValue, new: change.newValue) + } + ) + + cancellables.append( + Defaults.publisher(.hideMainWindow) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetHideMainWindow() + } + ) + + cancellables.append( + Defaults.publisher(.autoQueryOCRText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoQueryOCRText() + } + ) + + cancellables.append( + Defaults.publisher(.autoQuerySelectedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoQuerySelectedText() + } + ) + + cancellables.append( + Defaults.publisher(.autoQueryPastedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoQueryPastedText() + } + ) + + cancellables.append( + Defaults.publisher(.autoPlayAudio) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoPlayAudio() + } + ) + + cancellables.append( + Defaults.publisher(.autoCopySelectedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoCopySelectedText() + } + ) + + cancellables.append( + Defaults.publisher(.autoCopyOCRText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoCopyOCRText() + } + ) + + cancellables.append( + Defaults.publisher(.autoCopyFirstTranslatedText) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAutoCopyFirstTranslatedText() + } + ) + + cancellables.append( + Defaults.publisher(.languageDetectOptimize) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetLanguageDetectOptimize() + } + ) + + cancellables.append( + Defaults.publisher(.defaultTTSServiceType) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetDefaultTTSServiceType() + } + ) + + cancellables.append( + Defaults.publisher(.showGoogleQuickLink) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShowGoogleQuickLink() + } + ) + + cancellables.append( + Defaults.publisher(.showEudicQuickLink) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShowEudicQuickLink() + } + ) + + cancellables.append( + Defaults.publisher(.showAppleDictionaryQuickLink) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShowAppleDictionaryQuickLink() + } + ) + + cancellables.append( + Defaults.publisher(.hideMenuBarIcon) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetHideMenuBarIcon() + } + ) + + cancellables.append( + Defaults.publisher(.enableBetaNewApp) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetEnableBetaNewApp() + } + ) + + cancellables.append( + Defaults.publisher(.fixedWindowPosition) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetFixedWindowPosition() + } + ) + + cancellables.append( + Defaults.publisher(.mouseSelectTranslateWindowType) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetMouseSelectTranslateWindowType() + } + ) + + cancellables.append( + Defaults.publisher(.shortcutSelectTranslateWindowType) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetShortcutSelectTranslateWindowType() + } + ) + + cancellables.append( + Defaults.publisher(.adjustPopButtonOrigin) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAdjustPopButtomOrigin() + } + ) + + cancellables.append( + Defaults.publisher(.allowCrashLog) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAllowCrashLog() + } + ) + + cancellables.append( + Defaults.publisher(.allowAnalytics) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetAllowAnalytics() + } + ) + + cancellables.append( + Defaults.publisher(.clearInput) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetClearInput() + } + ) + + cancellables.append( + Defaults.publisher(.fontSizeOptionIndex) + .removeDuplicates() + .sink { [weak self] _ in + self?.didSetFontSizeIndex() + } + ) + + cancellables.append( + Defaults.publisher(.appearanceType) + .removeDuplicates() + .sink { [weak self] change in + let newValue = change.newValue + + self?.didSetAppearance(newValue) + } + ) + } + + var cancellables: [AnyCancellable] = [] + + func enableBetaFeaturesIfNeeded() { + guard beta else { return } + } +} + +// MARK: setter + +private extension Configuration { + func didSetFirstLanguage() { + logSettings(["first_language": firstLanguage]) + } + + func didSetSecondLanguage() { + logSettings(["second_language": secondLanguage]) + } + + func didSetAutoSelectText() { + logSettings(["auto_select_sext": autoSelectText]) + } + + func didSetForceAutoGetSelectedText() { + logSettings(["force_get_selected_text": forceAutoGetSelectedText]) + } + + func didSetDisableEmptyCopyBeep() { + logSettings(["disableEmptyCopyBeep": disableEmptyCopyBeep]) + } + + func didSetClickQuery() { + EZWindowManager.shared().updatePopButtonQueryAction() + + logSettings(["click_query": clickQuery]) + } + + func didSetLaunchAtStartup(_ old: Bool, new: Bool) { + if new != old { + updateLoginItemWithLaunchAtStartup(new) + } + + logSettings(["launch_at_startup": new]) + } + + func didSetAutomaticallyChecksForUpdates() { + logSettings(["automatically_checks_for_updates": automaticallyChecksForUpdates]) + } + + func didSetHideMainWindow() { + let windowManger = EZWindowManager.shared() + windowManger.updatePopButtonQueryAction() + if hideMainWindow { + windowManger.closeMainWindowIfNeeded() + } + + logSettings(["hide_main_window": hideMainWindow]) + } + + func didSetAutoQueryOCRText() { + logSettings(["auto_query_ocr_text": autoQueryOCRText]) + } + + func didSetAutoQuerySelectedText() { + logSettings(["auto_query_selected_text": autoQuerySelectedText]) + } + + func didSetAutoQueryPastedText() { + logSettings(["auto_query_pasted_text": autoQueryPastedText]) + } + + func didSetAutoPlayAudio() { + logSettings(["auto_play_word_audio": autoPlayAudio]) + } + + func didSetAutoCopySelectedText() { + logSettings(["auto_copy_selected_text": autoCopySelectedText]) + } + + func didSetAutoCopyOCRText() { + logSettings(["auto_copy_ocr_text": autoCopyOCRText]) + } + + func didSetAutoCopyFirstTranslatedText() { + logSettings(["auto_copy_first_translated_text": autoCopyFirstTranslatedText]) + } + + func didSetLanguageDetectOptimize() { + logSettings(["detect_optimize": languageDetectOptimize]) + } + + func didSetDefaultTTSServiceType() { + let value = defaultTTSServiceType + logSettings(["tts": value]) + } + + func didSetShowGoogleQuickLink() { + postUpdateQuickLinkButtonNotification() + + EZMenuItemManager.shared().googleItem?.isHidden = !showGoogleQuickLink + + logSettings(["show_google_link": showGoogleQuickLink]) + } + + func didSetShowEudicQuickLink() { + postUpdateQuickLinkButtonNotification() + + EZMenuItemManager.shared().eudicItem?.isHidden = !showEudicQuickLink + + logSettings(["show_eudic_link": showEudicQuickLink]) + } + + func didSetShowAppleDictionaryQuickLink() { + postUpdateQuickLinkButtonNotification() + + EZMenuItemManager.shared().appleDictionaryItem?.isHidden = !showAppleDictionaryQuickLink + + logSettings(["show_apple_dictionary_link": showAppleDictionaryQuickLink]) + } + + func didSetHideMenuBarIcon() { + if !NewAppManager.shared.enable { + hideMenuBarIcon(hidden: hideMenuBarIcon) + } + + logSettings(["hide_menu_bar_icon": hideMenuBarIcon]) + } + + func didSetEnableBetaNewApp() { + logSettings(["enable_beta_new_app": enableBetaNewApp]) + } + + func didSetFixedWindowPosition() { + logSettings(["show_fixed_window_position": fixedWindowPosition]) + } + + func didSetMouseSelectTranslateWindowType() { + logSettings(["show_mouse_window_type": mouseSelectTranslateWindowType]) + } + + func didSetShortcutSelectTranslateWindowType() { + logSettings(["show_shortcut_window_type": shortcutSelectTranslateWindowType]) + } + + func didSetAdjustPopButtomOrigin() { + logSettings(["adjust_pop_buttom_origin": adjustPopButtomOrigin]) + } + + func didSetAllowCrashLog() { + EZLog.setCrashEnabled(allowCrashLog) + logSettings(["allow_crash_log": allowCrashLog]) + } + + func didSetAllowAnalytics() { + logSettings(["allow_analytics": allowAnalytics]) + } + + func didSetClearInput() { + logSettings(["clear_input": clearInput]) + } + + func didSetFontSizeIndex() { + NotificationCenter.default.post(name: .init(ChangeFontSizeView.changeFontSizeNotificationName), object: nil) + } + + func didSetAppearance(_ appearance: AppearenceType) { + DarkModeManager.sharedManager().updateDarkMode(appearance.rawValue) + } +} + +// MARK: Window Frame + +extension Configuration { + func windowFrameWithType(_ windowType: EZWindowType) -> CGRect { + Defaults[.windorFrame(for: windowType)] + } + + func setWindowFrame(_ frame: CGRect, windowType: EZWindowType) { + Defaults[.windorFrame(for: windowType)] = frame + } +} + +// MARK: Intelligent Query Text Type of Service + +extension Configuration { + func setIntelligentQueryTextType(_ queryTextType: EZQueryTextType, serviceType: ServiceType) { + Defaults[.intelligentQueryTextType(for: serviceType)] = queryTextType + } + + func intelligentQueryTextTypeForServiceType(_ serviceType: ServiceType) -> EZQueryTextType { + Defaults[.intelligentQueryTextType(for: serviceType)] + } +} + +// MARK: Intelligent Query Text Type of Service + +extension Configuration { + func setQueryTextType(_ queryTextType: EZQueryTextType, serviceType: ServiceType) { + Defaults[.queryTextType(for: serviceType)] = queryTextType + } + + func queryTextTypeForServiceType(_ serviceType: ServiceType) -> EZQueryTextType { + Defaults[.queryTextType(for: serviceType)] + } +} + +// MARK: Intelligent Query Mode + +extension Configuration { + func setIntelligentQueryMode(_ enabled: Bool, windowType: EZWindowType) { + let key = EZConstKey.constkey("IntelligentQueryMode", windowType: windowType) + let stringValue = "\(enabled)" + UserDefaults.standard.set(stringValue, forKey: key) + + let parameters = [ + "enabled": enabled, + "window_type": windowType.rawValue, + ] as [String: Any] + + EZLog.logEvent(withName: "intelligent_query_mode", parameters: parameters) + } + + func intelligentQueryModeForWindowType(_ windowType: EZWindowType) -> Bool { + let key = EZConstKey.constkey("IntelligentQueryMode", windowType: windowType) + let defaultValue = "0" + // Turn on intelligent query mode by default in mini window. + if windowType == .mini { + return true + } + return UserDefaults.standard.string(forKey: key) ?? defaultValue == "1" + } +} + +private extension Configuration { + func postUpdateQuickLinkButtonNotification() { + let notification = Notification(name: .init("EZQuickLinkButtonUpdateNotification"), object: nil) + NotificationCenter.default.post(notification) + } + + func hideMenuBarIcon(hidden: Bool) { + if hidden { + EZMenuItemManager.shared().remove() + } else { + EZMenuItemManager.shared().setup() + } + } + + func updateLoginItemWithLaunchAtStartup(_ launchAtStartup: Bool) { + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String + let appBundlePath = Bundle.main.bundlePath + + let script = """ + tell application "System Events" to get the name of every login item + tell application "System Events" + set loginItems to every login item + repeat with aLoginItem in loginItems + if (aLoginItem's name is "\(appName ?? "")") then + delete aLoginItem + end if + end repeat + if \(launchAtStartup) then + make login item at end with properties {path:"\(appBundlePath)", hidden:false} + end if + end tell + """ + + let exeCommand = EZScriptExecutor() + exeCommand.runAppleScript(script) { result, error in + if let error { + MMLogInfo("launchAtStartup error: error: \(error)") + } else { + print("launchAtStartup result:", result) + } + } + } + + func logSettings(_ parameters: [String: Any]) { + EZLog.logEvent(withName: "settings", parameters: parameters) + } +} diff --git a/Easydict/Feature/Configuration/EZConfiguration.m b/Easydict/Feature/Configuration/EZConfiguration.m index b15c3184d..e1b40f69b 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.m +++ b/Easydict/Feature/Configuration/EZConfiguration.m @@ -455,7 +455,7 @@ - (void)setAppearance:(EZAppearenceType)appearance { [NSUserDefaults mm_write:@(appearance) forKey:kApperanceKey]; - [[DarkModeManager manager] updateDarkMode]; + [[DarkModeManager manager] updateDarkMode:appearance]; } #pragma mark - Window Frame diff --git a/Easydict/Feature/DarkMode/DarkModeManager.h b/Easydict/Feature/DarkMode/DarkModeManager.h index 722bee8a3..446803e26 100644 --- a/Easydict/Feature/DarkMode/DarkModeManager.h +++ b/Easydict/Feature/DarkMode/DarkModeManager.h @@ -10,15 +10,14 @@ NS_ASSUME_NONNULL_BEGIN - @interface DarkModeManager : NSObject @property (nonatomic, assign, readonly) BOOL systemDarkMode; -+ (instancetype)manager; ++ (instancetype)manager NS_SWIFT_NAME(sharedManager()); - (void)excuteLight:(void (^)(void))light dark:(void (^)(void))dark; -- (void)updateDarkMode; +- (void)updateDarkMode:(NSInteger)apperance; @end diff --git a/Easydict/Feature/DarkMode/DarkModeManager.m b/Easydict/Feature/DarkMode/DarkModeManager.m index af749d661..7dcdf5e59 100644 --- a/Easydict/Feature/DarkMode/DarkModeManager.m +++ b/Easydict/Feature/DarkMode/DarkModeManager.m @@ -42,14 +42,18 @@ - (void)excuteLight:(void (^)(void))light dark:(void (^)(void))dark { - (void)monitor { NSString *const darkModeNotificationName = @"AppleInterfaceThemeChangedNotification"; - [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(updateDarkMode) name:darkModeNotificationName object:nil]; + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(themeDidChange) name:darkModeNotificationName object:nil]; } -- (void)updateDarkMode { +- (void)themeDidChange { + [self updateDarkMode:Configuration.shared.appearance]; +} + +- (void)updateDarkMode:(NSInteger)apperance { BOOL isDarkMode = [self isDarkMode]; NSLog(@"%@", isDarkMode ? @"深色模式" : @"浅色模式"); - AppearenceType type = (AppearenceType)EZConfiguration.shared.appearance; + AppearenceType type = apperance; switch (type) { case AppearenceTypeDark: self.systemDarkMode = true; diff --git a/Easydict/Feature/EventMonitor/EZEventMonitor.m b/Easydict/Feature/EventMonitor/EZEventMonitor.m index 2578efd37..f3acfe5ab 100644 --- a/Easydict/Feature/EventMonitor/EZEventMonitor.m +++ b/Easydict/Feature/EventMonitor/EZEventMonitor.m @@ -17,6 +17,7 @@ #import "EZLocalStorage.h" #import "EZAppleScriptManager.h" #import "EZSystemUtility.h" +#import "Easydict-Swift.h" static CGFloat const kDismissPopButtonDelayTime = 0.1; static NSTimeInterval const kDelayGetSelectedTextTime = 0.1; @@ -300,7 +301,7 @@ - (void)getSelectedText:(BOOL)checkTextFrame completion:(void (^)(NSString *_Nul self.selectTextType = EZSelectTextTypeAccessibility; // Monitor CGEventTap must be required after using Accessibility successfully. - if (EZConfiguration.shared.autoSelectText) { + if (Configuration.shared.autoSelectText) { [self monitorCGEventTap]; } @@ -407,7 +408,7 @@ - (void)autoGetSelectedText:(BOOL)checkTextFrame { } - (BOOL)enabledAutoSelectText { - EZConfiguration *config = [EZConfiguration shared]; + Configuration *config = [Configuration shared]; BOOL enabled = config.autoSelectText && !config.disabledAutoSelect; if (!enabled) { NSLog(@"disabled autoSelectText"); @@ -437,7 +438,7 @@ - (void)getSelectedTextBySimulatedKey:(void (^)(NSString *_Nullable))completion // If playing audio, we do not silence system volume. [EZAudioUtils isPlayingAudio:^(BOOL isPlaying) { - BOOL shouldTurnOffSoundTemporarily = EZConfiguration.shared.disableEmptyCopyBeep && !isPlaying; + BOOL shouldTurnOffSoundTemporarily = Configuration.shared.disableEmptyCopyBeep && !isPlaying; // Set volume to 0 to avoid system alert. if (shouldTurnOffSoundTemporarily) { @@ -596,7 +597,7 @@ - (BOOL)isAccessibilityEnabled { /// Check if should use simulation key to get selected text. - (BOOL)shouldUseSimulatedKey:(NSString *)text error:(AXError)error { BOOL isAutoSelectQuery = self.actionType == EZActionTypeAutoSelectQuery; - BOOL allowedForceAutoGetSelectedText = [EZConfiguration.shared forceAutoGetSelectedText]; + BOOL allowedForceAutoGetSelectedText = [Configuration.shared forceAutoGetSelectedText]; NSString *easydictBundleID = [[NSBundle mainBundle] bundleIdentifier]; @@ -610,7 +611,7 @@ - (BOOL)shouldUseSimulatedKey:(NSString *)text error:(AXError)error { FIX: https://github.com/tisfeng/Easydict/issues/192#issuecomment-1797878909 */ - if (isInEasydict && EZConfiguration.shared.isRecordingSelectTextShortcutKey) { + if (isInEasydict && Configuration.shared.isRecordingSelectTextShortcutKey) { return NO; } diff --git a/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m b/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m index 477b392e3..86a5c8e56 100644 --- a/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m +++ b/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m @@ -16,6 +16,7 @@ #import "EZConfiguration.h" #import "NSImage+EZSymbolmage.h" #import "NSImage+EZResize.h" +#import "Easydict-Swift.h" static CGFloat const kMargin = 20; static CGFloat const kRowHeight = 45; @@ -254,7 +255,7 @@ - (void)selectApp { [openPanel setAllowedContentTypes:allowedTypes]; // ???: Since [auto select] will cause lag when dragging select apps, I don't know why 😰 - EZConfiguration.shared.disabledAutoSelect = YES; + Configuration.shared.disabledAutoSelect = YES; NSModalResponse result = [openPanel runModal]; if (result == NSModalResponseOK) { @@ -267,7 +268,7 @@ - (void)selectApp { [self.tableView reloadData]; } - EZConfiguration.shared.disabledAutoSelect = NO; + Configuration.shared.disabledAutoSelect = NO; } - (NSArray *)appModelsFromBundleIDDict:(NSDictionary *)appBundleIDDict { diff --git a/Easydict/Feature/PerferenceWindow/EZAboutViewController.m b/Easydict/Feature/PerferenceWindow/EZAboutViewController.m index 67fc4d277..9196c696a 100644 --- a/Easydict/Feature/PerferenceWindow/EZAboutViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZAboutViewController.m @@ -10,6 +10,7 @@ #import "EZBlueTextButton.h" #import "EZConfiguration.h" #import "EZMenuItemManager.h" +#import "Easydict-Swift.h" @interface EZAboutViewController () @@ -35,7 +36,7 @@ - (void)viewDidLoad { [super viewDidLoad]; [self setupUI]; - self.autoCheckUpdateButton.mm_isOn = EZConfiguration.shared.automaticallyChecksForUpdates; + self.autoCheckUpdateButton.mm_isOn = Configuration.shared.automaticallyChecksForUpdates; [self updateViewSize]; @@ -177,7 +178,7 @@ - (void)updateViewConstraints { #pragma mark - Actions - (void)autoCheckUpdateButtonClicked:(NSButton *)sender { - EZConfiguration.shared.automaticallyChecksForUpdates = sender.mm_isOn; + Configuration.shared.automaticallyChecksForUpdates = sender.mm_isOn; } - (void)updateLatestVersion { diff --git a/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m b/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m index e5fc7ccd3..fb00a64e3 100644 --- a/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZPrivacyViewController.m @@ -11,6 +11,7 @@ #import "EZConfiguration.h" #import "NSViewController+EZWindow.h" #import "NSImage+EZSymbolmage.h" +#import "Easydict-Swift.h" @interface EZPrivacyViewController () @@ -61,7 +62,7 @@ - (void)setupUI { action:@selector(analyticsButtonClicked:)]; [self.contentView addSubview:self.analyticsButton]; - EZConfiguration *configuration = [EZConfiguration shared]; + Configuration *configuration = [Configuration shared]; self.crashLogButton.mm_isOn = configuration.allowCrashLog; self.analyticsButton.mm_isOn = configuration.allowAnalytics; } @@ -120,16 +121,16 @@ - (void)crashLogButtonClicked:(NSButton *)sender { } else { sender.mm_isOn = YES; } - EZConfiguration.shared.allowCrashLog = sender.mm_isOn; + Configuration.shared.allowCrashLog = sender.mm_isOn; }]; } else { - EZConfiguration.shared.allowCrashLog = YES; + Configuration.shared.allowCrashLog = YES; } } - (void)analyticsButtonClicked:(NSButton *)sender { - EZConfiguration.shared.allowAnalytics = sender.mm_isOn; + Configuration.shared.allowAnalytics = sender.mm_isOn; } #pragma mark - MASPreferencesViewController diff --git a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m index 528ab13e0..38d2aec42 100644 --- a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m @@ -17,7 +17,7 @@ @interface EZSettingViewController () -@property (nonatomic, strong) EZConfiguration *config; +@property (nonatomic, strong) Configuration *config; @property (nonatomic, strong) NSTextField *selectLabel; @property (nonatomic, strong) NSTextField *inputLabel; @@ -153,7 +153,7 @@ - (void)viewDidLoad { [super viewDidLoad]; // Do view setup here. - self.config = [EZConfiguration shared]; + self.config = [Configuration shared]; [self setupUI]; @@ -165,7 +165,7 @@ - (void)viewDidLoad { // Observe selectionShortcutView.recording status. [self.KVOController observe:self.selectionShortcutView keyPath:@"recording" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZSettingViewController *settingVC, MASShortcutView *selectionShortcutView, NSDictionary *_Nonnull change) { - EZConfiguration.shared.isRecordingSelectTextShortcutKey = [change[NSKeyValueChangeNewKey] boolValue]; + Configuration.shared.isRecordingSelectTextShortcutKey = [change[NSKeyValueChangeNewKey] boolValue]; }]; } diff --git a/Easydict/Feature/Service/Apple/EZAppleService.m b/Easydict/Feature/Service/Apple/EZAppleService.m index e2a4b567c..be76679f2 100644 --- a/Easydict/Feature/Service/Apple/EZAppleService.m +++ b/Easydict/Feature/Service/Apple/EZAppleService.m @@ -16,6 +16,7 @@ #import #import "NSString+EZUtils.h" #import "EZAppleDictionary.h" +#import "Easydict-Swift.h" static NSString *const kLineBreakText = @"\n"; static NSString *const kParagraphBreakText = @"\n\n"; @@ -607,7 +608,7 @@ - (NLLanguage)detectUnkownText:(NSString *)text { NLLanguage language = NLLanguageEnglish; // 729 if ([text isNumbers]) { - EZLanguage firstLanguage = EZConfiguration.shared.firstLanguage; + EZLanguage firstLanguage = Configuration.shared.firstLanguage; language = [self appleLanguageFromLanguageEnum:firstLanguage]; } diff --git a/Easydict/Feature/Service/Apple/EZScriptExecutor.h b/Easydict/Feature/Service/Apple/EZScriptExecutor.h index a0b6259df..8ad161b9f 100644 --- a/Easydict/Feature/Service/Apple/EZScriptExecutor.h +++ b/Easydict/Feature/Service/Apple/EZScriptExecutor.h @@ -17,17 +17,17 @@ typedef void(^AppleScriptCompletionHandler)( NSString *_Nullable result, EZError /// Run translate shortcut with parameters. - (NSTask *)runTranslateShortcut:(NSDictionary *)parameters - completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; + completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; /// Run shortcut with parameters. - (NSTask *)runShortcut:(NSString *)shortcutName parameters:(NSDictionary *)parameters - completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; + completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; /// Use NSTask to run AppleScript. -- (NSTask *)runAppleScriptWithTask:(NSString *)script completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; +- (NSTask *)runAppleScriptWithTask:(NSString *)script completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; -- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError *error))completionHandler; +- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler; @end diff --git a/Easydict/Feature/Service/Apple/EZScriptExecutor.m b/Easydict/Feature/Service/Apple/EZScriptExecutor.m index 51ff38453..702f0fb08 100644 --- a/Easydict/Feature/Service/Apple/EZScriptExecutor.m +++ b/Easydict/Feature/Service/Apple/EZScriptExecutor.m @@ -94,7 +94,7 @@ - (NSTask *)runAppleScriptWithTask:(NSString *)script completionHandler:(void (^ /// Use NSAppleScript to run AppleScript, faster than NSTask. /// !!!: Note that this method may fail due to execution permissions, it will not automatically apply for permissions when I test. -- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError *error))completionHandler { +- (void)runAppleScript:(NSString *)script completionHandler:(void (^)(NSString *result, EZError * _Nullable error))completionHandler { NSAppleScript *appleScript = [[NSAppleScript alloc] initWithSource:script]; CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ diff --git a/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m b/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m index 7ef68a030..ce07b3d7a 100644 --- a/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m +++ b/Easydict/Feature/Service/AudioPlayer/EZAudioPlayer.m @@ -17,6 +17,7 @@ #import "EZServiceTypes.h" #import "EZConfiguration.h" #import +#import "Easydict-Swift.h" static NSString *const kFileExtendedAttributes = @"NSFileExtendedAttributes"; @@ -128,7 +129,7 @@ - (void)setIsPlaying:(BOOL)playing { // Note that user may change it when using, so we need to read it every time. - (EZQueryService *)defaultTTSService { - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; if (![_defaultTTSService.serviceType isEqualToString:defaultTTSServiceType]) { EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; _defaultTTSService = defaultTTSService; diff --git a/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m b/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m index 5b6137076..726add2ad 100644 --- a/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m +++ b/Easydict/Feature/Service/Baidu/EZBaiduTranslate.m @@ -13,6 +13,7 @@ #import "EZNetworkManager.h" #import "EZConfiguration.h" #import "NSString+EZRegex.h" +#import "Easydict-Swift.h" static NSString *const kBaiduTranslateURL = @"https://fanyi.baidu.com"; @@ -124,7 +125,8 @@ - (EZServiceType)serviceType { - (EZQueryTextType)queryTextType { EZQueryTextType defaultType = EZQueryTextTypeDictionary | EZQueryTextTypeSentence | EZQueryTextTypeTranslation; - EZQueryTextType type = [EZConfiguration.shared queryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared queryTextTypeForServiceType:self.serviceType]; + if (type == 0) { type = defaultType; } @@ -132,7 +134,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Bing/EZBingService.m b/Easydict/Feature/Service/Bing/EZBingService.m index db282c86b..2b08d0ed1 100644 --- a/Easydict/Feature/Service/Bing/EZBingService.m +++ b/Easydict/Feature/Service/Bing/EZBingService.m @@ -12,6 +12,7 @@ #import "EZBingLookupModel.h" #import "EZConfiguration.h" #import "NSString+EZUtils.h" +#import "Easydict-Swift.h" @interface EZBingService () @property (nonatomic, strong) EZBingRequest *request; @@ -32,7 +33,7 @@ - (instancetype)init { #pragma mark - override - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Google/EZGoogleTranslate.m b/Easydict/Feature/Service/Google/EZGoogleTranslate.m index 5c65d0e22..f2af0a9ae 100644 --- a/Easydict/Feature/Service/Google/EZGoogleTranslate.m +++ b/Easydict/Feature/Service/Google/EZGoogleTranslate.m @@ -11,6 +11,7 @@ #import #import "NSString+EZUtils.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" static NSString *const kGoogleTranslateURL = @"https://translate.google.com"; @@ -115,7 +116,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Language/EZLanguageManager.m b/Easydict/Feature/Service/Language/EZLanguageManager.m index f2e430349..6650763d5 100644 --- a/Easydict/Feature/Service/Language/EZLanguageManager.m +++ b/Easydict/Feature/Service/Language/EZLanguageManager.m @@ -9,6 +9,7 @@ #import "EZLanguageManager.h" #import "EZAppleService.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZLanguageManager () @@ -170,7 +171,7 @@ - (EZLanguage)systemSecondLanguage { } - (EZLanguage)userFirstLanguage { - EZLanguage firstLanguage = EZConfiguration.shared.firstLanguage; + EZLanguage firstLanguage = Configuration.shared.firstLanguage; if (!firstLanguage) { firstLanguage = [self systemPreferredTwoLanguages][0]; } @@ -178,7 +179,7 @@ - (EZLanguage)userFirstLanguage { } - (EZLanguage)userSecondLanguage { - EZLanguage secondLanguage = EZConfiguration.shared.secondLanguage; + EZLanguage secondLanguage = Configuration.shared.secondLanguage; if (!secondLanguage) { secondLanguage = [self systemPreferredTwoLanguages][1]; } diff --git a/Easydict/Feature/Service/Model/EZDetectManager.m b/Easydict/Feature/Service/Model/EZDetectManager.m index 40d6c9bff..a4c5575ee 100644 --- a/Easydict/Feature/Service/Model/EZDetectManager.m +++ b/Easydict/Feature/Service/Model/EZDetectManager.m @@ -12,6 +12,7 @@ #import "EZConfiguration.h" #import "EZYoudaoTranslate.h" #import "EZConfiguration+EZUserData.h" +#import "Easydict-Swift.h" @interface EZDetectManager () @@ -101,7 +102,7 @@ - (void)detectText:(NSString *)queryText completion:(void (^)(EZQueryModel *_Non [self.appleService detectText:queryText completion:^(EZLanguage appleDetectdedLanguage, NSError *_Nullable error) { NSMutableArray *preferredLanguages = [[EZLanguageManager.shared preferredLanguages] mutableCopy]; - EZLanguageDetectOptimize languageDetectOptimize = EZConfiguration.shared.languageDetectOptimize; + LanguageDetectOptimize languageDetectOptimize = Configuration.shared.languageDetectOptimize; // Add English and Chinese to the preferred language list, in general, sysytem detect English and Chinese is relatively accurate, so we don't need to use google or baidu to detect again. [preferredLanguages addObjectsFromArray:@[ @@ -111,7 +112,7 @@ - (void)detectText:(NSString *)queryText completion:(void (^)(EZQueryModel *_Non ]]; BOOL isPreferredLanguage = [preferredLanguages containsObject:appleDetectdedLanguage]; - if (isPreferredLanguage || languageDetectOptimize == EZLanguageDetectOptimizeNone) { + if (isPreferredLanguage || languageDetectOptimize == LanguageDetectOptimizeNone) { [self handleDetectedLanguage:appleDetectdedLanguage error:error completion:completion]; return; } @@ -237,7 +238,7 @@ - (void)handleOCRResult:(EZOCRResult *_Nullable)ocrResult error:(NSError *_Nulla Sometimes Apple OCR may fail, like Japanese text, but we have set Japanese as preferred language and OCR again when OCR result is empty, currently it seems work, but we do not guarantee it is always work in other languages. */ - if ([EZConfiguration.shared isBeta]) { + if (Configuration.shared.beta) { [self.youdaoService ocr:self.queryModel completion:^(EZOCRResult *_Nullable youdaoOCRResult, NSError *_Nullable youdaoOCRError) { if (!youdaoOCRError) { completion(youdaoOCRResult, nil); diff --git a/Easydict/Feature/Service/Model/EZQueryService.m b/Easydict/Feature/Service/Model/EZQueryService.m index b7cc43e1c..a2a5f7734 100644 --- a/Easydict/Feature/Service/Model/EZQueryService.m +++ b/Easydict/Feature/Service/Model/EZQueryService.m @@ -12,6 +12,7 @@ #import "NSString+EZChineseText.h" #import "NSString+EZUtils.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" #define MethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ @@ -54,7 +55,7 @@ - (BOOL)enabledAutoQuery { return NO; } - if ([EZConfiguration.shared intelligentQueryModeForWindowType:self.windowType]) { + if ([Configuration.shared intelligentQueryModeForWindowType:self.windowType]) { // We usually don't want to lookup dictionary if text word > 1. EZQueryTextType queryType = [self.queryModel.queryText queryTypeWithLanguage:self.queryModel.queryFromLanguage maxWordCount:1]; if ((queryType & self.intelligentQueryTextType) != queryType) { diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m b/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m index 2bfa712d6..2b32e8d74 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m +++ b/Easydict/Feature/Service/OpenAI/EZOpenAIService+EZPromptMessages.m @@ -9,6 +9,7 @@ #import "EZOpenAIService+EZPromptMessages.h" #import "EZConfiguration.h" #import "NSString+EZUtils.h" +#import "Easydict-Swift.h" // You are a faithful translation assistant that can only translate text and cannot interpret it, you can only return the translated text, do not show additional descriptions and annotations. @@ -84,7 +85,7 @@ - (NSArray *)translatioMessages:(NSString *)text from:(EZLanguage)sourceLanguage /// Sentence messages. - (NSArray *)sentenceMessages:(NSString *)sentence from:(EZLanguage)sourceLanguage to:(EZLanguage)targetLanguage { - NSString *answerLanguage = EZConfiguration.shared.firstLanguage; + NSString *answerLanguage = Configuration.shared.firstLanguage; self.result.to = answerLanguage; NSString *prompt = @""; @@ -274,7 +275,7 @@ - (NSArray *)translatioMessages:(NSString *)text from:(EZLanguage)sourceLanguage // V5. prompt NSString *prompt = @""; - NSString *answerLanguage = EZConfiguration.shared.firstLanguage; + NSString *answerLanguage = Configuration.shared.firstLanguage; self.result.to = answerLanguage; NSString *pronunciation = @"Pronunciation"; diff --git a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m b/Easydict/Feature/Service/OpenAI/EZOpenAIService.m index 1123049e4..f97a0ea71 100644 --- a/Easydict/Feature/Service/OpenAI/EZOpenAIService.m +++ b/Easydict/Feature/Service/OpenAI/EZOpenAIService.m @@ -57,7 +57,7 @@ - (NSString *)apiKey { // easydict://writeKeyValue?EZOpenAIAPIKey= NSString *apiKey = [[NSUserDefaults standardUserDefaults] stringForKey:EZOpenAIAPIKey] ?: @""; - if (apiKey.length == 0 && EZConfiguration.shared.isBeta) { + if (apiKey.length == 0 && Configuration.shared.beta) { apiKey = self.defaultAPIKey; } return apiKey; @@ -135,7 +135,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m b/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m index 0df3d35b1..3c9f476b4 100644 --- a/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m +++ b/Easydict/Feature/Service/Youdao/EZYoudaoTranslate.m @@ -174,7 +174,7 @@ - (EZQueryTextType)queryTextType { } - (EZQueryTextType)intelligentQueryTextType { - EZQueryTextType type = [EZConfiguration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; + EZQueryTextType type = [Configuration.shared intelligentQueryTextTypeForServiceType:self.serviceType]; return type; } diff --git a/Easydict/Feature/StatusItem/EZMenuItemManager.m b/Easydict/Feature/StatusItem/EZMenuItemManager.m index 1d47cb1d6..8d9f3b017 100644 --- a/Easydict/Feature/StatusItem/EZMenuItemManager.m +++ b/Easydict/Feature/StatusItem/EZMenuItemManager.m @@ -70,7 +70,7 @@ - (void)setup { if (self.statusItem) { return; } - if (EZConfiguration.shared.hideMenuBarIcon) { + if (Configuration.shared.hideMenuBarIcon) { return; } @@ -273,12 +273,12 @@ - (IBAction)appleDictionaryAction:(NSMenuItem *)sender { } - (IBAction)increaseFontSizeAction:(NSMenuItem *)sender { - EZConfiguration.shared.fontSizeIndex += 1; + Configuration.shared.fontSizeIndex += 1; } - (IBAction)decreaseFontSizeAction:(NSMenuItem *)sender { - EZConfiguration.shared.fontSizeIndex -= 1; + Configuration.shared.fontSizeIndex -= 1; } diff --git a/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m b/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m index a32fb0dda..6cf871203 100644 --- a/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m +++ b/Easydict/Feature/Utility/AppleScript/EZAppleScriptManager.m @@ -8,6 +8,7 @@ #import "EZAppleScriptManager.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZAppleScriptManager () @@ -225,7 +226,7 @@ - (void)checkApplicationSupportCopyAction:(NSString *)appBundleID completion:(vo NSString *edit; if (!appLanguage) { - appLanguage = EZConfiguration.shared.firstLanguage; + appLanguage = Configuration.shared.firstLanguage; } if ([appLanguage isEqualToString:EZLanguageEnglish]) { diff --git a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m index cd8ba1630..6a7a25442 100644 --- a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m +++ b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m @@ -14,6 +14,7 @@ #import "EZConfiguration+EZUserData.h" #import "EZConfiguration.h" #import "EZLocalStorage.h" +#import "Easydict-Swift.h" @implementation EZSchemeParser @@ -110,14 +111,14 @@ - (BOOL)writeKeyValues:(NSDictionary *)keyValues { NSString *value = keyValues[key]; handled = [self enabledReadWriteKey:key]; if (handled) { - EZConfiguration *config = [EZConfiguration shared]; - BOOL isBeta = config.isBeta; + Configuration *config = [Configuration shared]; + BOOL isBeta = config.beta; [[NSUserDefaults standardUserDefaults] setObject:value forKey:key]; // If enabling beta feature, setup beta features. - if (!isBeta && config.isBeta) { - [EZConfiguration.shared enableBetaFeaturesIfNeeded]; + if (!isBeta && config.beta) { + [Configuration.shared enableBetaFeaturesIfNeeded]; } } } @@ -154,15 +155,15 @@ - (BOOL)enabledReadWriteKey:(NSString *)key { - (void)resetUserDefaultsData { // easydict://resetUserDefaultsData - [EZConfiguration.shared resetUserDefaultsData]; + [Configuration.shared resetUserDefaultsData]; [EZLocalStorage destroySharedInstance]; - [EZConfiguration destroySharedInstance]; + [Configuration destroySharedInstance]; } - (void)saveUserDefaultsDataToDownloadFolder { // easydict://saveUserDefaultsDataToDownloadFolder - [EZConfiguration.shared saveUserDefaultsDataToDownloadFolder]; + [Configuration.shared saveUserDefaultsDataToDownloadFolder]; } diff --git a/Easydict/Feature/Utility/EZLog/EZLog.m b/Easydict/Feature/Utility/EZLog/EZLog.m index 81ba2860b..c17f1e7b9 100644 --- a/Easydict/Feature/Utility/EZLog/EZLog.m +++ b/Easydict/Feature/Utility/EZLog/EZLog.m @@ -47,7 +47,7 @@ + (void)setCrashEnabled:(BOOL)enabled { + (void)logEventWithName:(NSString *)name parameters:(nullable NSDictionary *)dict { // NSLog(@"log event: %@, %@", name, dict); - if (![EZConfiguration.shared allowAnalytics]) { + if (![Configuration.shared allowAnalytics]) { return; } diff --git a/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m b/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m index bc993c1df..1be65ee65 100644 --- a/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m +++ b/Easydict/Feature/ViewController/Cell/EZSelectLanguageCell.m @@ -11,6 +11,7 @@ #import "EZConfiguration.h" #import "NSColor+MyColors.h" #import "EZHoverButton.h" +#import "Easydict-Swift.h" @interface EZSelectLanguageCell () @@ -86,8 +87,8 @@ - (void)setup { mm_strongify(self); self.queryModel.userSourceLanguage = selectedLanguage; - if (![selectedLanguage isEqualToString:EZConfiguration.shared.from]) { - EZConfiguration.shared.from = selectedLanguage; + if (![selectedLanguage isEqualToString:Configuration.shared.fromLanguage]) { + Configuration.shared.fromLanguage = selectedLanguage; [self enterAction]; } }]; @@ -103,8 +104,8 @@ - (void)setup { mm_strongify(self); self.queryModel.userTargetLanguage = selectedLanguage; - if (![selectedLanguage isEqualToString:EZConfiguration.shared.to]) { - EZConfiguration.shared.to = selectedLanguage; + if (![selectedLanguage isEqualToString:Configuration.shared.toLanguage]) { + Configuration.shared.toLanguage = selectedLanguage; [self enterAction]; } }]; @@ -164,8 +165,8 @@ - (void)toggleTranslationLanguages { EZLanguage toLang = self.queryModel.userTargetLanguage; if (![fromLang isEqualToString:toLang]) { - EZConfiguration.shared.from = toLang; - EZConfiguration.shared.to = fromLang; + Configuration.shared.fromLanguage = toLang; + Configuration.shared.toLanguage = fromLang; [self.fromLanguageButton setSelectedLanguage:toLang]; [self.toLanguageButton setSelectedLanguage:fromLang]; @@ -181,7 +182,7 @@ - (void)enterAction { [self setNeedsUpdateConstraints:YES]; if (self.enterActionBlock) { - self.enterActionBlock(EZConfiguration.shared.from, EZConfiguration.shared.to); + self.enterActionBlock(Configuration.shared.fromLanguage, Configuration.shared.toLanguage); } } diff --git a/Easydict/Feature/ViewController/Model/EZQueryModel.m b/Easydict/Feature/ViewController/Model/EZQueryModel.m index c398ffd87..810385940 100644 --- a/Easydict/Feature/ViewController/Model/EZQueryModel.m +++ b/Easydict/Feature/ViewController/Model/EZQueryModel.m @@ -13,6 +13,7 @@ #import "NSString+EZSplit.h" #import "EZAppleDictionary.h" #import "NSString+EZHandleInputText.h" +#import "Easydict-Swift.h" @interface EZQueryModel () @@ -29,10 +30,10 @@ @implementation EZQueryModel - (instancetype)init { if (self = [super init]) { - [self.KVOController observe:EZConfiguration.shared keyPath:@"from" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, EZConfiguration *config, NSDictionary *_Nonnull change) { + [self.KVOController observe:Configuration.shared keyPath:@"fromLanguage" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, Configuration *config, NSDictionary *_Nonnull change) { queryModel.userSourceLanguage = change[NSKeyValueChangeNewKey]; }]; - [self.KVOController observe:EZConfiguration.shared keyPath:@"to" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, EZConfiguration *config, NSDictionary *_Nonnull change) { + [self.KVOController observe:Configuration.shared keyPath:@"toLanguage" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew block:^(EZQueryModel *queryModel, Configuration *config, NSDictionary *_Nonnull change) { queryModel.userTargetLanguage = change[NSKeyValueChangeNewKey]; }]; @@ -198,7 +199,7 @@ - (NSString *)handleInputText:(NSString *)inputText { } } - if (EZConfiguration.shared.isBeta) { + if (Configuration.shared.beta) { // Remove prefix [//,#,*,] and join texts. queryText = [queryText removeCommentBlockSymbols]; } diff --git a/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m b/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m index 6a88cde89..ab46a8a8b 100644 --- a/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m +++ b/Easydict/Feature/ViewController/View/EZQueryMenuTextView/EZQueryMenuTextView.m @@ -12,6 +12,7 @@ #import "EZCoordinateUtils.h" #import "EZLog.h" #import "NSString+EZUtils.h" +#import "Easydict-Swift.h" @interface EZQueryMenuTextView () @@ -54,10 +55,10 @@ - (void)queryInApp:(id)sender { EZWindowManager *windowManager = [EZWindowManager shared]; EZWindowType floatingWindowType = windowManager.floatingWindowType; - if (EZConfiguration.shared.mouseSelectTranslateWindowType == floatingWindowType) { - anotherWindowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + if (Configuration.shared.mouseSelectTranslateWindowType == floatingWindowType) { + anotherWindowType = Configuration.shared.shortcutSelectTranslateWindowType; } else { - anotherWindowType = EZConfiguration.shared.mouseSelectTranslateWindowType; + anotherWindowType = Configuration.shared.mouseSelectTranslateWindowType; } if (anotherWindowType != floatingWindowType) { diff --git a/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m b/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m index 964cd6483..a99a4d73a 100644 --- a/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m +++ b/Easydict/Feature/ViewController/View/QueryView/EZQueryView.m @@ -74,7 +74,7 @@ - (void)setup { textView.delegate = self; textView.textStorage.delegate = self; textView.textContainerInset = CGSizeMake(6, 8); - textView.font = [NSFont systemFontOfSize:14 * EZConfiguration.shared.fontSizeRatio]; + textView.font = [NSFont systemFontOfSize:14 * Configuration.shared.fontSizeRatio]; mm_weakify(self); [textView setPasteTextBlock:^(NSString *_Nonnull text) { @@ -87,7 +87,7 @@ - (void)setup { [[NSNotificationCenter defaultCenter] addObserverForName:ChangeFontSizeView.changeFontSizeNotificationName object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) { mm_strongify(self); - self.textView.font = [NSFont systemFontOfSize:14 * EZConfiguration.shared.fontSizeRatio]; + self.textView.font = [NSFont systemFontOfSize:14 * Configuration.shared.fontSizeRatio]; }]; // When programatically setting the text, like auto select text, or OCR text. diff --git a/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m b/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m index e6cb2d6e7..2b2c364aa 100644 --- a/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m +++ b/Easydict/Feature/ViewController/View/Titlebar/EZTitlebar.m @@ -13,6 +13,7 @@ #import "NSObject+EZDarkMode.h" #import "EZBaseQueryWindow.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZTitlebar () @@ -92,7 +93,7 @@ - (void)updateConstraints { // TODO: We should refactor it later. // Google - if (EZConfiguration.shared.showGoogleQuickLink) { + if (Configuration.shared.showGoogleQuickLink) { EZOpenLinkButton *googleButton = [[EZOpenLinkButton alloc] init]; [self addSubview:googleButton]; self.googleButton = googleButton; @@ -116,7 +117,7 @@ - (void)updateConstraints { } // Apple Dictionary - if (EZConfiguration.shared.showAppleDictionaryQuickLink) { + if (Configuration.shared.showAppleDictionaryQuickLink) { EZOpenLinkButton *appleDictButton = [[EZOpenLinkButton alloc] init]; [self addSubview:appleDictButton]; self.appleDictionaryButton = appleDictButton; @@ -140,7 +141,7 @@ - (void)updateConstraints { } // Eudic - if (EZConfiguration.shared.showEudicQuickLink) { + if (Configuration.shared.showEudicQuickLink) { EZOpenLinkButton *eudicButton = [[EZOpenLinkButton alloc] init]; // !!!: Note that some applications have multiple channel versions. Ref: https://github.com/tisfeng/Raycast-Easydict/issues/16 diff --git a/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m b/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m index aafe9b446..7c32f3634 100644 --- a/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m +++ b/Easydict/Feature/ViewController/View/WordResultView/EZWebViewManager.m @@ -8,6 +8,7 @@ #import "EZWebViewManager.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" static NSString *kObjcHandler = @"objcHandler"; static NSString *kMethod = @"method"; @@ -57,7 +58,7 @@ - (void)userContentController:(WKUserContentController *)userContentController d #pragma mark - WebView evaluateJavaScript - (void)updateAllIframe { - CGFloat fontSize = EZConfiguration.shared.fontSizeRatio; // 1.4 --> 140% + CGFloat fontSize = Configuration.shared.fontSizeRatio; // 1.4 --> 140% NSString *script = [NSString stringWithFormat:@"changeIframeBodyFontSize(%.1f); updateAllIframeStyle();", fontSize]; [self.webView evaluateJavaScript:script completionHandler:^(id _Nullable result, NSError *_Nullable error) { if (!error) { diff --git a/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m b/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m index 10268c06b..0bf1f2b72 100644 --- a/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m +++ b/Easydict/Feature/ViewController/View/WordResultView/EZWordResultView.m @@ -58,7 +58,7 @@ - (instancetype)initWithFrame:(NSRect)frame { if (self) { self.wantsLayer = YES; self.layer.cornerRadius = EZCornerRadius_8; - self.fontSizeRatio = EZConfiguration.shared.fontSizeRatio; + self.fontSizeRatio = Configuration.shared.fontSizeRatio; [self.layer excuteLight:^(CALayer *layer) { layer.backgroundColor = [NSColor ez_resultViewBgLightColor].CGColor; } dark:^(CALayer *layer) { @@ -71,7 +71,7 @@ - (instancetype)initWithFrame:(NSRect)frame { // TODO: This method is too long, need to refactor. - (void)refreshWithResult:(EZQueryResult *)result { self.result = result; - self.fontSizeRatio = EZConfiguration.shared.fontSizeRatio; + self.fontSizeRatio = Configuration.shared.fontSizeRatio; EZTranslateWordResult *wordResult = result.wordResult; self.webView = result.webViewManager.webView; @@ -826,7 +826,7 @@ - (void)refreshWithResult:(EZQueryResult *)result { language = result.to; } - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; EZQueryService *defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; [result.service.audioPlayer playTextAudio:text @@ -1324,7 +1324,7 @@ - (void)getTextWithHref:(NSString *)href completionHandler:(void (^_Nullable)(NS } - (void)updateWebViewAllIframeFontSize { - CGFloat fontSize = EZConfiguration.shared.fontSizeRatio * 100; + CGFloat fontSize = Configuration.shared.fontSizeRatio * 100; NSString *jsCode = [NSString stringWithFormat: @"var iframes = document.querySelectorAll('iframe');" diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index 58d48a8a3..c1bb5cc13 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -192,7 +192,7 @@ - (void)setupServices { NSMutableArray *services = [NSMutableArray array]; self.youdaoService = nil; - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; NSArray *allServices = [EZLocalStorage.shared allServices:self.windowType]; for (EZQueryService *service in allServices) { @@ -352,7 +352,7 @@ - (NSString *)queryText { } - (EZQueryService *)defaultTTSService { - EZServiceType defaultTTSServiceType = EZConfiguration.shared.defaultTTSServiceType; + EZServiceType defaultTTSServiceType = Configuration.shared.defaultTTSServiceType; if (![_defaultTTSService.serviceType isEqualToString:defaultTTSServiceType]) { _defaultTTSService = [EZServiceTypes.shared serviceWithType:defaultTTSServiceType]; } @@ -480,7 +480,7 @@ - (void)startOCRImage:(NSImage *)image actionType:(EZActionType)actionType { return; } - if (EZConfiguration.shared.autoCopyOCRText) { + if (Configuration.shared.autoCopyOCRText) { [inputText copyToPasteboardSafely]; } @@ -493,7 +493,7 @@ - (void)startOCRImage:(NSImage *)image actionType:(EZActionType)actionType { return; } - BOOL autoSnipTranslate = EZConfiguration.shared.autoQueryOCRText; + BOOL autoSnipTranslate = Configuration.shared.autoQueryOCRText; if (autoSnipTranslate && queryModel.autoQuery) { [self startQueryText]; } @@ -1199,7 +1199,7 @@ - (EZQueryView *)createQueryView { [queryView setPasteTextBlock:^(NSString *_Nonnull text) { mm_strongify(self); [self detectQueryText:^(NSString *_Nonnull language) { - if ([EZConfiguration.shared autoQueryPastedText]) { + if ([Configuration.shared autoQueryPastedText]) { [self startQueryWithType:EZActionTypeInputQuery]; } }]; @@ -1484,7 +1484,7 @@ - (CGFloat)miniQueryViewHeight { #pragma mark - Auto play English word - (void)autoPlayEnglishWordAudio { - if (!EZConfiguration.shared.autoPlayAudio) { + if (!Configuration.shared.autoPlayAudio) { return; } @@ -1502,7 +1502,7 @@ - (void)autoPlayEnglishWordAudio { /// Auto copy translated text. - (void)autoCopyTranslatedTextOfService:(EZQueryService *)service { - if (![EZConfiguration.shared autoCopyFirstTranslatedText]) { + if (![Configuration.shared autoCopyFirstTranslatedText]) { service.autoCopyTranslatedTextBlock = nil; return; } diff --git a/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m b/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m index 0fecef8c3..3b8490d02 100644 --- a/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m +++ b/Easydict/Feature/ViewController/Window/WindowManager/EZLayoutManager.m @@ -9,6 +9,7 @@ #import "EZLayoutManager.h" #import "EZBaseQueryWindow.h" #import "EZConfiguration.h" +#import "Easydict-Swift.h" @interface EZLayoutManager () @@ -46,7 +47,7 @@ - (void)commonInitialize { self.screen = NSScreen.mainScreen; self.minimumWindowSize = CGSizeMake(300, 70); - EZConfiguration *configuration = [EZConfiguration shared]; + Configuration *configuration = [Configuration shared]; self.miniWindowFrame = [configuration windowFrameWithType:EZWindowTypeMini]; if (CGRectEqualToRect(self.miniWindowFrame, CGRectZero)) { @@ -207,7 +208,7 @@ - (void)updateWindowFrame:(EZBaseQueryWindow *)window { break; } - [EZConfiguration.shared setWindowFrame:window.frame windowType:windowType]; + [Configuration.shared setWindowFrame:window.frame windowType:windowType]; } @end diff --git a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m index 24204c5e4..dd2c18664 100644 --- a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m +++ b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m @@ -15,6 +15,7 @@ #import "EZPreferencesWindowController.h" #import "EZConfiguration.h" #import "EZLog.h" +#import "Easydict-Swift.h" @interface EZWindowManager () @@ -154,7 +155,7 @@ - (void)updatePopButtonQueryAction { mm_weakify(self); EZButton *popButton = self.popButtonWindow.popButton; - EZConfiguration *config = [EZConfiguration shared]; + Configuration *config = [Configuration shared]; if (config.hideMainWindow) { // FIXME: Click pop button will also show preferences window. @@ -182,7 +183,7 @@ - (void)updatePopButtonQueryAction { } - (void)popButtonWindowClicked { - EZWindowType windowType = EZConfiguration.shared.mouseSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.mouseSelectTranslateWindowType; self.actionType = EZActionTypeAutoSelectQuery; [self showFloatingWindowType:windowType queryText:self.selectedText]; [self->_popButtonWindow close]; @@ -291,7 +292,7 @@ - (void)showFloatingWindowType:(EZWindowType)windowType queryText:(nullable NSSt - (void)showFloatingWindowType:(EZWindowType)windowType queryText:(nullable NSString *)queryText actionType:(EZActionType)actionType { - BOOL autoQuery = [EZConfiguration.shared autoQuerySelectedText]; + BOOL autoQuery = [Configuration.shared autoQuerySelectedText]; [self showFloatingWindowType:windowType queryText:queryText autoQuery:autoQuery actionType:actionType]; } @@ -308,7 +309,7 @@ - (void)showFloatingWindowType:(EZWindowType)windowType actionType:(EZActionType)actionType atPoint:(CGPoint)point completionHandler:(nullable void (^)(void))completionHandler { - BOOL autoQuery = [EZConfiguration.shared autoQuerySelectedText]; + BOOL autoQuery = [Configuration.shared autoQuerySelectedText]; [self showFloatingWindowType:windowType queryText:queryText autoQuery:autoQuery actionType:actionType atPoint:point completionHandler:completionHandler]; } @@ -361,7 +362,7 @@ - (void)showFloatingWindowType:(EZWindowType)windowType } // TODO: Maybe we should remove this option, it seems useless. - if ([EZConfiguration.shared autoCopySelectedText]) { + if ([Configuration.shared autoCopySelectedText]) { [queryText copyToPasteboard]; } @@ -517,7 +518,7 @@ - (CGPoint)getPopButtonWindowLocation { // NSLog(@"start point: %@", NSStringFromPoint(startLocation)); // NSLog(@"end point: %@", NSStringFromPoint(endLocation)); - if (EZConfiguration.shared.adjustPopButtomOrigin) { + if (Configuration.shared.adjustPopButtomOrigin) { // Since the pop button may cover selected text, we need to move it to the left. CGFloat horizontalOffset = 20; @@ -539,7 +540,7 @@ - (CGPoint)getPopButtonWindowLocation { - (CGPoint)getMiniWindowLocation { CGPoint position = [self getShowingMouseLocation]; - if (EZConfiguration.shared.adjustPopButtomOrigin) { + if (Configuration.shared.adjustPopButtomOrigin) { position.y = position.y - 8; } @@ -587,7 +588,7 @@ - (CGPoint)getMouseLocation:(BOOL)offsetFlag { /// !!!: This return value is top-left point. - (CGPoint)getFixedWindowLocation { CGPoint position = CGPointZero; - EZShowWindowPosition windowPosition = EZConfiguration.shared.fixedWindowPosition; + EZShowWindowPosition windowPosition = Configuration.shared.fixedWindowPosition; switch (windowPosition) { case EZShowWindowPositionRight: { position = [self getFloatingWindowInRightSideOfScreenPoint:self.fixedWindow]; @@ -651,7 +652,7 @@ - (void)saveFrontmostApplication { } - (void)showMainWindowIfNedded { - BOOL showFlag = !EZConfiguration.shared.hideMainWindow; + BOOL showFlag = !Configuration.shared.hideMainWindow; NSApplicationActivationPolicy activationPolicy = showFlag ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; [NSApp setActivationPolicy:activationPolicy]; @@ -689,7 +690,7 @@ - (void)selectTextTranslate { return; } - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; NSLog(@"selectTextTranslate windowType: %@", @(windowType)); self.eventMonitor.actionType = EZActionTypeShortcutQuery; [self.eventMonitor getSelectedText:^(NSString *_Nullable text) { @@ -715,7 +716,7 @@ - (void)snipTranslate { } // Since ocr detect may be inaccurate, sometimes need to set sourceLanguage manually, so show Fixed window. - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; EZBaseQueryWindow *window = [self windowWithType:windowType]; // Wait to close floating window if need. @@ -755,7 +756,7 @@ - (void)inputTranslate { return; } - EZWindowType windowType = EZConfiguration.shared.shortcutSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.shortcutSelectTranslateWindowType; if (self.floatingWindowType == windowType) { [self closeFloatingWindow]; @@ -763,7 +764,7 @@ - (void)inputTranslate { } NSString *queryText = nil; - if ([EZConfiguration.shared clearInput]) { + if ([Configuration.shared clearInput]) { queryText = @""; } @@ -775,7 +776,7 @@ - (void)inputTranslate { - (void)showMiniFloatingWindow { MMLogInfo(@"showMiniFloatingWindow"); - EZWindowType windowType = EZConfiguration.shared.mouseSelectTranslateWindowType; + EZWindowType windowType = Configuration.shared.mouseSelectTranslateWindowType; if (self.floatingWindowType == windowType) { [self closeFloatingWindow]; diff --git a/Easydict/NewApp/Configuration/Configuration.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift similarity index 68% rename from Easydict/NewApp/Configuration/Configuration.swift rename to Easydict/NewApp/Configuration/Configuration+Defaults.swift index 5777aa144..2cafce5ed 100644 --- a/Easydict/NewApp/Configuration/Configuration.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -1,5 +1,5 @@ // -// Configuration.swift +// Configuration+Defaults.swift // Easydict // // Created by 戴藏龙 on 2024/1/12. @@ -33,7 +33,7 @@ extension Defaults.Keys { static let autoCopyOCRText = Key("EZConfiguration_kAutoCopyOCRTextKey", default: false) static let autoCopySelectedText = Key("EZConfiguration_kAutoCopySelectedTextKey", default: false) static let autoCopyFirstTranslatedText = Key("EZConfiguration_kAutoCopyFirstTranslatedTextKey", default: false) - static let languageDetectOptimize = Key("EZConfiguration_kLanguageDetectOptimizeTypeKey", default: EZLanguageDetectOptimize.none) + static let languageDetectOptimize = Key("EZConfiguration_kLanguageDetectOptimizeTypeKey", default: LanguageDetectOptimize.none) static let defaultTTSServiceType = Key("EZConfiguration_kDefaultTTSServiceTypeKey", default: TTSServiceType.youdao) static let showGoogleQuickLink = Key("EZConfiguration_kShowGoogleLinkKey", default: true) static let showEudicQuickLink = Key("EZConfiguration_kShowEudicLinkKey", default: true) @@ -54,6 +54,80 @@ extension Defaults.Keys { static let fontSizeOptionIndex = Key("EZConfiguration_kTranslationControllerFontKey", default: 0) } +extension Defaults.Keys { + static func intelligentQueryTextType(for serviceType: ServiceType) -> Key { + let key = EZConstKey.constkey("IntelligentQueryTextType", serviceType: serviceType) + return .init(key, default: EZQueryTextType(rawValue: 7)) + } + + static func queryTextType(for serviceType: ServiceType) -> Key { + let key = EZConstKey.constkey("QueryTextType", serviceType: serviceType) + return .init(key, default: EZQueryTextType(rawValue: 0)) + } + + static func windorFrame(for windowType: EZWindowType) -> Key { + let key = "EZConfiguration_kWindowFrameKey_\(windowType)" + return .init(key, default: .zero) + } +} + +extension EZQueryTextType: Defaults.Serializable { + public static var bridge: Bridge = .init() + + public struct Bridge: Defaults.Bridge { + public func serialize(_ value: EZQueryTextType?) -> String? { + guard let value else { return "7" } + return "\(value.rawValue)" + } + + public func deserialize(_ object: String?) -> EZQueryTextType? { + guard let object else { return nil } + return EZQueryTextType(rawValue: UInt(object) ?? 7) + } + + public typealias Value = EZQueryTextType + + public typealias Serializable = String + } +} + +extension CGRect: Defaults.Serializable { + public static var bridge: Bridge = .init() + + public struct Bridge: Defaults.Bridge { + public func serialize(_ value: CGRect?) -> String? { + let value = value ?? .zero + return NSStringFromRect(value) + } + + public func deserialize(_ object: String?) -> CGRect? { + guard let object else { return nil } + return NSRectFromString(object) + } + + public typealias Value = CGRect + + public typealias Serializable = String + } +} + +@propertyWrapper +class DefaultsWrapper { + var wrappedValue: T { + get { + Defaults[key] + } set { + Defaults[key] = newValue + } + } + + init(_ key: Defaults.Key) { + self.key = key + } + + let key: Defaults.Key +} + // Service Configuration extension Defaults.Keys { // OPENAI diff --git a/Easydict/NewApp/Model/TTSServiceType.swift b/Easydict/NewApp/Model/TTSServiceType.swift index c28ef5938..a32efbe17 100644 --- a/Easydict/NewApp/Model/TTSServiceType.swift +++ b/Easydict/NewApp/Model/TTSServiceType.swift @@ -10,11 +10,11 @@ import Defaults import Foundation enum TTSServiceType: String, CaseIterable { - case youdao - case bing - case google - case baidu - case apple + case youdao = "Youdao" + case bing = "Bing" + case google = "Google" + case baidu = "Baidu" + case apple = "Apple" } @available(macOS 13, *) diff --git a/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift b/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift index 82ec281a1..2da99bad1 100644 --- a/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift +++ b/Easydict/NewApp/Utility/Extensions/LanguageDetectOptimizeExtensions.swift @@ -9,14 +9,14 @@ import Defaults import Foundation -extension EZLanguageDetectOptimize: Defaults.Serializable {} +extension LanguageDetectOptimize: Defaults.Serializable {} -extension EZLanguageDetectOptimize: CaseIterable { - public static let allCases: [EZLanguageDetectOptimize] = [.none, .baidu, .google] +extension LanguageDetectOptimize: CaseIterable { + public static let allCases: [LanguageDetectOptimize] = [.none, .baidu, .google] } @available(macOS 13, *) -extension EZLanguageDetectOptimize: CustomLocalizedStringResourceConvertible { +extension LanguageDetectOptimize: CustomLocalizedStringResourceConvertible { public var localizedStringResource: LocalizedStringResource { switch self { case .none: diff --git a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift index 852631707..84668e464 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift @@ -26,7 +26,7 @@ struct GeneralTab: View { Section { FirstAndSecondLanguageSettingView() Picker("setting.general.language.language_detect_optimize", selection: $languageDetectOptimize) { - ForEach(EZLanguageDetectOptimize.allCases, id: \.rawValue) { option in + ForEach(LanguageDetectOptimize.allCases, id: \.rawValue) { option in Text(option.localizedStringResource) .tag(option) } From 6c837d417a9dface038948aaa29686ddb1577038 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Tue, 23 Jan 2024 16:39:31 +0800 Subject: [PATCH 31/72] perf: add new advanced tab --- Easydict.xcodeproj/project.pbxproj | 4 ++ Easydict/App/Localizable.xcstrings | 19 ++++++++- .../NewApp/View/SettingView/SettingView.swift | 6 +++ .../View/SettingView/Tabs/AdvancedTab.swift | 42 +++++++++++++++++++ .../View/SettingView/Tabs/GeneralTab.swift | 15 ------- 5 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 Easydict/NewApp/View/SettingView/Tabs/AdvancedTab.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 43ca6f7b7..0feacd3fa 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ 037E006D2B3DC098006491C6 /* EZOpenAIService+EZPromptMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 03CF27FA2B3A787900E19B57 /* EZOpenAIService+EZPromptMessages.m */; }; 038030952B4106800009230C /* CocoaLumberjack in Frameworks */ = {isa = PBXBuildFile; productRef = 038030942B4106800009230C /* CocoaLumberjack */; }; 038030972B4106800009230C /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 038030962B4106800009230C /* CocoaLumberjackSwift */; }; + 03832F542B5F6BE200D0DC64 /* AdvancedTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */; }; 0383914C292FBE120009828C /* Main.strings in Resources */ = {isa = PBXBuildFile; fileRef = 03839140292FBE120009828C /* Main.strings */; }; 0383914D292FBE120009828C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03839143292FBE120009828C /* Assets.xcassets */; }; 0383914E292FBE120009828C /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03839144292FBE120009828C /* ViewController.m */; }; @@ -421,6 +422,7 @@ 037852B529588EDE00D0E2CF /* EZCustomTableRowView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZCustomTableRowView.m; sourceTree = ""; }; 037852B7295D49F900D0E2CF /* EZTableRowView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZTableRowView.h; sourceTree = ""; }; 037852B8295D49F900D0E2CF /* EZTableRowView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZTableRowView.m; sourceTree = ""; }; + 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedTab.swift; sourceTree = ""; }; 03839141292FBE120009828C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 03839142292FBE120009828C /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 03839143292FBE120009828C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -2099,6 +2101,7 @@ 0A8685C72B552A590022534F /* DisabledAppTab.swift */, 276742042B3DC230002A2C75 /* PrivacyTab.swift */, 276742052B3DC230002A2C75 /* AboutTab.swift */, + 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */, ); path = Tabs; sourceTree = ""; @@ -2831,6 +2834,7 @@ DC46DF802B4417B900DEAE3E /* Configuration.swift in Sources */, 036E7D7B293F4FC8002675DF /* EZOpenLinkButton.m in Sources */, EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */, + 03832F542B5F6BE200D0DC64 /* AdvancedTab.swift in Sources */, 276742092B3DC230002A2C75 /* AboutTab.swift in Sources */, 03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */, DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */, diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index aacf0329e..d96c42854 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -2,7 +2,14 @@ "sourceLanguage" : "en", "strings" : { "" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "about" : { "comment" : "about", @@ -37,6 +44,16 @@ } } }, + "advanced" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "高级" + } + } + } + }, "ali_translate" : { "extractionState" : "manual", "localizations" : { diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index f64f70efc..ec5c395ab 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -12,6 +12,7 @@ enum SettingTab: Int { case general case service case disabled + case advanced case privacy case about } @@ -34,6 +35,9 @@ struct SettingView: View { DisabledAppTab() .tabItem { Label("disabled_app_list", systemImage: "nosign") } .tag(SettingTab.disabled) + AdvancedTab() + .tabItem { Label("advanced", systemImage: "wrench.and.screwdriver") } + .tag(SettingTab.advanced) PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } @@ -71,6 +75,8 @@ struct SettingView: View { 320 case .about: 450 + default: + 400 } let newSize = CGSize(width: maxWidth, height: height) diff --git a/Easydict/NewApp/View/SettingView/Tabs/AdvancedTab.swift b/Easydict/NewApp/View/SettingView/Tabs/AdvancedTab.swift new file mode 100644 index 000000000..55b9da5c3 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/AdvancedTab.swift @@ -0,0 +1,42 @@ +// +// AdvancedTab.swift +// Easydict +// +// Created by tisfeng on 2024/1/23. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import SwiftUI + +@available(macOS 13, *) +struct AdvancedTab: View { + var body: some View { + Form { + Section { + Picker("setting.general.advance.default_tts_service", selection: $defaultTTSServiceType) { + ForEach(TTSServiceType.allCases, id: \.rawValue) { option in + Text(option.localizedStringResource) + .tag(option) + } + } + Toggle("setting.general.advance.enable_beta_feature", isOn: $enableBetaFeature) + Toggle(isOn: $enableBetaNewApp) { + Text("enable_beta_new_app") + } + } header: { + Text("setting.general.advance.header") + } + } + .formStyle(.grouped) + } + + @Default(.defaultTTSServiceType) private var defaultTTSServiceType + @Default(.enableBetaFeature) private var enableBetaFeature + @Default(.enableBetaNewApp) private var enableBetaNewApp +} + +@available(macOS 13, *) +#Preview { + AdvancedTab() +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift index 84668e464..6471771c1 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift @@ -139,21 +139,6 @@ struct GeneralTab: View { } header: { Text("other") } - - Section { - Picker("setting.general.advance.default_tts_service", selection: $defaultTTSServiceType) { - ForEach(TTSServiceType.allCases, id: \.rawValue) { option in - Text(option.localizedStringResource) - .tag(option) - } - } - Toggle("setting.general.advance.enable_beta_feature", isOn: $enableBetaFeature) - Toggle(isOn: $enableBetaNewApp) { - Text("enable_beta_new_app") - } - } header: { - Text("setting.general.advance.header") - } } .formStyle(.grouped) } From c673f6ca1f33a259dfb34fe4d4740a5a502b9a0d Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Wed, 24 Jan 2024 11:50:19 +0800 Subject: [PATCH 32/72] fix add disable app drag lag issue (#355) * fix: add disable app by dragging lag issue * fix: revert default disable app workaround and disableAutoSelect * fix: revert workarounds and fix by disableAutoSelect --- Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift index 7d70402ff..d45ba5a44 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift @@ -12,8 +12,13 @@ import SwiftUI private class DisabledAppViewModel: ObservableObject { @Published var appModelList: [EZAppModel] = [] @Published var selectedAppModels: Set = [] - @Published var isImporting = false @Published var isShowImportErrorAlert = false + @Published var isImporting = false { + didSet { + // https://github.com/tisfeng/Easydict/issues/346 + Configuration.shared.disabledAutoSelect = isImporting + } + } init() { fetchDisabledApps() From f0bfe0bd072a81d806c6c233157e647a28e21ab7 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Tue, 23 Jan 2024 23:25:12 +0800 Subject: [PATCH 33/72] docs: update sponsor list --- README.md | 1 + README_EN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 2f0a8a952..dfb27f0b8 100644 --- a/README.md +++ b/README.md @@ -823,6 +823,7 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! | | 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | | 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | +| 2024-01-23 | ㅤ | 5 | |

diff --git a/README_EN.md b/README_EN.md index 026b259d3..369d47ec0 100644 --- a/README_EN.md +++ b/README_EN.md @@ -820,6 +820,7 @@ If you don't want your username to be displayed in the list, please choose anony | 2024-01-09 | ㅤ Jack | 20 | 目前用过最好用的字典软件,谢谢! | | 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | | 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | +| 2024-01-23 | ㅤ | 5 | |

From 27c8eceeb36766031227e4fca82db00b7636348e Mon Sep 17 00:00:00 2001 From: tisfeng Date: Wed, 24 Jan 2024 11:50:06 +0800 Subject: [PATCH 34/72] chore: update issue templates --- .github/ISSUE_TEMPLATE/cn_bug_report_zh.yml | 20 +++++++++++++---- .github/ISSUE_TEMPLATE/cn_feature_request.yml | 8 +++++-- .github/ISSUE_TEMPLATE/en_bug_report.yml | 22 ++++++++++++++----- .github/ISSUE_TEMPLATE/en_feature_request.yml | 8 +++++-- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml index de97b7dfe..c2d0f068a 100644 --- a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml +++ b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml @@ -1,6 +1,6 @@ name: 反馈问题 description: 反馈问题 -title: "🐞 反馈问题:" +title: "🐞 反馈问题:{{请填写标题,不要留空}}" labels: ["bug"] body: @@ -22,18 +22,30 @@ body: id: description attributes: label: 问题描述 - description: 请详细描述你所遇到的问题,确保开发者能够理解、重现该问题。如果上下文信息不足,开发者无法定位,问题会被降低优先级或忽略。 + description: 请详细描述你所遇到的问题,确保开发者能够理解、重现该问题。如果上下文信息不足,导致开发者无法定位,问题会被降低优先级或忽略。 placeholder: 问题描述 validations: required: true + - type: dropdown + id: reproducible + attributes: + label: 该问题是否可以稳定重现? + multiple: false + options: + - 可重现 + - 不可重现 + validations: + required: true + - type: textarea id: reproduce attributes: label: 重现步骤 description: | - 请描述如何重现该问题。如果该问题是偶发性的,或者需要特定的操作步骤才能重现,请一定要详细提供重现步骤,否则开发者无法定位问题。 - (如果遇到一些很奇怪的问题,可以先尝试重启 Easydict,重启电脑,或卸载重装应用,看能否解决问题 🤔) + 如果该问题可重现,请一定要详细提供重现步骤,否则开发者无法定位问题。 + 如果该问题是偶发性的,可以先尝试重启 Easydict,重启电脑,或卸载重装应用,看能否解决问题 🤔 + 注意:鉴于开发者精力有限,目前只会处理可稳定重现的问题。对于不可重现的问题,当前只会简单记录下来,观察后续。 placeholder: 重现步骤 validations: required: true diff --git a/.github/ISSUE_TEMPLATE/cn_feature_request.yml b/.github/ISSUE_TEMPLATE/cn_feature_request.yml index 93baaa805..07b4f5297 100644 --- a/.github/ISSUE_TEMPLATE/cn_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/cn_feature_request.yml @@ -1,6 +1,6 @@ name: 功能建议 description: 功能建议 -title: "🚀 功能建议:" +title: "🚀 功能建议:{{请填写标题,不要留空}}" labels: ["enhancement"] body: @@ -15,6 +15,8 @@ body: required: true - label: Easydict 已升级到 [最新版本](https://github.com/tisfeng/Easydict/releases) required: true + - label: 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭** + required: true - type: textarea id: feature_description @@ -29,7 +31,9 @@ body: id: feature_usecase attributes: label: 使用场景 - description: 请描述你希望功能的使用场景,有无其他类似可供参考的 App 功能等。 + description: | + 请描述你希望功能的使用场景,有无其他类似可供参考的 App 功能等。 + 如果该功能没有明确的使用场景,或是无法被开发者理解,可能会被降低优先级或忽略,因此请务必清晰描述。 placeholder: 使用场景 validations: required: true diff --git a/.github/ISSUE_TEMPLATE/en_bug_report.yml b/.github/ISSUE_TEMPLATE/en_bug_report.yml index fc13b2380..2f87973a8 100644 --- a/.github/ISSUE_TEMPLATE/en_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/en_bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Report an issue -title: "🐞 Bug Report: " +title: "🐞 Bug Report: {{Please fill in the title, don't leave it blank}} " labels: ["bug"] body: @@ -27,14 +27,26 @@ body: validations: required: true + - type: dropdown + id: reproducible + attributes: + label: Is the issue consistently reproducible? + multiple: false + options: + - Reproducible + - Non-reproducible + validations: + required: true + - type: textarea id: reproduce attributes: - label: Reproduction steps + label: Steps to Reproduce description: | - Please describe how to reproduce the problem. If the problem is episodic or requires specific action steps to reproduce, please describe it in as much detail as possible, otherwise the developer will not be able to locate the problem. - (If the problem is episodic, try restarting Easydict, restarting the device, or uninstalling and reinstalling the app to see if that solves the problem 🤔) - placeholder: Reproduction steps + If the issue is reproducible, please provide detailed steps to reproduce it. Otherwise, the developer may not be able to locate the issue. + If the issue is intermittent, you can try restarting Easydict, restarting your computer, or uninstalling and reinstalling the application to see if the problem can be resolved 🤔 + Note: Given the limited resources of the developer, only issues that can be reliably reproduced will be addressed at this time. For non-reproducible issues, they will be simply recorded for future observation. + placeholder: Steps to Reproduce validations: required: true diff --git a/.github/ISSUE_TEMPLATE/en_feature_request.yml b/.github/ISSUE_TEMPLATE/en_feature_request.yml index 5b50e36f2..f63be4bf5 100644 --- a/.github/ISSUE_TEMPLATE/en_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/en_feature_request.yml @@ -1,6 +1,6 @@ name: Feature request description: Request a new feature -title: "🚀 Feature Request: " +title: "🚀 Feature Request: {{Please fill in the title, don't leave it blank}}" labels: ["enhancement"] body: @@ -15,6 +15,8 @@ body: required: true - label: Easydict has been upgraded to the [latest version](https://github.com/tisfeng/Easydict/releases) required: true + - label: I understand and agree to the above, and understand that the project maintainer has limited energy, **issues that do not follow the rules may be ignored or closed directly** + required: true - type: textarea id: feature_description @@ -29,7 +31,9 @@ body: id: feature_usecase attributes: label: Use case - description: Please describe the use case of the feature you're requesting, and whether there are any similar app features for reference. + description: | + Please describe the use case of the feature you're requesting, and whether there are any similar app features for reference. + If the feature has no actual usage scenarios or is not well understood by the developers it might get deprioritized or even ignored, so be sure to describe it clearly. placeholder: Use case validations: required: true From a2cc86277b9ea15d299afeb1dde8b961982b9908 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:57:11 +0800 Subject: [PATCH 35/72] fix: General tab other section title (#361) * fix: general tab other title issue * fix: other translation --- Easydict/App/Localizable.xcstrings | 29 ++++++++++++------- .../View/SettingView/Tabs/GeneralTab.swift | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index d96c42854..bb3696775 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1958,16 +1958,6 @@ } } }, - "other" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "其他" - } - } - } - }, "Parameter Error" : { "comment" : "Error description", "localizations" : { @@ -2772,6 +2762,23 @@ } } }, + "setting.general.other.header" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Other" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "其他" + } + } + } + }, "setting.general.quick_link.header" : { "localizations" : { "en" : { @@ -3443,4 +3450,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift index 6471771c1..26e427fee 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift @@ -137,7 +137,7 @@ struct GeneralTab: View { Text("hide_menu_bar_icon") } } header: { - Text("other") + Text("setting.general.other.header") } } .formStyle(.grouped) From e0fd57a072ca7b2fd274fda46118761da783adfb Mon Sep 17 00:00:00 2001 From: tisfeng Date: Wed, 24 Jan 2024 14:06:46 +0800 Subject: [PATCH 36/72] perf: change to use gearshape.2 SF image for Advanced tab --- Easydict/NewApp/View/SettingView/SettingView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index ec5c395ab..81333b8ee 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -36,7 +36,7 @@ struct SettingView: View { .tabItem { Label("disabled_app_list", systemImage: "nosign") } .tag(SettingTab.disabled) AdvancedTab() - .tabItem { Label("advanced", systemImage: "wrench.and.screwdriver") } + .tabItem { Label("advanced", systemImage: "gearshape.2") } .tag(SettingTab.advanced) PrivacyTab() From 1dcf8d6d9e3ff11a33aac73f9f7f27cf39057120 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Wed, 24 Jan 2024 18:18:51 +0800 Subject: [PATCH 37/72] fix: do not remove extra line breaks in DeepL --- Easydict/Feature/Service/DeepL/EZDeepLTranslate.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m b/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m index 44007d5ad..576c5de8c 100644 --- a/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m +++ b/Easydict/Feature/Service/DeepL/EZDeepLTranslate.m @@ -390,7 +390,6 @@ - (void)deepLTranslate:(NSString *)text from:(EZLanguage)from to:(EZLanguage)to } */ NSString *translatedText = [responseObject[@"translations"] firstObject][@"text"]; - translatedText = [translatedText.trim removeExtraLineBreaks]; NSArray *translatedTextArray = [translatedText toParagraphs]; return translatedTextArray; From a7bb7852f2088294602f3e35dab5bd38a94a88fa Mon Sep 17 00:00:00 2001 From: tisfeng Date: Thu, 25 Jan 2024 10:17:36 +0800 Subject: [PATCH 38/72] perf(ocr): do not check if new paragraph by big line spacing --- .../Feature/Service/Apple/EZAppleService.m | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Easydict/Feature/Service/Apple/EZAppleService.m b/Easydict/Feature/Service/Apple/EZAppleService.m index be76679f2..e2a681b15 100644 --- a/Easydict/Feature/Service/Apple/EZAppleService.m +++ b/Easydict/Feature/Service/Apple/EZAppleService.m @@ -24,7 +24,7 @@ static NSArray *const kAllowedCharactersInPoetryList = @[ @"《", @"》", @"〔", @"〕" ]; -static CGFloat const kParagraphLineHeightRatio = 1.2; +static CGFloat const kParagraphLineHeightRatio = 1.5; static NSInteger const kShortPoetryCharacterCountOfLine = 12; @@ -1211,18 +1211,6 @@ - (void)setupOCRResult:(EZOCRResult *)ocrResult CGFloat deltaY = prevBoundingBox.origin.y - (boundingBox.origin.y + boundingBox.size.height); CGFloat deltaX = boundingBox.origin.x - (prevBoundingBox.origin.x + prevBoundingBox.size.width); - // Note that line spacing is inaccurate, sometimes it's too small 😢 - BOOL isNewParagraph = NO; - if (deltaY > 0) { - // averageLineSpacing may too small, so deltaY should be much larger than averageLineSpacing - BOOL isBigLineSpacing = [self isBigSpacingLineOfTextObservation:textObservation - prevTextObservation:prevTextObservation - greaterThanLineHeightRatio:kParagraphLineHeightRatio]; - if (isBigLineSpacing) { - isNewParagraph = YES; - } - } - // Note that sometimes the line frames will overlap a little, then deltaY will less then 0 BOOL isNewLine = NO; if (deltaY > 0) { @@ -1251,10 +1239,9 @@ - (void)setupOCRResult:(EZOCRResult *)ocrResult if (isNeedRemoveLastDashOfText) { mergedText = [mergedText substringToIndex:mergedText.length - 1].mutableCopy; } - } else if (isNewParagraph || isNewLine) { + } else if (isNewLine) { joinedString = [self joinedStringOfTextObservation:textObservation - prevTextObservation:prevTextObservation - isNewParagraph:isNewParagraph]; + prevTextObservation:prevTextObservation]; } else { joinedString = @" "; // if the same line, just join two texts } @@ -1489,9 +1476,10 @@ - (BOOL)isPoetryOftextObservations:(NSArray *)tex /// Get joined string of text, according to its last char. - (NSString *)joinedStringOfTextObservation:(VNRecognizedTextObservation *)textObservation prevTextObservation:(VNRecognizedTextObservation *)prevTextObservation - isNewParagraph:(BOOL)isNewParagraph { +{ NSString *joinedString = @""; BOOL needLineBreak = NO; + BOOL isNewParagraph = NO; CGRect prevBoundingBox = prevTextObservation.boundingBox; CGFloat prevLineLength = prevBoundingBox.size.width; @@ -1509,7 +1497,7 @@ - (NSString *)joinedStringOfTextObservation:(VNRecognizedTextObservation *)textO BOOL hasPrevIndentation = [self hasIndentationOfTextObservation:prevTextObservation]; BOOL hasIndentation = [self hasIndentationOfTextObservation:textObservation]; - + BOOL isPrevLongText = [self isLongTextObservation:prevTextObservation isStrict:NO]; BOOL isEqualChineseText = [self isEqualChineseTextObservation:textObservation prevTextObservation:prevTextObservation]; @@ -1895,6 +1883,20 @@ - (BOOL)hasIndentationOfTextObservation:(VNRecognizedTextObservation *)textObser return hasIndentation; } +- (BOOL)hasIndentationOfTextObservation:(VNRecognizedTextObservation *)textObservation + prevTextObservation:(VNRecognizedTextObservation *)prevTextObservation { + BOOL isEqualX = [self isEqualXOfTextObservation:textObservation prevTextObservation:prevTextObservation]; + + CGFloat lineX = CGRectGetMinX(textObservation.boundingBox); + CGFloat prevLineX = CGRectGetMinX(prevTextObservation.boundingBox); + CGFloat dx = lineX - prevLineX; + + if (!isEqualX && dx < 0) { + return YES; + } + return NO; +} + - (BOOL)isEqualTextObservation:(VNRecognizedTextObservation *)textObservation prevTextObservation:(VNRecognizedTextObservation *)prevTextObservation { // 0.06 - 0.025 From b6974d1f20dc583a4212444622faca45c046103c Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:59:49 +0800 Subject: [PATCH 39/72] fix: setting crash after uninstall app in disabled app list (#364) Co-authored-by: Tisfeng --- .../EZDisableAutoSelectTextViewController.m | 14 +++++++++++++- .../View/SettingView/Tabs/DisabledAppTab.swift | 7 ++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m b/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m index 86a5c8e56..8df326346 100644 --- a/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m +++ b/Easydict/Feature/PerferenceWindow/DisableAutoSelectTextViewController/EZDisableAutoSelectTextViewController.m @@ -57,7 +57,7 @@ - (void)viewDidLoad { } - (void)setup { - self.appModelList = [[EZLocalStorage.shared selectTextTypeAppModelList] mutableCopy]; + [self setupAppModelList]; [self.titleTextField mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.inset(kMargin + 5); // ???: Why is the actual inset is 18? @@ -78,6 +78,18 @@ - (void)setup { }]; } +- (void)setupAppModelList { + self.appModelList = [[NSMutableArray alloc] init]; + NSArray *allAppModelList = [EZLocalStorage.shared selectTextTypeAppModelList]; + NSWorkspace* workspace = [NSWorkspace sharedWorkspace]; + for (EZAppModel *appModel in allAppModelList) { + NSURL *appURL = [workspace URLForApplicationWithBundleIdentifier:appModel.appBundleID]; + if (appURL) { + [self.appModelList addObject:appModel]; + } + } +} + #pragma mark - Getter && Setter diff --git a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift index d45ba5a44..ece9cc335 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift @@ -25,7 +25,12 @@ private class DisabledAppViewModel: ObservableObject { } func fetchDisabledApps() { - appModelList = EZLocalStorage.shared().selectTextTypeAppModelList + let allAppModelList = EZLocalStorage.shared().selectTextTypeAppModelList + + appModelList = allAppModelList.compactMap { appModel in + let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appModel.appBundleID) + return url == nil ? nil : appModel + } } func saveDisabledApps() { From 5d30f34604648cf67850df60a1ac025ff983f28c Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Fri, 26 Jan 2024 17:50:34 -0800 Subject: [PATCH 40/72] [Refactor] Introduce `GlobalContext` and migrate the updater controller into it (#365) * introduce global context and migrate updater controller into * refactor: mutually initialize global context * revert `@Default` in `EasydictApp` * fix: remove updaterController from Main.storyboard * style: fix typo * fix: updaterDelegate was nil * perf: set updaterHelper to private * perf: remove sparkle code from AppDelegate * perf: remove unused #import header --------- Co-authored-by: Tisfeng --- Easydict.xcodeproj/project.pbxproj | 4 ++ Easydict/App/AppDelegate.h | 3 -- Easydict/App/AppDelegate.m | 25 +---------- Easydict/App/Base.lproj/Main.storyboard | 14 +----- .../Feature/Configuration/Configuration.swift | 10 +---- .../Feature/Configuration/EZConfiguration.h | 3 ++ .../Feature/Configuration/EZConfiguration.m | 10 ++--- .../Feature/StatusItem/EZMenuItemManager.m | 7 +++ Easydict/NewApp/EasydictApp.swift | 39 +++-------------- Easydict/NewApp/Utility/GlobalContext.swift | 43 +++++++++++++++++++ Easydict/NewApp/View/MenuItemView.swift | 20 ++++----- 11 files changed, 83 insertions(+), 95 deletions(-) create mode 100644 Easydict/NewApp/Utility/GlobalContext.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 0feacd3fa..dd2932c73 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -277,6 +277,7 @@ 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 */; }; + EAE3D3502B62E9DE001EE3E3 /* GlobalContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE3D34F2B62E9DE001EE3E3 /* GlobalContext.swift */; }; EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */; }; EAED41EF2B54B1430005FE0A /* ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */; }; EAED41F22B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */; }; @@ -772,6 +773,7 @@ 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 = ""; }; + EAE3D34F2B62E9DE001EE3E3 /* GlobalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalContext.swift; sourceTree = ""; }; EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceConfigurationSection.swift; sourceTree = ""; }; EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableService.swift; sourceTree = ""; }; EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAIService+ConfigurableService.swift"; sourceTree = ""; }; @@ -2230,6 +2232,7 @@ EA9943DD2B534BAE00EE7B97 /* Utility */ = { isa = PBXGroup; children = ( + EAE3D34F2B62E9DE001EE3E3 /* GlobalContext.swift */, EAED41ED2B54B1390005FE0A /* Protocol */, EA9943E62B534D7C00EE7B97 /* Extensions */, ); @@ -2795,6 +2798,7 @@ 0320C5872B29F35700861B3D /* QueryServiceRecord.swift in Sources */, 03FC699A2B39D13A0035D2DA /* EZOpenAIChatResponse.m in Sources */, 03B022FA29231FA6001C7E63 /* EZServiceTypes.m in Sources */, + EAE3D3502B62E9DE001EE3E3 /* GlobalContext.swift in Sources */, EA9943F02B5354C400EE7B97 /* ShowWindowPositionExtensions.swift in Sources */, 03B0233129231FA6001C7E63 /* MMCrash.m in Sources */, 03B0232629231FA6001C7E63 /* NSAttributedString+MM.m in Sources */, diff --git a/Easydict/App/AppDelegate.h b/Easydict/App/AppDelegate.h index d604f4d1b..2ecaff2c6 100644 --- a/Easydict/App/AppDelegate.h +++ b/Easydict/App/AppDelegate.h @@ -7,10 +7,7 @@ // #import -#import @interface AppDelegate : NSObject -@property (weak) IBOutlet SPUStandardUpdaterController *updaterController; - @end diff --git a/Easydict/App/AppDelegate.m b/Easydict/App/AppDelegate.m index 6e6efd49c..ccec0a435 100644 --- a/Easydict/App/AppDelegate.m +++ b/Easydict/App/AppDelegate.m @@ -7,19 +7,12 @@ // #import "AppDelegate.h" -#import "EZMenuItemManager.h" #import "EZShortcut.h" #import "MMCrash.h" -#import "EZWindowManager.h" -#import "EZLanguageManager.h" -#import "EZLog.h" -#import "EZSchemeParser.h" #import "AppDelegate+EZURLScheme.h" -#import -#import #import "Easydict-Swift.h" -@interface AppDelegate () +@interface AppDelegate () @end @@ -99,20 +92,4 @@ - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)applica return NO; } -#pragma mark - SUUpdaterDelegate - -- (NSString *)feedURLStringForUpdater:(SPUUpdater *)updater { - NSString *feedURLString = @"https://raw.githubusercontent.com/tisfeng/Easydict/main/appcast.xml"; -#if DEBUG - feedURLString = @"http://localhost:8000/appcast.xml"; -#endif - return feedURLString; -} - -#pragma mark - SPUStandardUserDriverDelegate - -- (BOOL)supportsGentleScheduledUpdateReminders { - return YES; -} - @end diff --git a/Easydict/App/Base.lproj/Main.storyboard b/Easydict/App/Base.lproj/Main.storyboard index 9df59d2cb..52009ade9 100644 --- a/Easydict/App/Base.lproj/Main.storyboard +++ b/Easydict/App/Base.lproj/Main.storyboard @@ -716,18 +716,8 @@ DQ - - - - - + - - - - - - @@ -799,7 +789,7 @@ DQ - + diff --git a/Easydict/Feature/Configuration/Configuration.swift b/Easydict/Feature/Configuration/Configuration.swift index f888889c2..c78b3f2b1 100644 --- a/Easydict/Feature/Configuration/Configuration.swift +++ b/Easydict/Feature/Configuration/Configuration.swift @@ -30,12 +30,6 @@ let kHideMenuBarIconKey = "EZConfiguration_kHideMenuBarIconKey" } } - var appDelegate = NSApp.delegate as? AppDelegate - - var updater: SPUUpdater? { - appDelegate?.updaterController.updater - } - @DefaultsWrapper(.firstLanguage) var firstLanguage: Language @@ -65,10 +59,10 @@ let kHideMenuBarIconKey = "EZConfiguration_kHideMenuBarIconKey" var automaticallyChecksForUpdates: Bool { get { - updater?.automaticallyChecksForUpdates ?? false + GlobalContext.shared.updaterController.updater.automaticallyDownloadsUpdates } set { - updater?.automaticallyChecksForUpdates = newValue + GlobalContext.shared.updaterController.updater.automaticallyDownloadsUpdates = newValue logSettings(["automatically_checks_for_updates": newValue]) } } diff --git a/Easydict/Feature/Configuration/EZConfiguration.h b/Easydict/Feature/Configuration/EZConfiguration.h index 310f47031..266797563 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.h +++ b/Easydict/Feature/Configuration/EZConfiguration.h @@ -9,6 +9,7 @@ #import #import "EZLanguageManager.h" #import "EZLayoutManager.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -84,6 +85,8 @@ typedef NS_ENUM(NSUInteger, EZAppearenceType) { @property (nonatomic, assign) EZAppearenceType appearance; +@property (nonatomic, strong, readonly) SPUUpdater *updater; + + (instancetype)shared; + (void)destroySharedInstance; diff --git a/Easydict/Feature/Configuration/EZConfiguration.m b/Easydict/Feature/Configuration/EZConfiguration.m index e1b40f69b..a3104745a 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.m +++ b/Easydict/Feature/Configuration/EZConfiguration.m @@ -63,7 +63,7 @@ @interface EZConfiguration () @property (nonatomic, strong) AppDelegate *appDelegate; -@property (nonatomic, strong) SPUUpdater *updater; +@property (nonatomic, strong, readwrite) SPUUpdater *updater; @end @@ -142,12 +142,12 @@ - (BOOL)launchAtStartup { return launchAtStartup; } -- (BOOL)automaticallyChecksForUpdates { - return self.updater.automaticallyChecksForUpdates; +- (SPUUpdater *)updater { + return GlobalContext.shared.updaterController.updater; } -- (SPUUpdater *)updater { - return self.appDelegate.updaterController.updater; +- (BOOL)automaticallyChecksForUpdates { + return self.updater.automaticallyChecksForUpdates; } #pragma mark - setter diff --git a/Easydict/Feature/StatusItem/EZMenuItemManager.m b/Easydict/Feature/StatusItem/EZMenuItemManager.m index 8d9f3b017..9f40e3fc9 100644 --- a/Easydict/Feature/StatusItem/EZMenuItemManager.m +++ b/Easydict/Feature/StatusItem/EZMenuItemManager.m @@ -15,6 +15,8 @@ #import "EZRightClickDetector.h" #import "EZConfiguration.h" #import "Easydict-Swift.h" +#import +#import @interface EZMenuItemManager () @@ -181,6 +183,11 @@ - (IBAction)settingAction:(NSMenuItem *)sender { [EZPreferencesWindowController.shared show]; } +- (IBAction)checkForUpdateItem:(id)sender { + NSLog(@"checkForUpdate"); + [EZConfiguration.shared.updater checkForUpdates]; +} + - (IBAction)feedbackAction:(NSMenuItem *)sender { NSLog(@"反馈问题"); NSString *issueURL = [NSString stringWithFormat:@"%@/issues", EZGithubRepoEasydictURL]; diff --git a/Easydict/NewApp/EasydictApp.swift b/Easydict/NewApp/EasydictApp.swift index c81ba83f4..bb3e1be41 100644 --- a/Easydict/NewApp/EasydictApp.swift +++ b/Easydict/NewApp/EasydictApp.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import Sparkle import SwiftUI @@ -21,28 +22,14 @@ enum EasydictCmpatibilityEntry { } } -class SPUUpdaterHelper: NSObject, SPUUpdaterDelegate { - func feedURLString(for _: SPUUpdater) -> String? { - var feedURLString = "https://raw.githubusercontent.com/tisfeng/Easydict/main/appcast.xml" - #if DEBUG - feedURLString = "http://localhost:8000/appcast.xml" - #endif - return feedURLString - } -} - -class SPUUserDriverHelper: NSObject, SPUStandardUserDriverDelegate { - var supportsGentleScheduledUpdateReminders: Bool { - true - } -} - struct EasydictApp: App { @NSApplicationDelegateAdaptor - var delegate: AppDelegate + private var delegate: AppDelegate - @AppStorage(kHideMenuBarIconKey) - private var hideMenuBar = false + // Use `@Default` will cause a purple warning and continuously call `set` of it. + // I'm not sure why. Just leave `AppStorage` here. + @AppStorage(Defaults.Key.hideMenuBarIcon.name) + private var hideMenuBar = Defaults.Key.hideMenuBarIcon.defaultValue private var menuBarImage: String { #if DEBUG @@ -52,22 +39,10 @@ struct EasydictApp: App { #endif } - let userDriverHelper = SPUUserDriverHelper() - let upadterHelper = SPUUpdaterHelper() - - private let updaterController: SPUStandardUpdaterController - - init() { - // 参考 https://sparkle-project.org/documentation/programmatic-setup/ - // If you want to start the updater manually, pass false to startingUpdater and call .startUpdater() later - // This is where you can also pass an updater delegate if you need one - updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: upadterHelper, userDriverDelegate: userDriverHelper) - } - var body: some Scene { if #available(macOS 13, *) { MenuBarExtra(isInserted: $hideMenuBar.toggledValue) { - MenuItemView(updater: updaterController.updater) + MenuItemView() } label: { Label { Text("Easydict") diff --git a/Easydict/NewApp/Utility/GlobalContext.swift b/Easydict/NewApp/Utility/GlobalContext.swift new file mode 100644 index 000000000..557c22ae1 --- /dev/null +++ b/Easydict/NewApp/Utility/GlobalContext.swift @@ -0,0 +1,43 @@ +// +// GlobalContext.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/25. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import Sparkle + +@objcMembers +class GlobalContext: NSObject { + static let shared = GlobalContext() + + let updaterController: SPUStandardUpdaterController + + private let updaterHelper: SPUUpdaterHelper + private let userDriverHelper: SPUUserDriverHelper + + override init() { + updaterHelper = SPUUpdaterHelper() + userDriverHelper = SPUUserDriverHelper() + + updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: updaterHelper, userDriverDelegate: userDriverHelper) + } + + class SPUUpdaterHelper: NSObject, SPUUpdaterDelegate { + func feedURLString(for _: SPUUpdater) -> String? { + var feedURLString = "https://raw.githubusercontent.com/tisfeng/Easydict/main/appcast.xml" + #if DEBUG + feedURLString = "http://localhost:8000/appcast.xml" + #endif + return feedURLString + } + } + + class SPUUserDriverHelper: NSObject, SPUStandardUserDriverDelegate { + var supportsGentleScheduledUpdateReminders: Bool { + true + } + } +} diff --git a/Easydict/NewApp/View/MenuItemView.swift b/Easydict/NewApp/View/MenuItemView.swift index 5f25919b1..65a264c59 100644 --- a/Easydict/NewApp/View/MenuItemView.swift +++ b/Easydict/NewApp/View/MenuItemView.swift @@ -13,21 +13,19 @@ import ZipArchive @available(macOS 13, *) final class MenuItemStore: ObservableObject { @Published var canCheckForUpdates = false - var updater: SPUUpdater - init(updater: SPUUpdater) { - self.updater = updater - self.updater.publisher(for: \.canCheckForUpdates) + + init() { + GlobalContext.shared + .updaterController + .updater + .publisher(for: \.canCheckForUpdates) .assign(to: &$canCheckForUpdates) } } @available(macOS 13, *) struct MenuItemView: View { - @ObservedObject var store: MenuItemStore - - init(updater: SPUUpdater) { - store = MenuItemStore(updater: updater) - } + @ObservedObject private var store = MenuItemStore() var body: some View { // ️.menuBarExtraStyle为 .menu 时某些控件可能会失效 ,只能显示内容(按照菜单项高度、图像以 template 方式渲染)无法交互 ,比如 Stepper、Slider 等,像基本的 Button、Text、Divider、Image 等还是能正常显示的。 @@ -173,7 +171,7 @@ struct MenuItemView: View { private var checkUpdateItem: some View { Button("check_updates") { NSLog("检查更新") - store.updater.checkForUpdates() + GlobalContext.shared.updaterController.updater.checkForUpdates() }.disabled(!store.canCheckForUpdates) } @@ -225,5 +223,5 @@ struct MenuItemView: View { @available(macOS 13, *) #Preview { - MenuItemView(updater: SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil).updater) + MenuItemView() } From f8222995bdf6f5870df60c0c128787c0b112b254 Mon Sep 17 00:00:00 2001 From: Jerry Zhang Date: Fri, 26 Jan 2024 18:51:51 -0800 Subject: [PATCH 41/72] Add Traditional Chinese to TencentTranslateType dev (#368) * perf: add traditional chinese to target language * perf: remove unnecessary check condition * perf: add missing zh <--> zh-TW for Tencent translate --------- Co-authored-by: tisfeng --- .../Tencent/TencentTranslateType.swift | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/Easydict/Feature/Service/Tencent/TencentTranslateType.swift b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift index 84ac8b712..ca8bcd91b 100644 --- a/Easydict/Feature/Service/Tencent/TencentTranslateType.swift +++ b/Easydict/Feature/Service/Tencent/TencentTranslateType.swift @@ -14,24 +14,24 @@ struct TencentTranslateType: Equatable { static let unsupported = TencentTranslateType(sourceLanguage: "unsupported", targetLanguage: "unsupported") - // This docs missed traditionalChinese as target language if target languages contains simplifiedChinese. https://cloud.tencent.com/document/api/551/15619 + // https://cloud.tencent.com/document/api/551/15619 static let supportedTypes: [Language: [Language]] = [ .simplifiedChinese: [.english, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay], .traditionalChinese: [.english, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay], - .english: [.simplifiedChinese, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay, .arabic, .hindi], - .japanese: [.simplifiedChinese, .english, .korean], - .korean: [.simplifiedChinese, .english, .japanese], - .french: [.simplifiedChinese, .english, .spanish, .italian, .german, .turkish, .russian, .portuguese], - .spanish: [.simplifiedChinese, .english, .french, .italian, .german, .turkish, .russian, .portuguese], - .italian: [.simplifiedChinese, .english, .french, .spanish, .german, .turkish, .russian, .portuguese], - .german: [.simplifiedChinese, .english, .french, .spanish, .italian, .turkish, .russian, .portuguese], - .turkish: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .russian, .portuguese], - .russian: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .turkish, .portuguese], - .portuguese: [.simplifiedChinese, .english, .french, .spanish, .italian, .german, .turkish, .russian], - .vietnamese: [.simplifiedChinese, .english], - .indonesian: [.simplifiedChinese, .english], - .thai: [.simplifiedChinese, .english], - .malay: [.simplifiedChinese, .english], + .english: [.simplifiedChinese, .traditionalChinese, .japanese, .korean, .french, .spanish, .italian, .german, .turkish, .russian, .portuguese, .vietnamese, .indonesian, .thai, .malay, .arabic, .hindi], + .japanese: [.simplifiedChinese, .traditionalChinese, .english, .korean], + .korean: [.simplifiedChinese, .traditionalChinese, .english, .japanese], + .french: [.simplifiedChinese, .traditionalChinese, .english, .spanish, .italian, .german, .turkish, .russian, .portuguese], + .spanish: [.simplifiedChinese, .traditionalChinese, .english, .french, .italian, .german, .turkish, .russian, .portuguese], + .italian: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .german, .turkish, .russian, .portuguese], + .german: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .turkish, .russian, .portuguese], + .turkish: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .german, .russian, .portuguese], + .russian: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .german, .turkish, .portuguese], + .portuguese: [.simplifiedChinese, .traditionalChinese, .english, .french, .spanish, .italian, .german, .turkish, .russian], + .vietnamese: [.simplifiedChinese, .traditionalChinese, .english], + .indonesian: [.simplifiedChinese, .traditionalChinese, .english], + .thai: [.simplifiedChinese, .traditionalChinese, .english], + .malay: [.simplifiedChinese, .traditionalChinese, .english], .arabic: [.english], .hindi: [.english], ] @@ -59,9 +59,16 @@ struct TencentTranslateType: Equatable { ] static func transType(from: Language, to: Language) -> TencentTranslateType { - // !!!: Tencent translate support traditionalChinese as target language if target languages contain simplifiedChinese. + /** + 1. zh <--> zh-TW + 2. zh --> zh + + Tencent Translate supports Simplified Chinese and Traditional Chinese translations of each other, but the documentation doesn't mention this, so we need to handle it ourselves. + + In addition, it also supports one language as both source and target language if the language is supported. + */ guard let targetLanguages = supportedTypes[from], - targetLanguages.containsChinese() || targetLanguages.contains(to) || from == to || from.isKindOfChinese() + targetLanguages.contains(to) || from == to || EZLanguageManager.shared().onlyContainsChineseLanguages([from, to]) else { return .unsupported } From 4ff44c0bc3bcdde5fc8ebd094b96a5c7ce6029ff Mon Sep 17 00:00:00 2001 From: Jerry Zhang Date: Sun, 28 Jan 2024 00:08:29 -0800 Subject: [PATCH 42/72] Migrate to Defaults for Privacy & About tabs (#360) * refractor: migrate from AppStorage to Defaults in Privacy and About tabs * refractor: use binding to set autoChecksForUpdates * fix: unable to set updater * fix: automaticallyDownloadsUpdates in GlobalContext is wrong * perf: improve code * perf: add updater in Configuration for easy outside use --------- Co-authored-by: Tisfeng Co-authored-by: Lava <34743145+CanglongCl@users.noreply.github.com> --- .../Feature/Configuration/Configuration.swift | 6 +++-- Easydict/NewApp/View/MenuItemView.swift | 6 ++--- .../View/SettingView/Tabs/AboutTab.swift | 23 ++++++++++++++++--- .../View/SettingView/Tabs/PrivacyTab.swift | 8 +++---- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Easydict/Feature/Configuration/Configuration.swift b/Easydict/Feature/Configuration/Configuration.swift index c78b3f2b1..7f7c6dc53 100644 --- a/Easydict/Feature/Configuration/Configuration.swift +++ b/Easydict/Feature/Configuration/Configuration.swift @@ -57,12 +57,14 @@ let kHideMenuBarIconKey = "EZConfiguration_kHideMenuBarIconKey" @DefaultsWrapper(.launchAtStartup) var launchAtStartup: Bool + let updater = GlobalContext.shared.updaterController.updater + var automaticallyChecksForUpdates: Bool { get { - GlobalContext.shared.updaterController.updater.automaticallyDownloadsUpdates + updater.automaticallyChecksForUpdates } set { - GlobalContext.shared.updaterController.updater.automaticallyDownloadsUpdates = newValue + updater.automaticallyChecksForUpdates = newValue logSettings(["automatically_checks_for_updates": newValue]) } } diff --git a/Easydict/NewApp/View/MenuItemView.swift b/Easydict/NewApp/View/MenuItemView.swift index 65a264c59..7b25db502 100644 --- a/Easydict/NewApp/View/MenuItemView.swift +++ b/Easydict/NewApp/View/MenuItemView.swift @@ -15,9 +15,7 @@ final class MenuItemStore: ObservableObject { @Published var canCheckForUpdates = false init() { - GlobalContext.shared - .updaterController - .updater + Configuration.shared.updater .publisher(for: \.canCheckForUpdates) .assign(to: &$canCheckForUpdates) } @@ -171,7 +169,7 @@ struct MenuItemView: View { private var checkUpdateItem: some View { Button("check_updates") { NSLog("检查更新") - GlobalContext.shared.updaterController.updater.checkForUpdates() + Configuration.shared.updater.checkForUpdates() }.disabled(!store.canCheckForUpdates) } diff --git a/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift b/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift index aa6f6a4e4..28e79d37f 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/AboutTab.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import SwiftUI @available(macOS 13, *) @@ -20,7 +21,7 @@ struct AboutTab: View { .font(.system(size: 26, weight: .semibold)) Text("current_version") + Text(verbatim: " \(version)") .font(.system(size: 14)) - Toggle("auto_check_update", isOn: $autoChecksForUpdates) + Toggle("auto_check_update", isOn: $checkUpdaterViewModel.autoChecksForUpdates) Text(verbatim: "(") + Text("lastest_version") + Text(verbatim: " \(lastestVersion ?? version))") HStack { @@ -45,8 +46,8 @@ struct AboutTab: View { } } - @AppStorage("EZConfiguration_kAutomaticallyChecksForUpdatesKey") - private var autoChecksForUpdates = false + @StateObject private var checkUpdaterViewModel = CheckUpdaterViewModel() + @State private var lastestVersion: String? private var appName: String { @@ -56,6 +57,22 @@ struct AboutTab: View { private var version: String { Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" } + + class CheckUpdaterViewModel: ObservableObject { + private let updater = Configuration.shared.updater + + @Published var autoChecksForUpdates = true { + didSet { + updater.automaticallyChecksForUpdates = autoChecksForUpdates + } + } + + init() { + updater + .publisher(for: \.automaticallyChecksForUpdates) + .assign(to: &$autoChecksForUpdates) + } + } } @available(macOS 13, *) diff --git a/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift b/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift index 1cb7714e0..b51403174 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/PrivacyTab.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import Defaults import SwiftUI @available(macOS 13, *) @@ -33,11 +34,8 @@ struct PrivacyTab: View { .formStyle(.grouped) } - @AppStorage("EZConfiguration_kAllowCrashLogKey") - private var allowCollectCrashLog = true - - @AppStorage("EZConfiguration_kAllowAnalyticsKey") - private var allowCollectAnalytics = true + @Default(.allowCrashLog) private var allowCollectCrashLog + @Default(.allowAnalytics) private var allowCollectAnalytics } @available(macOS 13, *) From 896d04d41a7f7aaa9055acf6c53a13d1f7a5ff24 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sun, 28 Jan 2024 11:28:45 +0800 Subject: [PATCH 43/72] docs: update sponsor list --- README.md | 1 + README_EN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index dfb27f0b8..3e2f06b69 100644 --- a/README.md +++ b/README.md @@ -824,6 +824,7 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | | 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | | 2024-01-23 | ㅤ | 5 | | +| 2024-01-28 | ㅤ | 7 | |

diff --git a/README_EN.md b/README_EN.md index 369d47ec0..eeffe8858 100644 --- a/README_EN.md +++ b/README_EN.md @@ -821,6 +821,7 @@ If you don't want your username to be displayed in the list, please choose anony | 2024-01-15 | ㅤ | 20 | 感谢开源,感谢有你:) | | 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | | 2024-01-23 | ㅤ | 5 | | +| 2024-01-28 | ㅤ | 7 | |

From 1a89fb9afe94889fabdc1a199450dbcd9292112f Mon Sep 17 00:00:00 2001 From: Jerry Zhang Date: Sun, 28 Jan 2024 04:57:05 -0800 Subject: [PATCH 44/72] Add GeminiService Support (#297) * test Google Gemini * perf: use Task to wrap gemini async stream * feat: initiate support for Google Gemini * fix: build error * fix: missing GeminiTranslateType file * perf: restore CaiyunService * perf: improve gemini prompt from openai translate * refractor: define static property for translation prompt Co-Authored-By: Kyle * perf: bump debug version * fix: resolve deprecated warnings * refractor: remove GeminiTranslateType * perf: change min version back to 11.0 * perf: resolve xcode warning Xcode Autofix * perf: adjust gemini safety level https://github.com/tisfeng/Easydict/pull/297#issuecomment-1908144296 * refractor: manual error handling * perf: improve error message for Gemini * fix: content streaming on macOS 12+ * perf: improve code, remove logs * perf: update generative-ai-swift to 0.4.7 * perf: rename variable --------- Co-authored-by: tisfeng Co-authored-by: Kyle --- Easydict.xcodeproj/project.pbxproj | 41 ++++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ .../Gemini.imageset/Contents.json | 21 +++ .../service-icon/Gemini.imageset/Gemini.png | Bin 0 -> 165317 bytes Easydict/App/Localizable.xcstrings | 17 +++ .../Service/Gemini/GeminiService.swift | 139 ++++++++++++++++++ Easydict/Feature/Service/Model/EZConstKey.h | 1 + Easydict/Feature/Service/Model/EZEnumTypes.h | 1 + Easydict/Feature/Service/Model/EZEnumTypes.m | 1 + .../Feature/Service/Model/EZServiceTypes.m | 1 + .../Utility/EZAudioUtils/EZAudioUtils.m | 6 +- .../Utility/EZLinkParser/EZSchemeParser.m | 1 + .../Extensions/String/String+Regex.swift | 26 ++++ 13 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json create mode 100644 Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png create mode 100644 Easydict/Feature/Service/Gemini/GeminiService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/String/String+Regex.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index dd2932c73..d61b66620 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 03008B2B2940D3230062B821 /* EZDeepLTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B2A2940D3230062B821 /* EZDeepLTranslate.m */; }; 03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B2D2941956D0062B821 /* EZURLSchemeHandler.m */; }; 03008B3F29444B0A0062B821 /* NSView+EZAnimatedHidden.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B3E29444B0A0062B821 /* NSView+EZAnimatedHidden.m */; }; + 03022F192B3591AE00B63209 /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F182B3591AE00B63209 /* GoogleGenerativeAI */; }; 03022F1C2B35DEBA00B63209 /* Hue in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F1B2B35DEBA00B63209 /* Hue */; }; 03022F1F2B36CF3100B63209 /* SwiftShell in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F1E2B36CF3100B63209 /* SwiftShell */; }; 03022F222B36D1A400B63209 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F212B36D1A400B63209 /* SnapKit */; }; @@ -83,6 +84,7 @@ 03882F9029D95044005B5A52 /* ToastWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03882F8829D95044005B5A52 /* ToastWindowController.xib */; }; 03882F9129D95044005B5A52 /* CTCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 03882F8929D95044005B5A52 /* CTCommon.m */; }; 03882F9229D95044005B5A52 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 03882F8C29D95044005B5A52 /* Info.plist */; }; + 038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A723F2B62C0B9004995E3 /* String+Regex.swift */; }; 038EA1AA2B41169C008A6DD1 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1A92B41169C008A6DD1 /* ZipArchive */; }; 038EA1AD2B41282F008A6DD1 /* MJExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1AC2B41282F008A6DD1 /* MJExtension */; }; 0396D611292C932F006A11D9 /* EZSelectLanguageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */; }; @@ -260,6 +262,7 @@ 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */; }; A0B65CA0F31AC8ECFB8347CC /* Pods_EasydictTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 378E73A7EA8FC8FB9C975A63 /* Pods_EasydictTests.framework */; }; B87AC7E36367075BA5D13234 /* Pods_Easydict.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */; }; + C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415C0AC2B450D4800A9D231 /* GeminiService.swift */; }; C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01E82B12B3C80025EE8E /* TencentService.swift */; }; C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */; }; C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */; }; @@ -445,6 +448,7 @@ 03882F8A29D95044005B5A52 /* CTView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTView.h; sourceTree = ""; }; 03882F8B29D95044005B5A52 /* CoolToast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoolToast.h; sourceTree = ""; }; 03882F8C29D95044005B5A52 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 038A723F2B62C0B9004995E3 /* String+Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Regex.swift"; sourceTree = ""; }; 0396D60F292C932F006A11D9 /* EZSelectLanguageCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZSelectLanguageCell.h; sourceTree = ""; }; 0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZSelectLanguageCell.m; sourceTree = ""; }; 0396D613292CC4C3006A11D9 /* EZLocalStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZLocalStorage.h; sourceTree = ""; }; @@ -755,6 +759,7 @@ 9672D7D02B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MASShortcutBinder+EZMASShortcutBinder.h"; sourceTree = ""; }; 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutBinder+EZMASShortcutBinder.m"; sourceTree = ""; }; A230E9A2358C7FBC7FB26189 /* Pods-EasydictTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EasydictTests.debug.xcconfig"; path = "Target Support Files/Pods-EasydictTests/Pods-EasydictTests.debug.xcconfig"; sourceTree = ""; }; + C415C0AC2B450D4800A9D231 /* GeminiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiService.swift; sourceTree = ""; }; C4DD01E82B12B3C80025EE8E /* TencentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentService.swift; sourceTree = ""; }; C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentResponse.swift; sourceTree = ""; }; C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentTranslateType.swift; sourceTree = ""; }; @@ -806,6 +811,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 03022F192B3591AE00B63209 /* GoogleGenerativeAI in Frameworks */, 2721E4D02AFE920700A059AC /* Alamofire in Frameworks */, 03022F1F2B36CF3100B63209 /* SwiftShell in Frameworks */, 038030952B4106800009230C /* CocoaLumberjack in Frameworks */, @@ -1198,6 +1204,14 @@ path = Kit; sourceTree = ""; }; + 038A723E2B62C07B004995E3 /* String */ = { + isa = PBXGroup; + children = ( + 038A723F2B62C0B9004995E3 /* String+Regex.swift */, + ); + path = String; + sourceTree = ""; + }; 0396D612292CBDFD006A11D9 /* Storage */ = { isa = PBXGroup; children = ( @@ -1317,6 +1331,7 @@ isa = PBXGroup; children = ( 62E2BF462B4082BA00E42D38 /* Ali */, + C415C0AB2B450C4500A9D231 /* Gemini */, 17BCAEF22B0DFF9000A7D372 /* Niutrans */, 2746AEBF2AF95040005FE0A1 /* Caiyun */, C4DD01E72B12B3B00025EE8E /* Tencent */, @@ -2166,6 +2181,14 @@ path = Pods; sourceTree = ""; }; + C415C0AB2B450C4500A9D231 /* Gemini */ = { + isa = PBXGroup; + children = ( + C415C0AC2B450D4800A9D231 /* GeminiService.swift */, + ); + path = Gemini; + sourceTree = ""; + }; C4A40A9B2AC0168400B8E6EF /* Recovered References */ = { isa = PBXGroup; children = ( @@ -2250,6 +2273,7 @@ EA9943E62B534D7C00EE7B97 /* Extensions */ = { isa = PBXGroup; children = ( + 038A723E2B62C07B004995E3 /* String */, EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */, EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */, EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */, @@ -2375,6 +2399,7 @@ 038EA1A92B41169C008A6DD1 /* ZipArchive */, 038EA1AC2B41282F008A6DD1 /* MJExtension */, EA3B81FB2B52555C004C0E8B /* Defaults */, + 03022F182B3591AE00B63209 /* GoogleGenerativeAI */, ); productName = Bob; productReference = C99EEB182385796700FEE666 /* Easydict-debug.app */; @@ -2434,6 +2459,7 @@ 038EA1A82B41169C008A6DD1 /* XCRemoteSwiftPackageReference "ZipArchive" */, 038EA1AB2B41282F008A6DD1 /* XCRemoteSwiftPackageReference "MJExtension" */, EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */, + 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */, ); productRefGroup = C99EEB192385796700FEE666 /* Products */; projectDirPath = ""; @@ -2758,8 +2784,10 @@ 03B0232229231FA6001C7E63 /* NSImage+MM.m in Sources */, 03BB2DEF29F59C8A00447EDD /* EZSymbolImageButton.m in Sources */, 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */, + C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */, 62A2D03F2A82967F007EEB01 /* EZBingRequest.m in Sources */, 03BDA7BE2A26DA280079D04F /* XPMCountedArgument.m in Sources */, + 038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */, 03D35DAA2AA6C49B00B023FE /* NSString+EZRegex.m in Sources */, 03B022FE29231FA6001C7E63 /* EZBaseQueryViewController.m in Sources */, DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */, @@ -3318,6 +3346,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/google/generative-ai-swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.4.4; + }; + }; 03022F1A2B35DEBA00B63209 /* XCRemoteSwiftPackageReference "Hue" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/zenangst/Hue"; @@ -3425,6 +3461,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 03022F182B3591AE00B63209 /* GoogleGenerativeAI */ = { + isa = XCSwiftPackageProductDependency; + package = 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */; + productName = GoogleGenerativeAI; + }; 03022F1B2B35DEBA00B63209 /* Hue */ = { isa = XCSwiftPackageProductDependency; package = 03022F1A2B35DEBA00B63209 /* XCRemoteSwiftPackageReference "Hue" */; diff --git a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved index 38cf97c19..3c8f58191 100644 --- a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -72,6 +72,15 @@ "version" : "10.19.1" } }, + { + "identity" : "generative-ai-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/generative-ai-swift", + "state" : { + "revision" : "dcbdb5e591e1aa2bb68851dc7515f6b0a59026cd", + "version" : "0.4.7" + } + }, { "identity" : "googleappmeasurement", "kind" : "remoteSourceControl", diff --git a/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json new file mode 100644 index 000000000..f0bbce672 --- /dev/null +++ b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Gemini.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png b/Easydict/App/Assets.xcassets/service-icon/Gemini.imageset/Gemini.png new file mode 100644 index 0000000000000000000000000000000000000000..de0d56846487af69e72e446bfce691db3e4fe4b2 GIT binary patch literal 165317 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 ServiceType { + .gemini + } + + override public func link() -> String? { + "https://bard.google.com/chat" + } + + override public func name() -> String { + NSLocalizedString("gemini_translate", comment: "The name of Gemini Translate") + } + + // https://ai.google.dev/available_regions + private static let unsupportedLanguages: [Language] = [.persian, .filipino, .khmer, .lao, .malay, .mongolian, .burmese, .telugu, .tamil, .urdu] + + override public func supportLanguagesDictionary() -> MMOrderedDictionary { + // TODO: Replace MMOrderedDictionary. + let orderedDict = MMOrderedDictionary() + for language in EZLanguageManager.shared().allLanguages { + let value = language.rawValue + if !GeminiService.unsupportedLanguages.contains(language) { + orderedDict.setObject(value as NSString, forKey: language.rawValue as NSString) + } + } + + return orderedDict + } + + override public func ocr(_: EZQueryModel) async throws -> EZOCRResult { + NSLog("Gemini Translate does not support OCR") + throw QueryServiceError.notSupported + } + + override public func needPrivateAPIKey() -> Bool { + true + } + + override public func hasPrivateAPIKey() -> Bool { + if apiKey == defaultAPIKey { + return false + } + return true + } + + private let defaultAPIKey = "" /* .decryptAES() */ + + // easydict://writeKeyValue?EZGeminiAPIKey=xxx + private var apiKey: String { + let apiKey = UserDefaults.standard.string(forKey: EZGeminiAPIKey) + if let apiKey, !apiKey.isEmpty { + return apiKey + } else { + return defaultAPIKey + } + } + + // Set Gemini safety level to BLOCK_NONE + private static let harassmentSafety = SafetySetting(harmCategory: .harassment, threshold: .blockNone) + private static let hateSpeechSafety = SafetySetting(harmCategory: .hateSpeech, threshold: .blockNone) + private static let sexuallyExplicitSafety = SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockNone) + private static let dangerousContentSafety = SafetySetting(harmCategory: .dangerousContent, threshold: .blockNone) + + private static let translationPrompt = "You are a translation expert proficient in various languages that can only translate text and cannot interpret it. You are able to accurately understand the meaning of proper nouns, idioms, metaphors, allusions or other obscure words in sentences and translate them into appropriate words by combining the context and language environment. The result of the translation should be natural and fluent, you can only return the translated text, do not show additional information and notes." + + override public func autoConvertTraditionalChinese() -> Bool { + true + } + + override public func translate(_ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> Void) { + Task { + do { + let prompt = GeminiService.translationPrompt + "Translate the following \(from.rawValue) text into \(to.rawValue): \(text)" + print("gemini prompt: \(prompt)") + let model = GenerativeModel( + name: "gemini-pro", + apiKey: apiKey, + safetySettings: [ + GeminiService.harassmentSafety, + GeminiService.hateSpeechSafety, + GeminiService.sexuallyExplicitSafety, + GeminiService.dangerousContentSafety, + ] + ) + + if #available(macOS 12.0, *) { + var resultString = "" + let outputContentStream = model.generateContentStream(prompt) + + // stream response + for try await outputContent in outputContentStream { + guard let line = outputContent.text else { + return + } + + resultString += line + result.translatedResults = [resultString] + completion(result, nil) + } + + } else { + let outputContent = try await model.generateContent(prompt) + guard let resultString = outputContent.text else { + return + } + + result.translatedResults = [resultString] + completion(result, nil) + } + } catch { + /** + https://github.com/google/generative-ai-swift/issues/89 + + String(describing: error) + + "internalError(underlying: GoogleGenerativeAI.RPCError(httpResponseCode: 400, message: \"API key not valid. Please pass a valid API key.\", status: GoogleGenerativeAI.RPCStatus.invalidArgument))" + */ + let ezError = EZError(nsError: error) + let errorString = String(describing: error) + let errorMessage = errorString.extract(withPattern: "message: \"([^\"]*)\"") ?? errorString + ezError?.errorDataMessage = errorMessage + + completion(result, ezError) + } + } + } +} diff --git a/Easydict/Feature/Service/Model/EZConstKey.h b/Easydict/Feature/Service/Model/EZConstKey.h index be00eeea2..cf422670b 100644 --- a/Easydict/Feature/Service/Model/EZConstKey.h +++ b/Easydict/Feature/Service/Model/EZConstKey.h @@ -35,6 +35,7 @@ static NSString *const EZNiuTransAPIKey = @"EZNiuTransAPIKey"; static NSString *const EZCaiyunToken = @"EZCaiyunToken"; static NSString *const EZTencentSecretId = @"EZTencentSecretId"; static NSString *const EZTencentSecretKey = @"EZTencentSecretKey"; +static NSString *const EZGeminiAPIKey = @"EZGeminiAPIKey"; static NSString *const EZAliAccessKeyId = @"EZAliAccessKeyId"; static NSString *const EZAliAccessKeySecret = @"EZAliAccessKeySecret"; diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.h b/Easydict/Feature/Service/Model/EZEnumTypes.h index 28dbeea3a..cad41e525 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.h +++ b/Easydict/Feature/Service/Model/EZEnumTypes.h @@ -43,6 +43,7 @@ FOUNDATION_EXPORT EZServiceType const EZServiceTypeNiuTrans; FOUNDATION_EXPORT EZServiceType const EZServiceTypeCaiyun; FOUNDATION_EXPORT EZServiceType const EZServiceTypeTencent; FOUNDATION_EXPORT EZServiceType const EZServiceTypeAli; +FOUNDATION_EXPORT EZServiceType const EZServiceTypeGemini; FOUNDATION_EXPORT NSString *const EZQueryTextTypeKey; FOUNDATION_EXPORT NSString *const EZIntelligentQueryTextTypeKey; diff --git a/Easydict/Feature/Service/Model/EZEnumTypes.m b/Easydict/Feature/Service/Model/EZEnumTypes.m index 76e778da2..879b9102b 100644 --- a/Easydict/Feature/Service/Model/EZEnumTypes.m +++ b/Easydict/Feature/Service/Model/EZEnumTypes.m @@ -22,6 +22,7 @@ NSString *const EZServiceTypeCaiyun = @"Caiyun"; NSString *const EZServiceTypeTencent = @"Tencent"; NSString *const EZServiceTypeAli = @"Alibaba"; +NSString *const EZServiceTypeGemini = @"Gemini"; NSString *const EZServiceTypeAppleDictionary = @"AppleDictionary"; diff --git a/Easydict/Feature/Service/Model/EZServiceTypes.m b/Easydict/Feature/Service/Model/EZServiceTypes.m index d63ed6649..c40d7ceba 100644 --- a/Easydict/Feature/Service/Model/EZServiceTypes.m +++ b/Easydict/Feature/Service/Model/EZServiceTypes.m @@ -63,6 +63,7 @@ + (instancetype)allocWithZone:(struct _NSZone *)zone { EZServiceTypeCaiyun, [EZCaiyunService class], EZServiceTypeTencent, [EZTencentService class], EZServiceTypeAli, [EZAliService class], + EZServiceTypeGemini, [EZGeminiService class], nil]; return allServiceDict; } diff --git a/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m b/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m index c18ed7160..59ed2348d 100644 --- a/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m +++ b/Easydict/Feature/Utility/EZAudioUtils/EZAudioUtils.m @@ -34,7 +34,7 @@ + (float)getSystemVolume { AudioObjectPropertyAddress address = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster}; + kAudioObjectPropertyElementMain}; if (!AudioObjectHasProperty(outputDeviceID, &address)) { NSLog(@"No volume returned for device 0x%0x", outputDeviceID); @@ -72,7 +72,7 @@ + (void)setSystemVolume:(float)volume { AudioObjectPropertyAddress address = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeOutput, - kAudioObjectPropertyElementMaster}; + kAudioObjectPropertyElementMain}; if (!AudioObjectHasProperty(outputDeviceID, &address)) { NSLog(@"No volume returned for device 0x%0x", outputDeviceID); @@ -92,7 +92,7 @@ + (AudioDeviceID)getDefaultOutputDeviceID { AudioObjectPropertyAddress address = { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMaster}; + kAudioObjectPropertyElementMain}; UInt32 dataSize = sizeof(AudioDeviceID); OSStatus status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &dataSize, &outputDeviceID); diff --git a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m index 6a7a25442..bdefc8e56 100644 --- a/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m +++ b/Easydict/Feature/Utility/EZLinkParser/EZSchemeParser.m @@ -220,6 +220,7 @@ - (NSArray *)allowedReadWriteKeys { EZAliAccessKeyId, EZAliAccessKeySecret, + EZGeminiAPIKey, EZIntelligentQueryModeKey, ]; diff --git a/Easydict/NewApp/Utility/Extensions/String/String+Regex.swift b/Easydict/NewApp/Utility/Extensions/String/String+Regex.swift new file mode 100644 index 000000000..0acccba89 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/String/String+Regex.swift @@ -0,0 +1,26 @@ +// +// String+Regex.swift +// Easydict +// +// Created by tisfeng on 2024/1/26. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension String { + func extract(withPattern pattern: String) -> String? { + do { + let regex = try NSRegularExpression(pattern: pattern) + let range = NSRange(location: 0, length: utf16.count) + if let match = regex.firstMatch(in: self, options: [], range: range) { + if let range = Range(match.range(at: 1), in: self) { + return String(self[range]) + } + } + } catch { + print("Invalid regex: \(error.localizedDescription)") + } + return nil + } +} From bece937da70503980eab6819f600d0f5215b8d7d Mon Sep 17 00:00:00 2001 From: Tisfeng Date: Tue, 30 Jan 2024 13:30:04 +0800 Subject: [PATCH 45/72] fix: new services were disabled by mistake (#373) --- .../ViewController/Storage/EZLocalStorage.m | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Easydict/Feature/ViewController/Storage/EZLocalStorage.m b/Easydict/Feature/ViewController/Storage/EZLocalStorage.m index 44b9735e3..92d3f1663 100644 --- a/Easydict/Feature/ViewController/Storage/EZLocalStorage.m +++ b/Easydict/Feature/ViewController/Storage/EZLocalStorage.m @@ -62,12 +62,26 @@ - (void)setup { EZWindowType windowType = [number integerValue]; for (EZServiceType serviceType in allServiceTypes) { EZServiceInfo *serviceInfo = [self serviceInfoWithType:serviceType windowType:windowType]; + + // New service. if (!serviceInfo) { serviceInfo = [[EZServiceInfo alloc] init]; serviceInfo.type = serviceType; - serviceInfo.enabled = NO; // disable new service + serviceInfo.enabled = YES; + serviceInfo.enabledQuery = YES; - // Mini type should keep concise, services <= 4 + /** + Fix https://github.com/tisfeng/Easydict/issues/269 and https://github.com/tisfeng/Easydict/issues/372 + + If there is a new service, we enable it but disable auto query, and add it to the end of the array. + + If it is the user's first time, auto query should be all allowed. + */ + if (self.queryCount > 0) { + serviceInfo.enabledQuery = NO; + } + + // Mini window should keep concise, so default enabled services should <= 4 if (windowType == EZWindowTypeMini) { NSArray *defaultEnabledServices = @[ EZServiceTypeAppleDictionary, @@ -78,10 +92,6 @@ - (void)setup { serviceInfo.enabled = [defaultEnabledServices containsObject:serviceType]; } - // There is a very small probability that Volcano webView translator will crash. - if (serviceType != EZServiceTypeVolcano) { - serviceInfo.enabledQuery = YES; - } [self setServiceInfo:serviceInfo windowType:windowType]; } } @@ -100,10 +110,6 @@ - (void)setup { NSMutableArray *array = [NSMutableArray arrayWithArray:allStoredServiceTypes]; if (![allStoredServiceTypes isEqualToArray:allServiceTypes]) { for (EZServiceType type in allServiceTypes) { - /** - If there is a new service, add it to the end of the array. - Fix https://github.com/tisfeng/Easydict/issues/269 - */ if ([allStoredServiceTypes indexOfObject:type] == NSNotFound) { [array addObject:type]; } From fa87f7d2dd364a5f9a80d1736e878ee65555faed Mon Sep 17 00:00:00 2001 From: NeverAgain11 Date: Wed, 31 Jan 2024 16:45:29 +0800 Subject: [PATCH 46/72] Set appearance at launch (#377) --- Easydict/App/AppDelegate.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Easydict/App/AppDelegate.m b/Easydict/App/AppDelegate.m index ccec0a435..f4dc71d53 100644 --- a/Easydict/App/AppDelegate.m +++ b/Easydict/App/AppDelegate.m @@ -37,6 +37,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self registerRouters]; + [[DarkModeManager manager] updateDarkMode:Configuration.shared.appearance]; // Change App icon manually. // NSApplication.sharedApplication.applicationIconImage = [NSImage imageNamed:@"white-black-icon"]; } From 557292c9918b97b0dc1d1a39fba64eafb4232afc Mon Sep 17 00:00:00 2001 From: Tisfeng Date: Wed, 31 Jan 2024 22:32:57 +0800 Subject: [PATCH 47/72] perf: show new settings option in release mode (#374) --- .../EZSettingViewController.m | 42 ++++++++----------- Easydict/NewApp/NewAppManager.swift | 9 ---- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m index 38d2aec42..415295202 100644 --- a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m @@ -495,16 +495,14 @@ - (void)setupUI { self.hideMenuBarIconButton = [NSButton checkboxWithTitle:hideMenuBarIcon target:self action:@selector(hideMenuBarIconButtonClicked:)]; [self.contentView addSubview:self.hideMenuBarIconButton]; - if (EasydictNewAppManager.shared.showEnableToggleUI) { - NSTextField *betaNewAppLabel = [NSTextField labelWithString:NSLocalizedString(@"beta_new_app", nil)]; - betaNewAppLabel.font = font; - [self.contentView addSubview:betaNewAppLabel]; - self.betaNewAppLabel = betaNewAppLabel; - - NSString *enableBetaNewApp = NSLocalizedString(@"enable_beta_new_app", nil); - self.enableBetaNewAppButton = [NSButton checkboxWithTitle:enableBetaNewApp target:self action:@selector(enableBetaNewAppButtonClicked:)]; - [self.contentView addSubview:self.enableBetaNewAppButton]; - } + NSTextField *betaNewAppLabel = [NSTextField labelWithString:NSLocalizedString(@"beta_new_app", nil)]; + betaNewAppLabel.font = font; + [self.contentView addSubview:betaNewAppLabel]; + self.betaNewAppLabel = betaNewAppLabel; + + NSString *enableBetaNewApp = NSLocalizedString(@"enable_beta_new_app", nil); + self.enableBetaNewAppButton = [NSButton checkboxWithTitle:enableBetaNewApp target:self action:@selector(enableBetaNewAppButtonClicked:)]; + [self.contentView addSubview:self.enableBetaNewAppButton]; NSTextField *fontSizeLabel = [NSTextField labelWithString:NSLocalizedString(@"font_size", nil)]; fontSizeLabel.font = font; @@ -866,23 +864,17 @@ - (void)updateViewConstraints { make.centerY.equalTo(self.menuBarIconLabel); }]; - if (EasydictNewAppManager.shared.showEnableToggleUI) { - [self.betaNewAppLabel mas_remakeConstraints:^(MASConstraintMaker *make) { - make.right.equalTo(self.autoGetSelectedTextLabel); - make.top.equalTo(self.hideMenuBarIconButton.mas_bottom).offset(self.verticalPadding); - }]; - [self.enableBetaNewAppButton mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.betaNewAppLabel.mas_right).offset(self.horizontalPadding); - make.centerY.equalTo(self.betaNewAppLabel); - }]; - } + [self.betaNewAppLabel mas_remakeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.autoGetSelectedTextLabel); + make.top.equalTo(self.hideMenuBarIconButton.mas_bottom).offset(self.verticalPadding); + }]; + [self.enableBetaNewAppButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.betaNewAppLabel.mas_right).offset(self.horizontalPadding); + make.centerY.equalTo(self.betaNewAppLabel); + }]; self.topmostView = self.inputLabel; - if (EasydictNewAppManager.shared.showEnableToggleUI) { - self.bottommostView = self.enableBetaNewAppButton; - } else { - self.bottommostView = self.hideMenuBarIconButton; - } + self.bottommostView = self.enableBetaNewAppButton; if ([EZLanguageManager.shared isSystemChineseFirstLanguage]) { self.leftmostView = self.adjustQueryIconPostionLabel; diff --git a/Easydict/NewApp/NewAppManager.swift b/Easydict/NewApp/NewAppManager.swift index 5b54187f0..8b3d9a8dc 100644 --- a/Easydict/NewApp/NewAppManager.swift +++ b/Easydict/NewApp/NewAppManager.swift @@ -22,13 +22,4 @@ public final class NewAppManager: NSObject { public var enable: Bool { UserDefaults.standard.bool(forKey: Self.enableKey) } - - @objc - public var showEnableToggleUI: Bool { - #if DEBUG - true - #else - false - #endif - } } From 8a3715d39eca066ce1d2a5f83a74fb26289e569d Mon Sep 17 00:00:00 2001 From: choykarl <253440030@qq.com> Date: Thu, 1 Feb 2024 11:20:53 +0800 Subject: [PATCH 48/72] Modify menu bar icon (#370) * feat: modify menu bar icon * feat: use the debug icon in debug mode * Update Easydict/App/Localizable.xcstrings `Menubar icon` would do Co-authored-by: Tisfeng * perf(UI): adjust localization for menubar * fix: string catalog file * Update Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift * Update Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift * Update Easydict/NewApp/EasydictApp.swift Co-authored-by: Tisfeng * pref: optimize the use of MenuBarIconType * feat: switch icon colors based on the color scheme * pref: use .primary style * perf: rename MenuBarIconType enum value * Revert "perf(UI): adjust localization for menubar" This reverts commit dd4bfccecf44b3a9a3a7de6ad9c47a18068b1163. * perf: update menu bar icon, use original rendering mode in debug * perf: rename menu bar icon --------- Co-authored-by: Jerry Zhang Co-authored-by: Tisfeng Co-authored-by: Jerry <89069957+Jerry23011@users.noreply.github.com> --- .../Contents.json | 7 +++--- .../rounded.png | Bin 0 -> 4444 bytes .../Contents.json | 4 +--- .../square_menu_bar_icon.imageset/square.png | Bin 0 -> 2813 bytes .../status_icon.imageset/release_18.png | Bin 1489 -> 0 bytes .../status_icon.imageset/release_36.png | Bin 1756 -> 0 bytes .../status_icon.imageset/release_54.png | Bin 838 -> 0 bytes .../status_icon_debug.imageset/debug_20 1.png | Bin 1213 -> 0 bytes .../status_icon_debug.imageset/debug_40 1.png | Bin 2122 -> 0 bytes .../status_icon_debug.imageset/debug_60 1.png | Bin 1101 -> 0 bytes Easydict/App/Localizable.xcstrings | 19 ++++++++++++++- .../Feature/StatusItem/EZMenuItemManager.m | 4 ++-- .../Configuration+Defaults.swift | 1 + Easydict/NewApp/EasydictApp.swift | 22 +++++++++++------- .../View/SettingView/Tabs/GeneralTab.swift | 13 +++++++++++ 15 files changed, 53 insertions(+), 17 deletions(-) rename Easydict/App/Assets.xcassets/menu-icon/{status_icon.imageset => rounded_menu_bar_icon.imageset}/Contents.json (70%) create mode 100644 Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/rounded.png rename Easydict/App/Assets.xcassets/menu-icon/{status_icon_debug.imageset => square_menu_bar_icon.imageset}/Contents.json (70%) create mode 100644 Easydict/App/Assets.xcassets/menu-icon/square_menu_bar_icon.imageset/square.png delete mode 100644 Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_18.png delete mode 100644 Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_36.png delete mode 100644 Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_54.png delete mode 100644 Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_20 1.png delete mode 100644 Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_40 1.png delete mode 100644 Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_60 1.png diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/Contents.json b/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/Contents.json similarity index 70% rename from Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/Contents.json rename to Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/Contents.json index d0d1860d8..ca238cdf9 100644 --- a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/Contents.json +++ b/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/Contents.json @@ -1,17 +1,15 @@ { "images" : [ { - "filename" : "release_18.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "release_36.png", + "filename" : "rounded.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "release_54.png", "idiom" : "universal", "scale" : "3x" } @@ -19,5 +17,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "localizable" : true } } diff --git a/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/rounded.png b/Easydict/App/Assets.xcassets/menu-icon/rounded_menu_bar_icon.imageset/rounded.png new file mode 100644 index 0000000000000000000000000000000000000000..72cd3f20084652e98713eade6c81028f7eee37aa GIT binary patch literal 4444 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 literal 0 HcmV?d00001 diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_18.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_18.png deleted file mode 100644 index 24e2328eb4a8f26bfceeb22e907cf004ecd8bff2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1489 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lxRtf@J#ddWzYh$IT%Q&?xfOIj~R9FF-xv3?I3Kh9IdBs*0wn|_XRzNmLSYJs2tfVB{Rw=?aK*2e`C{@8s z&p^*W$&O1wLBXadCCw_x#SN+*$g@?-C@Cqh($_C9FV`zK*2^zS*Eh7ZwA42+(l;{F z1**_3uFNY*tkBIXR)!b?Gsh*hIJqdZpd>RtPXT0ZVp4u-iLH_n$Rap^xU(cP4PjGW zG1OZ?59)(t^bPe4^s#A6t;oco4I~562KE=kIvbE-R*^xe#rZj)0j_clpZ(YC~^4QbGF~mZ!cglY6utbrD_tP_z>?d4fQ8Tw5!0vbGd_|W0w|mwB z>naML#?5GT-C(6%v@rexXV|nwxvKsT^H?@Fe|q4;61+9>%hKY;vd66*(|f%lS=PB5 zd{y};)#G+YGOD)v$M%cnhxD|>Pkrj^h&o%KV#_(VI9U7ihM+aq9!|M7&pp7D!~TKo zn}f0eMPVynZ`|B@ebSyx>)8fu+l*Pda$iROHLeidChM{DqZ9MS_qsP4%^x`5WJ(KO zwsD`SEl)X66aR+l1KaWriPW@58}NNUXnBvB|AFw0S9()ob?5Y_&$;;P=FvV#*puwU5r zP3qS|OX60p9PEyK?RkktDnm{r-UW|!#D5l diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_36.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_36.png deleted file mode 100644 index 2a7dbe54091ec3c3907821f84a15b24ed98e3372..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1756 zcmY*Z2~-nT6#WAf5>hn~7J(w;Kr5moY*N4w0TUs}E|jgcgpiadfsh0N2_A?G2^_@` zSzHLvVm&R#$f7AADzyY@p@LP~fEFc4k=lx=C?Y~9O0oTO{=EC&zxTfX{+Tn>5dW63+!9!W+w4zS|r?9fo)gklVM4>AS%D1y)jK-NI#h9VFQ*(~EI z$Z?6I2S5=A7%XuT(5M?h=+tT6M*Ujoonx)uGTWh6{~gxJhCGfeq5;bbiWdNYBj_ds zPMlhey2W#%;2dsRy1*yL7kR_*L9s4P zLf|(P;cj0fE;It(n!#tmoTL#OAbe_zGihrURUL~s~$5R8^kPB zN4#%UW{0y`03WjasqxZ-QO{fOxb=w*Ru&r_HwBl`Q@X^{CtXEbCKOXyk1kE+P4(Te zx;%4fBDd!0sR>cP;CkN3W_I7=_nHpQWPx05IQ^a1i6mu>wzPweX?0RbG1mC$!yX+2 z*x-43n&ninV#gn!!(wPrd__w>qkT4(oS4?#%coI#_SrY*@09hKKmlJ%1j(KXe>EBN zCE>JWXx(M=^;}w`=$Hn_V~u}n+$}OIm8@&n_O1I_{06_ihJ^ixh8DA2#_%bP$xix@ zpQbhbv@i3k)*y$M+@{$2_V~eLn>(4D-H180kzn5~)*xLx6;aYa<9$MQ{$wG|#V9xHr9Er2QW2hFOQr>tdVasM8b2 z?F~mvr>L)F397RePEK`D{AF!dF4j3U+>{sj{Pk={2zUE=TN}Hg-=en%*ct7qXr&vR zQ-{f7M>OV#`^+0wZ7Hwb@W`@FWLYxncI`g*_!akyV@al;nnKbwCA00c>x7NOm+Xr;YY4&1<3Pml2{vSn32w$8J+*~sqL%?>q7k(jac z;f;tQV^lA*9E0!lTA-gV?zXm?HyV4zk=@TNTNjiMQ5q#)jue8))jf!D*->&=0YO`r z6tjTGd4)HAke@5oOdZtDd1((0KfIwFoZUI6^t;=0p?e@)#8kj~8i5MBGvS)@LV1i}t*WT|GOo=W?*q~?d)h@Rc^t2-u1 zOuwQBnUw9bFQ45jy4Gb@y|?pjO>CWE7T@A}$+6}{gWSRO&ngxW1=)~bJ^s|fGGSyC zdS8Wkm3p)On=Kb?f2i?N75>!ZNOA8bTg@j{Ihlqh8sr^m2~4wqHtcsS8d-UK(~UaJ zlT6Q%5((pW+r`IQ6{8w1o+l%&s&}jJ+qw3ez{%$gH_7&0U>?) z(AE41mxl}Ea=)D!A7ZB=(LTY}O(qARoDC<$F>`ml+YjA3Di04i*E8x|G|^QvtdD7a zGSuAW-!^v>_Faixvt9k7_=Pt0)y))hN~~n7m#>>`#JRuKWk!YDcHOp2Fo-Pol)=vl znKK13^8Et2f>smG7fhvy3y*RGB52t02BVHgl=IQOiw3O@I|^s|T-%w8;Ny=>?t1)n Q!Cm*yrqjZx%77&CKOA_s^#A|> diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_54.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon.imageset/release_54.png deleted file mode 100644 index 22fa1bbc39c439db39b2000287a6a797921c3df1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 838 zcmV-M1G)T(P)Px&07*naRA@u(noGz{Q5462-_*o}sVGS(N{A5>6B(f-kpUSD5{m1At48d1U=pAWV;^wRJi7y|Y73Yu ziw#EXbwIE(=$KpG0BcgrT+q}9y=+6b^Lf=H|pxQL_9m^s(v~44{`%tl2+I1FeQ}2!>%VLDK9jh}GB~pjh zv?AF3MKkReSLMA-MR4e972Pf*K@ZGXx5q*jVF+edY20}%Bt*(+<2!g37K&vE4jEGw zMQn8+w0VSxD5Fi`U`_dxSm9ubYyGWH_v>|9;b3*bls0##|6y9%s?|C0A3;$5YdPx# diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_20 1.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_20 1.png deleted file mode 100644 index 9f51401bcb4027976e1319cc6db548a5badc1c7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1213 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>>x%Bs2U~)hW1Pb7O0viAPoW@Kn&6Yp_vyjB1~Ms z1XnGxfEmFCX>)u&_aBhrEbxddW?VstT4fPE4;bsH1+JHo@{EISEfi{E8w==W>t3(ll+GC>+vK+}V5TAlYfnK%a zveAbJn;n;A+(UK-1|~aC7sn6_!P?1Zy~7+u+SiBOY7Y=`>TW5QUMAor?~%XW!adpqcnZe@jnBAWLU!=V_*czJgyE^^Wwe5NQj`I&XYt=e_Sje;@4pe#Y|o zyqT89+n4^>AH|U$9whVrT=LtqK4qdu*LfB2e-o2w(JtV4=D1&8yL+~>SaD8oQIPP` zl7#I|C-oBrR#}~Vp!KIow1QWDVccSi_Rp3!t>GWo#U60(X*j-MUGwDdow*E0^DaM7 zJMZzU^RIRP*2DSQRu4qJILV)B`1p9isYTNzC3Y}=JE!a4RP>pw*5=yaC7F zewY5iw)D=b&c@Z8x4#R?G+xkX+x{TJqN4iJQmc*U4slxWXB)8cTJU~5wN|S*X;byd zV3D^!=f?R?{rF>{yw8#s=ag@KRycn56+1`$9Oqw3bNKGe`u=ep^OHBcws-jZM6v++l_ z*~a}1_D%Jt)H8y7E<9PoyXw0xpLS5xYd&T%(eSl<4xAD$S6#PCEBl7f=WYL(cI@c- T_~!XfaHjNh^>bP0l+XkKM{2R( diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_40 1.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_40 1.png deleted file mode 100644 index 76d13685bc8c1c88262d538bb0dda064c85737e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2122 zcmY*ad0dj&8a}w-icMm7T(EHTKN)D8?_LF_d07%Xld|+xrd(^(|R9^rN+W?S!9DoIgN`4JM z91Z{_0{~(c0H)Eo_X0@J!ErX-Gu+n~`~YEHpbawynh=6P4}e(!wHt(h7tHc2rou2^ zG#UW>GZJWh(d>eiecXz#TsAL7_sIztS(C*G$@6VUi9{09smYNb-+M& zt{LPO92rRG(|vu2`=U5@Ojguhww;g@t*!zjArXQcHlK+Sat?BNM4>bKlY$6gHH}51 zK1uiooY8b&3hL*mST>4ahqJ?>UGz~X6e*S!PNceT`%(_=oY4_{el!t_6$k`&0=!*R z?0&4hqoX4h=YVx^u!R)1yhB_*Q)tWOS${V9ua7&Mw=Xs_njaa(MX7z6d!yp`&Sc!nvl32@XypRggjqt2$J8Xj*5KpcaA?|13Rcm|f->VSXR z8CEB+Sxg$56Hd(hvcvR4TVl!G*{~yTj?PvMN+ey=e>>pV?sNDPLvri><`T!+H%SH( z^IiFlfldU%iY3?EX1$pU4>AwDFu+YQ$(BC0kz*I#x`GB~lutoiNf0O5LxCBDUre2L zF;fIn2D(-)w(4A(Bt6?Kx-+@?A7J^S@xg_|YvB61=Vt^T%gd5uC{JFv^uNC+k50dW(d%s-PiwQfB1 z^(;khgcP@do+YD^28>k8TJv#MsmZk)jk#mM_2CqnuH`LrbLc~M?_Np}>XPnoVA+WE zB@(RD9QAG29FnpSZ=1H8_IFoBd=fKP>Jm~NG2$CPRZEWa==R((y_ZvLVsL%2b4kC3 zV|AfTtMut}O2OsGGHauFsd2$Vc5J??)vcq#>b=i};)nIhlPUbG+Ht{KfNk#*I{IDUU#$1vYvtPbJd)9z8RlIt3uxq zXm_9U5z*55J9D;O_9+i-%2Uj|3LY?YD%?<`$krdQtZ&ISi=Mc5^%ghhDS1v@efefJ zvr69tzn@~)B=uJsAtsWaD`Y|Idux^zq|J-(!wQc5c}%uZvfICA0-qJrXJlm>XLb5? zw14DS@z91=wr~RIoE>1A#B@AXi3?=u9xbypu^FS7m5kXfDt>p+Yr-k%O7E~`$@`Vf z1hz+j6})gzZc8LP?q685amiZewUTszTTBn4#)mZD`mL{rebBJaZKrIQx*0alqOPts z4ZUK{K}bd9abnu+*mpI-=L5FN2X$LdW*ptqc#fXv!M%Cv1KTm#2^*0pvBPQgirZK`rZ?j}Op zS+RFe>1|h@bJ*d<@+xz&kq*PdzPz%wyJxh--`AO;6fj?63s_O-76iC}^KCC2eWw1T z*6D>Z7Ou~FYb1)e_`j4!DI=rq#(UZ)g2Z)|J&Bzw8_<=~Osk?Ca*lOTLLRl{R$NFA z-hApWxq+yuq*i-QHl(P^T%nNp$KQxwc&Dl;e3gOoA&8%lklm_bmaKVxn?xG>V}|*a z;9F6rZr_kDT{z6uyffU#w1|7~D=~a!S-&4kL0Es%VqP>o{$uLD1|~0^Y41#XlT&uS zyq%R0E-tOxRM0iSc`L@eypMSK0Uw=tZ%IdHI~S%}*Qc>2h3u-~?Upw;Xwj#s>~C^2 zY9v*{Kj4J^iSTNMmquY??scA1LNS`S$Q!Z^nrsRmIWiTLUeL11y>1P|M3jE^zu6IhF?HrVr<-9C%RUv*Di@|@qUYq)mG$e1SPx>WI Vq|;VcsH^@%dT#Y|zqN&N>_0c8jX3}S diff --git a/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_60 1.png b/Easydict/App/Assets.xcassets/menu-icon/status_icon_debug.imageset/debug_60 1.png deleted file mode 100644 index 0e7cca491c9fea1adad0cdf802c1fd5dafe1fa33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1101 zcmV-T1hV^yP)Px(2T4RhRA@u(npvn$Q5462Hy$WMUX?NrNk}rs1L0A4A#)*9NFom?4~8OSC}fH! z51tIA%wxuq$V_=uNGPI}@7#`k*ZI!c=bWqk-FsGj58pZewb%OZ|Jm#8z0a?hEOZ=R16_+EY%MZ*a6W`P0#AXqj#C%l zO;STX3g;oxSTnMVz!-blgh9@!lvEuJner$P|t9 zsnUXN)JIyItGeSylCa|PE?gqQS|=4!w0wI%4POM|v=|NU`;1}l##0mTB=g)}rNASYqI68v>bzL?)+{gz~l-HNODZUqwaP z+Bxd0a^B>u?_uA(1TVW6J*zlA0tWat94cWU^1wi*Pi2W{fc}MqO86Ks!(nkrSqjR| z$6_~>;(0lEP%l^u@hz)7phArh8PDKfE$TlC)5 z-phA^!T*b}HI5Z0iOcsR*Tw-CfC-gPEbje&2{W<=4dqqXO2mpg0Zeni4}pQPYx2Lp zgV_lzw**NeoHXOI&crM^#4%b;SOaW{fz|<=vKB83VXr+SUxqRK3Shh9-x+utyDr!oz{>KHRc$CTOsl~5oL-p*)TYqm2fBEo#T8T zkTLh8pbTZEG^n7qq?yagBBQ?fG=&BD9O&*Ov=-Q0K$4>YSp~T!Gph53_QwO~6r9Z1 zq$3-ch43z5sp4A%>4U&Lr|r)`djr`B@<}-nAd3(6gpY-=G%TwW?L+ei;2t24c{S5! zX03sbl_C@OH6Ku^IB8!Wz5yMyVP_>g3b^9Y>n>D_qb&o)Pt+BattRI^L}k}xS5sD5 z|Cp7qIB7j;O-hS%Q#php<5tG6ugN%;@f^2nfC!bDusHUr>=X|1%*Z7?GT|_pwOF~~ zlZuy|__-Aa`zRdu+X_qpWFl`dH;#57#kW|wakb(laTXOpIBG*yGP1*%wp|)8T5W(; zN-xRM&~%zEp@)-}ta6m;YvhwvVT-CkVoJh6%2HrbrNRYvW{^#l3I{1mfl2)VXfww> T0qogd00000NkvXXu0mjf)p!0x diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index bfb9668af..0819e750d 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1805,6 +1805,23 @@ } } }, + "modify_menubar_icon" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Menu bar icon" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "菜单栏图标" + } + } + } + }, "mouse_select_translate_window_type" : { "localizations" : { "en" : { @@ -3467,4 +3484,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Easydict/Feature/StatusItem/EZMenuItemManager.m b/Easydict/Feature/StatusItem/EZMenuItemManager.m index 9f40e3fc9..81497fd5e 100644 --- a/Easydict/Feature/StatusItem/EZMenuItemManager.m +++ b/Easydict/Feature/StatusItem/EZMenuItemManager.m @@ -83,9 +83,9 @@ - (void)setup { NSStatusBarButton *button = statusItem.button; #if DEBUG - NSImage *image = [NSImage imageNamed:@"status_icon_debug"]; + NSImage *image = [NSImage imageNamed:@"rounded_menu_bar_icon"]; #else - NSImage *image = [NSImage imageNamed:@"status_icon"]; + NSImage *image = [NSImage imageNamed:@"square_menu_bar_icon"]; #endif [button setImage:image]; diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index 2cafce5ed..b8ab739dd 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -52,6 +52,7 @@ extension Defaults.Keys { static let appearanceType = Key("EZConfiguration_kApperanceKey", default: .followSystem) static let fontSizeOptionIndex = Key("EZConfiguration_kTranslationControllerFontKey", default: 0) + static let selectedMenuBarIcon = Key("EZConfiguration_kSelectedMenuBarIconKey", default: .square) } extension Defaults.Keys { diff --git a/Easydict/NewApp/EasydictApp.swift b/Easydict/NewApp/EasydictApp.swift index bb3e1be41..95e985b93 100644 --- a/Easydict/NewApp/EasydictApp.swift +++ b/Easydict/NewApp/EasydictApp.swift @@ -31,13 +31,8 @@ struct EasydictApp: App { @AppStorage(Defaults.Key.hideMenuBarIcon.name) private var hideMenuBar = Defaults.Key.hideMenuBarIcon.defaultValue - private var menuBarImage: String { - #if DEBUG - "status_icon_debug" - #else - "status_icon" - #endif - } + @Default(.selectedMenuBarIcon) + private var menuBarIcon var body: some Scene { if #available(macOS 13, *) { @@ -47,9 +42,13 @@ struct EasydictApp: App { Label { Text("Easydict") } icon: { - Image(menuBarImage) + Image(menuBarIcon.rawValue) .resizable() + #if DEBUG + .renderingMode(.original) + #else .renderingMode(.template) + #endif .scaledToFit() } .help("Easydict 🍃") @@ -67,3 +66,10 @@ extension Bool { mutating set { self = newValue.toggledValue } } } + +enum MenuBarIconType: String, CaseIterable, Defaults.Serializable, Identifiable { + var id: Self { self } + + case square = "square_menu_bar_icon" + case rounded = "rounded_menu_bar_icon" +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift index 26e427fee..a1e59e04e 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift @@ -11,6 +11,8 @@ import SwiftUI @available(macOS 13, *) struct GeneralTab: View { + @Environment(\.colorScheme) var colorScheme + var body: some View { Form { Section { @@ -136,6 +138,16 @@ struct GeneralTab: View { Toggle(isOn: $hideMenuBarIcon) { Text("hide_menu_bar_icon") } + Picker( + "modify_menubar_icon", + selection: $selectedMenuBarIcon + ) { + ForEach(MenuBarIconType.allCases) { option in + Image(option.rawValue) + .renderingMode(.template) + .foregroundStyle(.primary) + } + } } header: { Text("setting.general.other.header") } @@ -180,6 +192,7 @@ struct GeneralTab: View { @Default(.appearanceType) private var appearanceType @Default(.fontSizeOptionIndex) private var fontSizeOptionIndex + @Default(.selectedMenuBarIcon) private var selectedMenuBarIcon } @available(macOS 13, *) From b559959859b9d782428a8801e474dfd8787aebd6 Mon Sep 17 00:00:00 2001 From: Sharker <1548742234@qq.com> Date: Fri, 2 Feb 2024 12:55:26 +0800 Subject: [PATCH 49/72] Add shortcut with keyholder (#349) * feat: add shortcut * feat: add shortcut * feat: binding shortcut * feat: shortcut in setting page * fix: move shortcut setup into appdelegate * feat: add show mini window * chore: try add shortcut in menu item * chore: remove unuse code * chore: add menu shortcut * fix: use old shortcut key cuase crash in new settings * feat: add shortcut tab page * feat: add shortcut into menu item * fix: unable to delete shortcut & menubar shortcut update & refactor using defaults bridge * feat: add shortcut into new page * feat: add shortcut confict validator * feat: shortcut confict alter * feat: shortcut confict msg * fix: string formate * fix: unused string in xcstrings * fix: add advanced en string * pref: UI optimize * fix: marked the localized as reviewed * fix: review problem * fix: optimize the shortcut confict alter message * fix: optimize alter message * fix: remove the duplicate package.resloved --------- Co-authored-by: Tisfeng Co-authored-by: Lava <34743145+CanglongCl@users.noreply.github.com> --- Easydict.xcodeproj/project.pbxproj | 83 +++++++ .../xcshareddata/swiftpm/Package.resolved | 27 +++ Easydict/App/AppDelegate.m | 5 +- Easydict/App/Easydict-Bridging-Header.h | 2 + Easydict/App/Localizable.xcstrings | 135 ++++++++++- .../Configuration+Defaults.swift | 10 + .../NewApp/Feature/Shortcut/Shortcut.swift | 229 ++++++++++++++++++ .../Feature/Shortcut/ShortcutValidator.swift | 101 ++++++++ .../KeyCombo+Defaults.Serializable.swift | 31 +++ Easydict/NewApp/View/MenuItemView.swift | 5 + .../NewApp/View/SettingView/SettingView.swift | 6 +- .../View/SettingView/Tabs/ShortcutTab.swift | 25 ++ .../Shortcut/GeneralKeyHolderWrapper.swift | 115 +++++++++ .../Shortcut/GeneralShortcutSetting.swift | 69 ++++++ 14 files changed, 829 insertions(+), 14 deletions(-) create mode 100644 Easydict/NewApp/Feature/Shortcut/Shortcut.swift create mode 100644 Easydict/NewApp/Feature/Shortcut/ShortcutValidator.swift create mode 100644 Easydict/NewApp/Utility/Extensions/Defaults/KeyCombo+Defaults.Serializable.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralKeyHolderWrapper.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index d61b66620..9e8730358 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -259,7 +259,13 @@ 62E2BF4B2B4082BA00E42D38 /* AliResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E2BF482B4082BA00E42D38 /* AliResponse.swift */; }; 62E2BF4C2B4082BA00E42D38 /* AliTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E2BF492B4082BA00E42D38 /* AliTranslateType.swift */; }; 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 62ED29A12B15F1F500901F51 /* EZWrapView.m */; }; + 960835502B6791F200C6A931 /* ShortcutValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9608354F2B6791F200C6A931 /* ShortcutValidator.swift */; }; + 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96099AE12B5D40330055C4DD /* ShortcutTab.swift */; }; + 9627F9382B59956800B1E999 /* GeneralShortcutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9352B59956800B1E999 /* GeneralShortcutSetting.swift */; }; + 9627F9392B59956800B1E999 /* GeneralKeyHolderWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9362B59956800B1E999 /* GeneralKeyHolderWrapper.swift */; }; 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */; }; + 967712EA2B5B913600105E0F /* KeyHolder in Frameworks */ = {isa = PBXBuildFile; productRef = 967712E92B5B913600105E0F /* KeyHolder */; }; + 967712EE2B5B943400105E0F /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967712ED2B5B943400105E0F /* Shortcut.swift */; }; A0B65CA0F31AC8ECFB8347CC /* Pods_EasydictTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 378E73A7EA8FC8FB9C975A63 /* Pods_EasydictTests.framework */; }; B87AC7E36367075BA5D13234 /* Pods_Easydict.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */; }; C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415C0AC2B450D4800A9D231 /* GeminiService.swift */; }; @@ -272,6 +278,8 @@ DC46DF802B4417B900DEAE3E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC46DF7F2B4417B900DEAE3E /* Configuration.swift */; }; DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */; }; DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C882B3969510055EFFC /* Appearance.swift */; }; + EA1013442B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */; }; + EA3B81F92B5254AA004C0E8B /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration.swift */; }; DCF176F22B57CED700CA6026 /* Configuration+UserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */; }; EA3B81F92B5254AA004C0E8B /* Configuration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */; }; EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = EA3B81FB2B52555C004C0E8B /* Defaults */; }; @@ -756,8 +764,13 @@ 62ED29A12B15F1F500901F51 /* EZWrapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZWrapView.m; sourceTree = ""; }; 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Easydict.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91E3E579C6DB88658B4BB102 /* Pods-Easydict.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.release.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.release.xcconfig"; sourceTree = ""; }; + 9608354F2B6791F200C6A931 /* ShortcutValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutValidator.swift; sourceTree = ""; }; + 96099AE12B5D40330055C4DD /* ShortcutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutTab.swift; sourceTree = ""; }; + 9627F9352B59956800B1E999 /* GeneralShortcutSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralShortcutSetting.swift; sourceTree = ""; }; + 9627F9362B59956800B1E999 /* GeneralKeyHolderWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralKeyHolderWrapper.swift; sourceTree = ""; }; 9672D7D02B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MASShortcutBinder+EZMASShortcutBinder.h"; sourceTree = ""; }; 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutBinder+EZMASShortcutBinder.m"; sourceTree = ""; }; + 967712ED2B5B943400105E0F /* Shortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcut.swift; sourceTree = ""; }; A230E9A2358C7FBC7FB26189 /* Pods-EasydictTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EasydictTests.debug.xcconfig"; path = "Target Support Files/Pods-EasydictTests/Pods-EasydictTests.debug.xcconfig"; sourceTree = ""; }; C415C0AC2B450D4800A9D231 /* GeminiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiService.swift; sourceTree = ""; }; C4DD01E82B12B3C80025EE8E /* TencentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentService.swift; sourceTree = ""; }; @@ -771,6 +784,8 @@ DC46DF7F2B4417B900DEAE3E /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeHintView.swift; sourceTree = ""; }; DC6D9C882B3969510055EFFC /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; + EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyCombo+Defaults.Serializable.swift"; sourceTree = ""; }; + EA3B81F82B5254AA004C0E8B /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+UserData.swift"; sourceTree = ""; }; EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+Defaults.swift"; sourceTree = ""; }; EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSServiceType.swift; sourceTree = ""; }; @@ -827,6 +842,7 @@ EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */, 03CF27FE2B3DA7D500E19B57 /* Realm in Frameworks */, 03A830902B4073E700112834 /* AppCenterAnalytics in Frameworks */, + 967712EA2B5B913600105E0F /* KeyHolder in Frameworks */, 03B63ABF2A86967800E155ED /* CoreServices.framework in Frameworks */, 038030972B4106800009230C /* CocoaLumberjackSwift in Frameworks */, 038EA1AA2B41169C008A6DD1 /* ZipArchive in Frameworks */, @@ -2079,6 +2095,7 @@ 27FE98032B3DCA9F000AD654 /* NewApp */ = { isa = PBXGroup; children = ( + 967712EB2B5B93E200105E0F /* Feature */, EA9943E12B534C2900EE7B97 /* Model */, EA9943DD2B534BAE00EE7B97 /* Utility */, EA3B81F72B52549B004C0E8B /* Configuration */, @@ -2112,12 +2129,14 @@ 27FE980C2B3DD749000AD654 /* Tabs */ = { isa = PBXGroup; children = ( + 9627F9332B59956800B1E999 /* View */, EAED41EA2B54A4900005FE0A /* ServiceConfiguration */, 278540332B3DE04F004E9488 /* GeneralTab.swift */, 0A057D6C2B499A000025C51D /* ServiceTab.swift */, 0A8685C72B552A590022534F /* DisabledAppTab.swift */, 276742042B3DC230002A2C75 /* PrivacyTab.swift */, 276742052B3DC230002A2C75 /* AboutTab.swift */, + 96099AE12B5D40330055C4DD /* ShortcutTab.swift */, 03832F532B5F6BE200D0DC64 /* AdvancedTab.swift */, ); path = Tabs; @@ -2170,6 +2189,40 @@ name = Frameworks; sourceTree = ""; }; + 9627F9332B59956800B1E999 /* View */ = { + isa = PBXGroup; + children = ( + 9627F9342B59956800B1E999 /* Shortcut */, + ); + path = View; + sourceTree = ""; + }; + 9627F9342B59956800B1E999 /* Shortcut */ = { + isa = PBXGroup; + children = ( + 9627F9352B59956800B1E999 /* GeneralShortcutSetting.swift */, + 9627F9362B59956800B1E999 /* GeneralKeyHolderWrapper.swift */, + ); + path = Shortcut; + sourceTree = ""; + }; + 967712EB2B5B93E200105E0F /* Feature */ = { + isa = PBXGroup; + children = ( + 967712EC2B5B941600105E0F /* Shortcut */, + ); + path = Feature; + sourceTree = ""; + }; + 967712EC2B5B941600105E0F /* Shortcut */ = { + isa = PBXGroup; + children = ( + 967712ED2B5B943400105E0F /* Shortcut.swift */, + 9608354F2B6791F200C6A931 /* ShortcutValidator.swift */, + ); + path = Shortcut; + sourceTree = ""; + }; 9CB57B9B45EC322A11ED8865 /* Pods */ = { isa = PBXGroup; children = ( @@ -2244,6 +2297,14 @@ path = ChangeFontSizeView; sourceTree = ""; }; + EA1013412B5DBDA5005E43F9 /* Defaults */ = { + isa = PBXGroup; + children = ( + EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */, + ); + path = Defaults; + sourceTree = ""; + }; EA3B81F72B52549B004C0E8B /* Configuration */ = { isa = PBXGroup; children = ( @@ -2273,6 +2334,7 @@ EA9943E62B534D7C00EE7B97 /* Extensions */ = { isa = PBXGroup; children = ( + EA1013412B5DBDA5005E43F9 /* Defaults */, 038A723E2B62C07B004995E3 /* String */, EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */, EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */, @@ -2399,6 +2461,7 @@ 038EA1A92B41169C008A6DD1 /* ZipArchive */, 038EA1AC2B41282F008A6DD1 /* MJExtension */, EA3B81FB2B52555C004C0E8B /* Defaults */, + 967712E92B5B913600105E0F /* KeyHolder */, 03022F182B3591AE00B63209 /* GoogleGenerativeAI */, ); productName = Bob; @@ -2459,6 +2522,7 @@ 038EA1A82B41169C008A6DD1 /* XCRemoteSwiftPackageReference "ZipArchive" */, 038EA1AB2B41282F008A6DD1 /* XCRemoteSwiftPackageReference "MJExtension" */, EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */, + 967712E82B5B913600105E0F /* XCRemoteSwiftPackageReference "KeyHolder" */, 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */, ); productRefGroup = C99EEB192385796700FEE666 /* Products */; @@ -2675,6 +2739,7 @@ 62E2BF4A2B4082BA00E42D38 /* AliService.swift in Sources */, 03B0233729231FA6001C7E63 /* MMMake.m in Sources */, 03B0232E29231FA6001C7E63 /* MMCrashSignalExceptionHandler.m in Sources */, + 9627F9382B59956800B1E999 /* GeneralShortcutSetting.swift in Sources */, 03BDA7C42A26DA280079D04F /* NSDictionary+RubyDescription.m in Sources */, 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */, C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */, @@ -2699,6 +2764,7 @@ 03B3B8B22925D5B200168E8D /* EZPopButtonWindow.m in Sources */, 03B0231529231FA6001C7E63 /* SnipWindow.m in Sources */, 033363A0293A05D200FED9C8 /* EZSelectLanguageButton.m in Sources */, + 960835502B6791F200C6A931 /* ShortcutValidator.swift in Sources */, 03542A522937B69200C34C33 /* EZYoudaoTranslateResponse.m in Sources */, 03B0230129231FA6001C7E63 /* EZQueryView.m in Sources */, 03542A3D2937AF4F00C34C33 /* EZQueryResult.m in Sources */, @@ -2729,11 +2795,13 @@ 03882F8F29D95044005B5A52 /* CTScreen.m in Sources */, 27FE980B2B3DD5D1000AD654 /* MenuItemView.swift in Sources */, 03DC7C6A2A3CA852000BF7C9 /* EZAppCell.m in Sources */, + 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */, 0399C6AC29A860AA00B4AFCC /* EZOpenAIService.m in Sources */, 03542A432937B45E00C34C33 /* EZBaiduTranslate.m in Sources */, 03BB2DEB29F57DC000447EDD /* NSImage+EZSymbolmage.m in Sources */, 03B0230629231FA6001C7E63 /* EZLabel.m in Sources */, 03F25CB329327BC200E66A12 /* EZShortcut.m in Sources */, + EA1013442B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift in Sources */, 033B7134293CE2430096E2DF /* EZWebViewTranslator.m in Sources */, 03CF88632B137F650030C199 /* Array+Convenience.swift in Sources */, 03B0231229231FA6001C7E63 /* NSObject+DarkMode.m in Sources */, @@ -2773,6 +2841,7 @@ 03542A342936F70F00C34C33 /* EZLanguageManager.m in Sources */, 6295DE312A84D82E006145F4 /* EZBingTranslateModel.m in Sources */, 0361967B2A0037F700806370 /* NSData+EZMD5.m in Sources */, + 967712EE2B5B943400105E0F /* Shortcut.swift in Sources */, 03BFFC68295F4B87004E033E /* EZYoudaoDictModel.m in Sources */, 03247E3A296AE8EC00AFCD67 /* EZLoadingAnimationView.m in Sources */, 0396D615292CC4C3006A11D9 /* EZLocalStorage.m in Sources */, @@ -2831,6 +2900,7 @@ 03B0233129231FA6001C7E63 /* MMCrash.m in Sources */, 03B0232629231FA6001C7E63 /* NSAttributedString+MM.m in Sources */, 03542A402937B3C900C34C33 /* EZOCRResult.m in Sources */, + 9627F9392B59956800B1E999 /* GeneralKeyHolderWrapper.swift in Sources */, C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */, 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */, EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */, @@ -3450,6 +3520,14 @@ minimumVersion = 5.8.1; }; }; + 967712E82B5B913600105E0F /* XCRemoteSwiftPackageReference "KeyHolder" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Clipy/KeyHolder.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.2.0; + }; + }; EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/Defaults.git"; @@ -3541,6 +3619,11 @@ package = 2721E4CE2AFE920700A059AC /* XCRemoteSwiftPackageReference "Alamofire" */; productName = Alamofire; }; + 967712E92B5B913600105E0F /* KeyHolder */ = { + isa = XCSwiftPackageProductDependency; + package = 967712E82B5B913600105E0F /* XCRemoteSwiftPackageReference "KeyHolder" */; + productName = KeyHolder; + }; EA3B81FB2B52555C004C0E8B /* Defaults */ = { isa = XCSwiftPackageProductDependency; package = EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */; diff --git a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3c8f58191..0d2cecd07 100644 --- a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -144,6 +144,15 @@ "version" : "100.0.0" } }, + { + "identity" : "keyholder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Clipy/KeyHolder.git", + "state" : { + "revision" : "5da0ee06fd8709f7d812504bff1555462a3f6956", + "version" : "4.2.0" + } + }, { "identity" : "leveldb", "kind" : "remoteSourceControl", @@ -153,6 +162,15 @@ "version" : "1.22.3" } }, + { + "identity" : "magnet", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Clipy/Magnet", + "state" : { + "revision" : "26d672a031fd33eac94887b4e19aca0ab4761f45", + "version" : "3.4.0" + } + }, { "identity" : "mjextension", "kind" : "remoteSourceControl", @@ -207,6 +225,15 @@ "version" : "10.45.2" } }, + { + "identity" : "sauce", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Clipy/Sauce", + "state" : { + "revision" : "8f8fabaa8509c1a653d6c2c3c87396a4c493d876", + "version" : "2.4.0" + } + }, { "identity" : "snapkit", "kind" : "remoteSourceControl", diff --git a/Easydict/App/AppDelegate.m b/Easydict/App/AppDelegate.m index f4dc71d53..4d3f7c9ee 100644 --- a/Easydict/App/AppDelegate.m +++ b/Easydict/App/AppDelegate.m @@ -29,9 +29,10 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { if (!EasydictNewAppManager.shared.enable) { [EZMenuItemManager.shared setup]; + [EZShortcut setup]; + } else { + [Shortcut setupShortcut]; } - - [EZShortcut setup]; [EZWindowManager.shared showMainWindowIfNedded]; diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 7bcde5af5..66f840155 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -32,3 +32,5 @@ #import "DarkModeManager.h" #import "EZScriptExecutor.h" #import "EZOpenAIService.h" + + diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 0819e750d..6e2c2cf10 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -46,6 +46,12 @@ }, "advanced" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Advanced" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1326,7 +1332,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "Gemini 翻译" } } @@ -1342,6 +1348,23 @@ } } }, + "global_shortcut_setting" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Global Shortcut" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "全局快捷键" + } + } + } + }, "google_translate" : { "localizations" : { "en" : { @@ -1487,13 +1510,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Input Translate:" + "value" : "Input Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "输入翻译:" + "value" : "输入翻译" } } } @@ -2329,13 +2352,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Select Text Translate:" + "value" : "Select Text Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "划词翻译:" + "value" : "划词翻译" } } } @@ -3063,6 +3086,96 @@ } } }, + "shortcut" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷键" + } + } + } + }, + "shortcut_confict %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@" + } + } + } + }, + "shortcut_confict_confirm" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "确定" + } + } + } + }, + "shortcut_confict_message (Shortcut.shared.confictMenuItem?.title ?? \"\")" : { + + }, + "shortcut_confict_message %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The current shortcut can't be used because it is already used \"%@\"" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "当前快捷键无法使用,因为它已被\n\"%@\"占用" + } + } + } + }, + "shortcut_confict_title (keyCombo!.keyEquivalentModifierMaskString + keyCombo!.characters)" : { + + }, + "shortcut_confict_title %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Shortcut Confict With \"%@\" Unable Use This" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "快捷键\"%@\",无法使用" + } + } + } + }, "shortcut_select_translate_window_type" : { "localizations" : { "en" : { @@ -3164,13 +3277,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Show Mini Window:" + "value" : "Show Mini Window" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "显示迷你窗口:" + "value" : "显示迷你窗口" } } } @@ -3180,13 +3293,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Silent Screenshot OCR:" + "value" : "Silent Screenshot OCR" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "静默截图 OCR:" + "value" : "静默截图 OCR" } } } @@ -3229,13 +3342,13 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Snip Translate:" + "value" : "Snip Translate" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "截图翻译:" + "value" : "截图翻译" } } } diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index b8ab739dd..6b529ec6b 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -8,6 +8,7 @@ import Defaults import Foundation +import Magnet // Setting extension Defaults.Keys { @@ -162,3 +163,12 @@ extension Defaults.Keys { static let aliAccessKeyId = Key("EZAliAccessKeyId") static let aliAccessKeySecret = Key("EZAliAccessKeySecret") } + +/// shortcut +extension Defaults.Keys { + static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder", default: nil) + static let snipShortcut = Key("EZSnipShortcutKey_keyHolder", default: nil) + static let inputShortcut = Key("EZInputShortcutKey_keyHolder", default: nil) + static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder", default: nil) + static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder", default: nil) +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift new file mode 100644 index 000000000..434c33c77 --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift @@ -0,0 +1,229 @@ +// +// Shortcut.swift +// Easydict +// +// Created by Sharker on 2024/1/20. +// Copyright © 2024 izual. All rights reserved. + +import Defaults +import Foundation +import KeyHolder +import Magnet +import SwiftUI + +/// Shortcut Service +public enum ShortcutType: String { + case inputTranslate = "EZInputShortcutKey" + case snipTranslate = "EZSnipShortcutKey" + case selectTranslate = "EZSelectionShortcutKey" + case silentScreenshotOcr = "EZScreenshotOCRShortcutKey" + case showMiniWindow = "EZShowMiniShortcutKey" +} + +// Confict Message +public struct ShortcutConfictAlertMessage: Identifiable { + public var id: String { message } + var title: String + var message: String +} + +class Shortcut: NSObject { + var confictMenuItem: NSMenuItem? + + static let shared = Shortcut() + + @objc static func setupShortcut() { + let shortcut = Shortcut.shared + shortcut.restoreShortcut() + } + + // Make sure the class has only one instance + // Should not init or copy outside + override private init() {} + + override func copy() -> Any { + self // SingletonClass.shared + } + + override func mutableCopy() -> Any { + self // SingletonClass.shared + } + + // Optional + func reset() { + // Reset all properties to default value + } +} + +// restore shortcut +extension Shortcut { + func restoreShortcut() { + // inputTranslate + bindingShortcut(keyCombo: Defaults[.inputShortcut], type: .inputTranslate) + // snipTranslate + bindingShortcut(keyCombo: Defaults[.snipShortcut], type: .snipTranslate) + // selectTranslate + bindingShortcut(keyCombo: Defaults[.selectionShortcut], type: .selectTranslate) + // silentScreenshotOcr + bindingShortcut(keyCombo: Defaults[.screenshotOCRShortcut], type: .silentScreenshotOcr) + // showMiniWindow + bindingShortcut(keyCombo: Defaults[.showMiniWindowShortcut], type: .showMiniWindow) + } +} + +// binding shortcut +extension Shortcut { + func bindingShortcut(keyCombo: KeyCombo?, type: ShortcutType) { + guard let keyCombo else { + HotKeyCenter.shared.unregisterHotKey(with: type.rawValue) + return + } + var hotKey: HotKey + switch type { + case .inputTranslate: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.inputTranslate)) + case .snipTranslate: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.snipTranslate)) + case .selectTranslate: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.selectTextTranslate)) + case .silentScreenshotOcr: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.screenshotOCR)) + case .showMiniWindow: + hotKey = HotKey(identifier: type.rawValue, + keyCombo: keyCombo, + target: Shortcut.shared, + action: #selector(Shortcut.showMiniFloatingWindow)) + } + hotKey.register() + } +} + +// shortcut binding func +extension Shortcut { + @objc func selectTextTranslate() { + EZWindowManager.shared().selectTextTranslate() + } + + @objc func snipTranslate() { + EZWindowManager.shared().snipTranslate() + } + + @objc func inputTranslate() { + EZWindowManager.shared().inputTranslate() + } + + @objc func showMiniFloatingWindow() { + EZWindowManager.shared().showMiniFloatingWindow() + } + + @objc func screenshotOCR() { + EZWindowManager.shared().screenshotOCR() + } +} + +// fetch shortcut KeyCombo +extension Shortcut { + public func shortcutKeyCombo(_ type: ShortcutType) -> KeyCombo? { + switch type { + case .inputTranslate: + guard let keyCombo = Defaults[.inputShortcut] else { return nil } + return keyCombo + case .snipTranslate: + guard let keyCombo = Defaults[.snipShortcut] else { return nil } + return keyCombo + case .selectTranslate: + guard let keyCombo = Defaults[.selectionShortcut] else { return nil } + return keyCombo + case .silentScreenshotOcr: + guard let keyCombo = Defaults[.screenshotOCRShortcut] else { return nil } + return keyCombo + case .showMiniWindow: + guard let keyCombo = Defaults[.showMiniWindowShortcut] else { return nil } + return keyCombo + } + } +} + +struct KeyboardShortcut: ViewModifier { + init(type: ShortcutType) { + let key: Defaults.Key = switch type { + case .inputTranslate: + .inputShortcut + case .snipTranslate: + .snipShortcut + case .selectTranslate: + .selectionShortcut + case .silentScreenshotOcr: + .screenshotOCRShortcut + case .showMiniWindow: + .showMiniWindowShortcut + } + + _shortcut = .init(key) + } + + @Default var shortcut: KeyCombo? + + func body(content: Content) -> some View { + if let shortcut { + content + .keyboardShortcut( + fetchShortcutKeyEquivalent(shortcut), + modifiers: fetchShortcutKeyEventModifiers(shortcut) + ) + } else { + content + } + } + + private func fetchShortcutKeyEquivalent(_ keyCombo: KeyCombo) -> KeyEquivalent { + if keyCombo.doubledModifiers { + return KeyEquivalent(Character(keyCombo.keyEquivalentModifierMaskString)) + } else { + return KeyEquivalent(Character(keyCombo.keyEquivalent)) + } + } + + private func fetchShortcutKeyEventModifiers(_ keyCombo: KeyCombo) -> EventModifiers { + var modifiers: EventModifiers = [] + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.command) { + modifiers.update(with: EventModifiers.command) + } + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.control) { + modifiers.update(with: EventModifiers.control) + } + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.option) { + modifiers.update(with: EventModifiers.option) + } + + if keyCombo.keyEquivalentModifierMask.contains(NSEvent.ModifierFlags.shift) { + modifiers.update(with: EventModifiers.shift) + } + + return modifiers + } +} + +/// can't using keyEquivalent and EventModifiers in SwiftUI MenuItemView direct, because item +/// keyboardShortcut not support double modifier key but can use ⌥ as character +public extension View { + @ViewBuilder + func keyboardShortcut(_ type: ShortcutType) -> some View { + modifier(KeyboardShortcut(type: type)) + } +} diff --git a/Easydict/NewApp/Feature/Shortcut/ShortcutValidator.swift b/Easydict/NewApp/Feature/Shortcut/ShortcutValidator.swift new file mode 100644 index 000000000..0f818a228 --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/ShortcutValidator.swift @@ -0,0 +1,101 @@ +// +// ShortcutValidator.swift +// Easydict +// +// Created by Sharker on 2024/1/29. +// Copyright © 2024 izual. All rights reserved. +// + +import Carbon +import Carbon.HIToolbox +import Foundation +import KeyHolder +import Magnet +import Sauce + +extension Shortcut { + static func validateShortcut(_ keyCombo: KeyCombo) -> Bool { + validateShortcutConfictBySystem(keyCombo) || + validateShortcutConfictByMenuItem(keyCombo) || + validateShortcutConfictByCustom(keyCombo) + } +} + +// validate shortcut used by system +// ref: https://github.com/cocoabits/MASShortcut/blob/6f2603c6b6cc18f64a799e5d2c9d3bbc467c413a/Framework/Model/MASShortcutValidator.m#L94 +extension Shortcut { + static func validateShortcutConfictBySystem(_ keyCombo: KeyCombo) -> Bool { + systemUsedShortcut().contains(keyCombo) + } + + static func systemUsedShortcut() -> [KeyCombo] { + var shortcutsUnmanaged: Unmanaged? + guard + CopySymbolicHotKeys(&shortcutsUnmanaged) == noErr, + let shortcuts = shortcutsUnmanaged?.takeRetainedValue() as? [[String: Any]] + else { + assertionFailure("Could not get system keyboard shortcuts") + return [] + } + return shortcuts.compactMap { + guard + ($0[kHISymbolicHotKeyEnabled] as? Bool) == true, + let carbonKeyCode = $0[kHISymbolicHotKeyCode] as? Int, + let carbonModifiers = $0[kHISymbolicHotKeyModifiers] as? Int + else { + return nil + } + guard let key = Sauce.shared.key(for: Int(carbonKeyCode)) else { return nil } + guard let keyCombo = KeyCombo(key: key, carbonModifiers: carbonModifiers) else { return nil } + return keyCombo + } + } +} + +// validate shortcut used by menuItem +extension Shortcut { + static func validateShortcutConfictByMenuItem(_ keyCombo: KeyCombo) -> Bool { + if let item = menuItemUsedShortcut(keyCombo) { + Shortcut.shared.confictMenuItem = item + return true + } else { + return false + } + } + + static func menuItemUsedShortcut(_ keyCombo: KeyCombo) -> NSMenuItem? { + guard let mainMenu = NSApp.mainMenu else { + return nil + } + return menuItemWithMatchingShortcut(in: mainMenu, keyCombo: keyCombo) + } + + static func menuItemWithMatchingShortcut(in menu: NSMenu, keyCombo: KeyCombo) -> NSMenuItem? { + for item in menu.items { + let keyEquivalent = item.keyEquivalent + let keyEquivalentModifierMask = item.keyEquivalentModifierMask + if keyCombo.keyEquivalent == keyEquivalent, + keyCombo.keyEquivalentModifierMask == keyEquivalentModifierMask, + keyCombo.keyEquivalent != "" + { + return item + } + if let submenu = item.submenu, + let menuItem = menuItemWithMatchingShortcut(in: submenu, keyCombo: keyCombo) + { + return menuItem + } + } + return nil + } +} + +// validate shortcut used by custom +// ref: https://support.apple.com/zh-cn/HT201236 +extension Shortcut { + static func validateShortcutConfictByCustom(_: KeyCombo) -> Bool { + false + } + + static func customUsedShortcut(_: KeyCombo) {} +} diff --git a/Easydict/NewApp/Utility/Extensions/Defaults/KeyCombo+Defaults.Serializable.swift b/Easydict/NewApp/Utility/Extensions/Defaults/KeyCombo+Defaults.Serializable.swift new file mode 100644 index 000000000..6858aa387 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/Defaults/KeyCombo+Defaults.Serializable.swift @@ -0,0 +1,31 @@ +// +// KeyCombo+Defaults.Serializable.swift +// Easydict +// +// Created by 戴藏龙 on 2024/1/21. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation +import Magnet + +extension KeyCombo: Defaults.Serializable { + public static var bridge = ShortcutBridge() + + public struct ShortcutBridge: Defaults.Bridge { + public func serialize(_ value: Magnet.KeyCombo??) -> Data? { + guard let value else { return nil } + return try? JSONEncoder().encode(value) + } + + public func deserialize(_ object: Data?) -> Magnet.KeyCombo?? { + guard let data = object else { return nil } + return try? JSONDecoder().decode(KeyCombo.self, from: data) as Magnet.KeyCombo? + } + + public typealias Value = KeyCombo? + + public typealias Serializable = Data + } +} diff --git a/Easydict/NewApp/View/MenuItemView.swift b/Easydict/NewApp/View/MenuItemView.swift index 7b25db502..2c5a6aca4 100644 --- a/Easydict/NewApp/View/MenuItemView.swift +++ b/Easydict/NewApp/View/MenuItemView.swift @@ -32,11 +32,16 @@ struct MenuItemView: View { versionItem Divider() inputItem + .keyboardShortcut(.inputTranslate) screenshotItem + .keyboardShortcut(.snipTranslate) selectWordItem + .keyboardShortcut(.selectTranslate) miniWindowItem + .keyboardShortcut(.showMiniWindow) Divider() ocrItem + .keyboardShortcut(.silentScreenshotOcr) Divider() settingItem .keyboardShortcut(.init(",")) diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index 81333b8ee..bb631a6e0 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -13,6 +13,7 @@ enum SettingTab: Int { case service case disabled case advanced + case shortcut case privacy case about } @@ -39,6 +40,9 @@ struct SettingView: View { .tabItem { Label("advanced", systemImage: "gearshape.2") } .tag(SettingTab.advanced) + ShortcutTab() + .tabItem { Label("shortcut", systemImage: "command.square") } + .tag(SettingTab.shortcut) PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } .tag(SettingTab.privacy) @@ -69,7 +73,7 @@ struct SettingView: View { let height = switch selection { case .general: maxWidth - case .service, .disabled: + case .service, .disabled, .shortcut: 500 case .privacy: 320 diff --git a/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift new file mode 100644 index 000000000..52daaabe6 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift @@ -0,0 +1,25 @@ +// +// ShortcutTab.swift +// Easydict +// +// Created by Sharker on 2024/1/21. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +struct ShortcutTab: View { + var body: some View { + Form { + // Global shortcut + GeneralShortcutSettingView() + // In app shortcut + }.formStyle(.grouped) + } +} + +@available(macOS 13, *) +#Preview { + ShortcutTab() +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralKeyHolderWrapper.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralKeyHolderWrapper.swift new file mode 100644 index 000000000..e94dfda4e --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralKeyHolderWrapper.swift @@ -0,0 +1,115 @@ +// +// GeneralKeyHolderWrapper.swift +// Easydict +// +// Created by Sharker on 2024/1/2. +// Copyright © 2024 izual. All rights reserved. +// + +import AppKit +import Defaults +import KeyHolder +import Magnet +import SwiftUI + +struct GeneralKeyHolderWrapper: NSViewRepresentable { + func makeCoordinator() -> Coordinator { + .init(shortcutType: type, confictAlterMessage: $confictAlterMessage) + } + + private var type: ShortcutType + @Binding var confictAlterMessage: ShortcutConfictAlertMessage + init(shortcutType: ShortcutType, confictAlterMessage: Binding) { + type = shortcutType + _confictAlterMessage = confictAlterMessage + } + + func makeNSView(context: Context) -> some NSView { + let recordView = RecordView(frame: CGRect.zero) + recordView.tintColor = NSColor(red: 0.164, green: 0.517, blue: 0.823, alpha: 1) + recordView.delegate = context.coordinator + recordView.layer?.cornerRadius = 6.0 + recordView.layer?.masksToBounds = true + recordView.clearButtonMode = .whenRecorded + + context.coordinator.restoreKeyCombo(recordView) + return recordView + } + + func updateNSView(_: NSViewType, context _: Context) {} +} + +extension GeneralKeyHolderWrapper { + class Coordinator: NSObject, RecordViewDelegate { + private var type: ShortcutType + @Binding var confictAlterMessage: ShortcutConfictAlertMessage + init(shortcutType: ShortcutType, confictAlterMessage: Binding) { + type = shortcutType + _confictAlterMessage = confictAlterMessage + } + + func recordViewShouldBeginRecording(_: KeyHolder.RecordView) -> Bool { + true + } + + func recordView(_: KeyHolder.RecordView, canRecordKeyCombo _: Magnet.KeyCombo) -> Bool { + true + } + + func recordViewDidEndRecording(_: RecordView) {} + + func recordView(_ recordView: RecordView, didChangeKeyCombo keyCombo: KeyCombo?) { + if let key = keyCombo { + // shortcut validate confict + if Shortcut.validateShortcut(key) { + if #available(macOS 12, *) { + confictAlterMessage = ShortcutConfictAlertMessage(title: String(localized: "shortcut_confict_title \(keyCombo!.keyEquivalentModifierMaskString + keyCombo!.characters)"), message: String(localized: "shortcut_confict_message \(Shortcut.shared.confictMenuItem?.title ?? "")")) + } else { + // Fallback on earlier versions + let title = NSLocalizedString("shortcut_confict_title \(keyCombo!.keyEquivalentModifierMaskString + keyCombo!.characters)", comment: "") + let msg = NSLocalizedString("shortcut_confict_message \(Shortcut.shared.confictMenuItem?.title ?? "")", comment: "") + confictAlterMessage = ShortcutConfictAlertMessage(title: title, message: msg) + } + recordView.clear() + return + } + } + storeKeyCombo(with: keyCombo) + Shortcut.shared.bindingShortcut(keyCombo: keyCombo, type: type) + } + + func restoreKeyCombo(_ recordView: RecordView) { + var keyCombo: KeyCombo? + switch type { + case .inputTranslate: + keyCombo = Defaults[.inputShortcut] + case .snipTranslate: + keyCombo = Defaults[.snipShortcut] + case .selectTranslate: + keyCombo = Defaults[.selectionShortcut] + case .silentScreenshotOcr: + keyCombo = Defaults[.screenshotOCRShortcut] + case .showMiniWindow: + keyCombo = Defaults[.showMiniWindowShortcut] + } + recordView.keyCombo = keyCombo + Shortcut.shared.bindingShortcut(keyCombo: keyCombo, type: type) + } + + // shortcut + func storeKeyCombo(with keyCombo: KeyCombo?) { + switch type { + case .inputTranslate: + Defaults[.inputShortcut] = keyCombo + case .snipTranslate: + Defaults[.snipShortcut] = keyCombo + case .selectTranslate: + Defaults[.selectionShortcut] = keyCombo + case .silentScreenshotOcr: + Defaults[.screenshotOCRShortcut] = keyCombo + case .showMiniWindow: + Defaults[.showMiniWindowShortcut] = keyCombo + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift new file mode 100644 index 000000000..7387a5b32 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift @@ -0,0 +1,69 @@ +// +// GeneralShortcutSetting.swift +// Easydict +// +// Created by Sharker on 2024/1/1. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +extension ShortcutTab { + struct GeneralShortcutSettingView: View { + @State var confictAlterMessage: ShortcutConfictAlertMessage = .init(title: "", message: "") + + var body: some View { + let showAlter = Binding( + get: { + if !confictAlterMessage.message.isEmpty { + true + } else { + false + } + }, set: { _ in + } + ) + Section { + HStack { + Text("input_translate") + Spacer() + GeneralKeyHolderWrapper(shortcutType: .inputTranslate, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) + } + HStack { + Text("snip_translate") + Spacer() + GeneralKeyHolderWrapper(shortcutType: .snipTranslate, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) + } + HStack { + Text("select_translate") + Spacer() + GeneralKeyHolderWrapper(shortcutType: .selectTranslate, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) + } + HStack { + Text("show_mini_window") + Spacer() + GeneralKeyHolderWrapper(shortcutType: .showMiniWindow, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) + } + HStack { + Text("silent_screenshot_ocr") + Spacer() + GeneralKeyHolderWrapper(shortcutType: .silentScreenshotOcr, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) + } + } header: { + Text("global_shortcut_setting") + } + + .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), + isPresented: showAlter, + presenting: confictAlterMessage) + { _ in + Button(String(localized: "shortcut_confict_confirm")) { + confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") + } + } message: { message in + Text(message.message) + } + } + } +} From 35e600b73cca052bc05a94abf6160cac9601c511 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Sat, 3 Feb 2024 21:48:41 +0800 Subject: [PATCH 50/72] fix: not show beta feature for macOS 13 below (#384) --- .../EZSettingViewController.m | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m index 415295202..8715ec11e 100644 --- a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m @@ -495,14 +495,16 @@ - (void)setupUI { self.hideMenuBarIconButton = [NSButton checkboxWithTitle:hideMenuBarIcon target:self action:@selector(hideMenuBarIconButtonClicked:)]; [self.contentView addSubview:self.hideMenuBarIconButton]; - NSTextField *betaNewAppLabel = [NSTextField labelWithString:NSLocalizedString(@"beta_new_app", nil)]; - betaNewAppLabel.font = font; - [self.contentView addSubview:betaNewAppLabel]; - self.betaNewAppLabel = betaNewAppLabel; - - NSString *enableBetaNewApp = NSLocalizedString(@"enable_beta_new_app", nil); - self.enableBetaNewAppButton = [NSButton checkboxWithTitle:enableBetaNewApp target:self action:@selector(enableBetaNewAppButtonClicked:)]; - [self.contentView addSubview:self.enableBetaNewAppButton]; + if (@available(macOS 13.0, *)) { + NSTextField *betaNewAppLabel = [NSTextField labelWithString:NSLocalizedString(@"beta_new_app", nil)]; + betaNewAppLabel.font = font; + [self.contentView addSubview:betaNewAppLabel]; + self.betaNewAppLabel = betaNewAppLabel; + + NSString *enableBetaNewApp = NSLocalizedString(@"enable_beta_new_app", nil); + self.enableBetaNewAppButton = [NSButton checkboxWithTitle:enableBetaNewApp target:self action:@selector(enableBetaNewAppButtonClicked:)]; + [self.contentView addSubview:self.enableBetaNewAppButton]; + } NSTextField *fontSizeLabel = [NSTextField labelWithString:NSLocalizedString(@"font_size", nil)]; fontSizeLabel.font = font; @@ -556,7 +558,9 @@ - (void)setupUI { self.showEudicQuickLinkButton.mm_isOn = self.config.showEudicQuickLink; self.showAppleDictionaryQuickLinkButton.mm_isOn = self.config.showAppleDictionaryQuickLink; self.hideMenuBarIconButton.mm_isOn = self.config.hideMenuBarIcon; - self.enableBetaNewAppButton.mm_isOn = self.config.enableBetaNewApp; + if (@available(macOS 13.0, *)) { + self.enableBetaNewAppButton.mm_isOn = self.config.enableBetaNewApp; + } } - (void)updateViewConstraints { @@ -863,18 +867,22 @@ - (void)updateViewConstraints { make.left.equalTo(self.menuBarIconLabel.mas_right).offset(self.horizontalPadding); make.centerY.equalTo(self.menuBarIconLabel); }]; - - [self.betaNewAppLabel mas_remakeConstraints:^(MASConstraintMaker *make) { - make.right.equalTo(self.autoGetSelectedTextLabel); - make.top.equalTo(self.hideMenuBarIconButton.mas_bottom).offset(self.verticalPadding); - }]; - [self.enableBetaNewAppButton mas_makeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.betaNewAppLabel.mas_right).offset(self.horizontalPadding); - make.centerY.equalTo(self.betaNewAppLabel); - }]; + if (@available(macOS 13.0, *)) { + [self.betaNewAppLabel mas_remakeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.autoGetSelectedTextLabel); + make.top.equalTo(self.hideMenuBarIconButton.mas_bottom).offset(self.verticalPadding); + }]; + [self.enableBetaNewAppButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.betaNewAppLabel.mas_right).offset(self.horizontalPadding); + make.centerY.equalTo(self.betaNewAppLabel); + }]; + self.bottommostView = self.enableBetaNewAppButton; + } else { + self.bottommostView = self.hideMenuBarIconButton; + } + self.topmostView = self.inputLabel; - self.bottommostView = self.enableBetaNewAppButton; if ([EZLanguageManager.shared isSystemChineseFirstLanguage]) { self.leftmostView = self.adjustQueryIconPostionLabel; From 2b00b9386f2bc4cb0ff9eb0e0f1f34c67c338244 Mon Sep 17 00:00:00 2001 From: Yam Liu <1056803+yam-liu@users.noreply.github.com> Date: Sun, 4 Feb 2024 11:52:33 +0800 Subject: [PATCH 51/72] feat(Select Translate): keep previous result if selected text is empty when open Select Translate (#375) * feat(Select Translate): keep previous result if selected text is empty when open Select Translate * feat(Select Translate): make auto select query text behavior when window got activated a standalone option * feat(Select Translate): Update Chinese value for key "select_query_text_when_window_activate" in Localizable.xcstrings Co-authored-by: Tisfeng --------- Co-authored-by: Tisfeng --- Easydict/App/Localizable.xcstrings | 67 ++++++++++++++----- .../Feature/Configuration/Configuration.swift | 6 ++ .../Feature/Configuration/EZConfiguration.h | 1 + .../Feature/Configuration/EZConfiguration.m | 10 +++ .../EZSettingViewController.m | 48 ++++++++++--- .../EZBaseQueryViewController.m | 3 + .../Window/WindowManager/EZWindowManager.m | 9 ++- .../Configuration+Defaults.swift | 2 + .../View/SettingView/Tabs/GeneralTab.swift | 4 ++ 9 files changed, 122 insertions(+), 28 deletions(-) diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 6e2c2cf10..257c47129 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -635,22 +635,6 @@ } } }, - "clear_input" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Clear Input:" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "清空查询内容:" - } - } - } - }, "clear_input_when_translating" : { "localizations" : { "en" : { @@ -1548,6 +1532,40 @@ } } }, + "keep_prev_result_when_selected_text_is_empty" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keep previous result when selected text is empty" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "划词翻译未选中文本时,保留上次结果" + } + } + } + }, + "keep_result" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keep Result:" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "保留结果:" + } + } + } + }, "language_detect_optimize" : { "localizations" : { "en" : { @@ -2347,6 +2365,23 @@ } } }, + "select_query_text_when_window_activate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select query text when window activate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : "打开窗口时自动选中查询文本" + } + } + } + }, "select_translate" : { "localizations" : { "en" : { diff --git a/Easydict/Feature/Configuration/Configuration.swift b/Easydict/Feature/Configuration/Configuration.swift index 7f7c6dc53..774f4a193 100644 --- a/Easydict/Feature/Configuration/Configuration.swift +++ b/Easydict/Feature/Configuration/Configuration.swift @@ -141,6 +141,12 @@ let kHideMenuBarIconKey = "EZConfiguration_kHideMenuBarIconKey" @DefaultsWrapper(.clearInput) var clearInput: Bool + @DefaultsWrapper(.keepPrevResultWhenEmpty) + var keepPrevResultWhenEmpty: Bool + + @DefaultsWrapper(.selectQueryTextWhenWindowActivate) + var selectQueryTextWhenWindowActivate: Bool + var disabledAutoSelect: Bool = false var isRecordingSelectTextShortcutKey: Bool = false diff --git a/Easydict/Feature/Configuration/EZConfiguration.h b/Easydict/Feature/Configuration/EZConfiguration.h index 266797563..5cf592d6c 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.h +++ b/Easydict/Feature/Configuration/EZConfiguration.h @@ -73,6 +73,7 @@ typedef NS_ENUM(NSUInteger, EZAppearenceType) { @property (nonatomic, assign) BOOL allowCrashLog; @property (nonatomic, assign) BOOL allowAnalytics; @property (nonatomic, assign) BOOL clearInput; +@property (nonatomic, assign) BOOL keepPrevResult; // TODO: Need to move them. These are read/write properties only and will not be stored locally, only for external use. /// Only use when showing NSOpenPanel to select disabled apps. diff --git a/Easydict/Feature/Configuration/EZConfiguration.m b/Easydict/Feature/Configuration/EZConfiguration.m index a3104745a..407f9883c 100644 --- a/Easydict/Feature/Configuration/EZConfiguration.m +++ b/Easydict/Feature/Configuration/EZConfiguration.m @@ -52,6 +52,7 @@ static NSString *const kAllowCrashLogKey = @"EZConfiguration_kAllowCrashLogKey"; static NSString *const kAllowAnalyticsKey = @"EZConfiguration_kAllowAnalyticsKey"; static NSString *const kClearInputKey = @"EZConfiguration_kClearInputKey"; +static NSString *const kKeepPrevResultKey = @"EZConfiguration_kKeepPrevResultKey"; static NSString *const kTranslationControllerFontKey = @"EZConfiguration_kTranslationControllerFontKey"; static NSString *const kApperanceKey = @"EZConfiguration_kApperanceKey"; @@ -126,6 +127,7 @@ - (void)setup { self.allowCrashLog = [NSUserDefaults mm_readBool:kAllowCrashLogKey defaultValue:YES]; self.allowAnalytics = [NSUserDefaults mm_readBool:kAllowAnalyticsKey defaultValue:YES]; self.clearInput = [NSUserDefaults mm_readBool:kClearInputKey defaultValue:NO]; + self.keepPrevResult = [NSUserDefaults mm_readBool:kKeepPrevResultKey defaultValue:YES]; self.fontSizes = @[@(1), @(1.1), @(1.2), @(1.3), @(1.4)]; [[NSUserDefaults standardUserDefaults]registerDefaults:@{kTranslationControllerFontKey: @(0)}]; @@ -432,6 +434,14 @@ - (void)setClearInput:(BOOL)clearInput { [self logSettings:@{@"clear_input" : @(clearInput)}]; } +- (void)setKeepPrevResult:(BOOL)keepPrevResult { + _keepPrevResult = keepPrevResult; + + [NSUserDefaults mm_write:@(keepPrevResult) forKey:kKeepPrevResultKey]; + + [self logSettings:@{@"keep_prev_result": @(keepPrevResult)}]; +} + - (void)setFontSizeIndex:(NSInteger)fontSizeIndex { NSInteger targetIndex = MIN(_fontSizes.count-1, MAX(fontSizeIndex, 0)); diff --git a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m index 8715ec11e..a2d28b21e 100644 --- a/Easydict/Feature/PerferenceWindow/EZSettingViewController.m +++ b/Easydict/Feature/PerferenceWindow/EZSettingViewController.m @@ -74,8 +74,10 @@ @interface EZSettingViewController () @property (nonatomic, strong) NSTextField *playAudioLabel; @property (nonatomic, strong) NSButton *autoPlayAudioButton; -@property (nonatomic, strong) NSTextField *clearInputLabel; +@property (nonatomic, strong) NSTextField *inputFieldLabel; @property (nonatomic, strong) NSButton *clearInputButton; +@property (nonatomic, strong) NSButton *keepPrevResultButton; +@property (nonatomic, strong) NSButton *selectQueryTextWhenWindowActivateButton; @property (nonatomic, strong) NSTextField *autoQueryLabel; @property (nonatomic, strong) NSButton *autoQueryOCRTextButton; @@ -395,14 +397,23 @@ - (void)setupUI { self.autoPlayAudioButton = [NSButton checkboxWithTitle:autoPlayAudioTitle target:self action:@selector(autoPlayAudioButtonClicked:)]; [self.contentView addSubview:self.autoPlayAudioButton]; - NSTextField *clearInputLabel = [NSTextField labelWithString:NSLocalizedString(@"clear_input", nil)]; - clearInputLabel.font = font; - [self.contentView addSubview:clearInputLabel]; - self.clearInputLabel = clearInputLabel; + NSString *inputFieldLabelTitle = [NSString stringWithFormat:@"%@:", NSLocalizedString(@"setting.general.input.header", nil)]; + NSTextField *inputFieldLabel = [NSTextField labelWithString:inputFieldLabelTitle]; + inputFieldLabel.font = font; + [self.contentView addSubview:inputFieldLabel]; + self.inputFieldLabel = inputFieldLabel; NSString *clearInputTitle = NSLocalizedString(@"clear_input_when_translating", nil); self.clearInputButton = [NSButton checkboxWithTitle:clearInputTitle target:self action:@selector(clearInputButtonClicked:)]; [self.contentView addSubview:self.clearInputButton]; + + NSString *keepPrevResultTitle = NSLocalizedString(@"keep_prev_result_when_selected_text_is_empty", nil); + self.keepPrevResultButton = [NSButton checkboxWithTitle:keepPrevResultTitle target:self action:@selector(keepPrevResultButtonClicked:)]; + [self.contentView addSubview:self.keepPrevResultButton]; + + NSString *selectQueryTextWhenWindowActivateTitle = NSLocalizedString(@"select_query_text_when_window_activate", nil); + self.selectQueryTextWhenWindowActivateButton = [NSButton checkboxWithTitle:selectQueryTextWhenWindowActivateTitle target:self action:@selector(selectQueryTextWhenWindowActivateButtonClicked:)]; + [self.contentView addSubview:self.selectQueryTextWhenWindowActivateButton]; NSTextField *autoQueryLabel = [NSTextField labelWithString:NSLocalizedString(@"auto_query", nil)]; autoQueryLabel.font = font; @@ -546,6 +557,8 @@ - (void)setupUI { self.autoPlayAudioButton.mm_isOn = self.config.autoPlayAudio; self.clearInputButton.mm_isOn = self.config.clearInput; + self.keepPrevResultButton.mm_isOn = self.config.keepPrevResultWhenEmpty; + self.selectQueryTextWhenWindowActivateButton.mm_isOn = self.config.selectQueryTextWhenWindowActivate; self.launchAtStartupButton.mm_isOn = self.config.launchAtStartup; self.hideMainWindowButton.mm_isOn = self.config.hideMainWindow; self.autoQueryOCRTextButton.mm_isOn = self.config.autoQueryOCRText; @@ -747,19 +760,26 @@ - (void)updateViewConstraints { }]; - [self.clearInputLabel mas_remakeConstraints:^(MASConstraintMaker *make) { + [self.inputFieldLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.autoGetSelectedTextLabel); make.top.equalTo(self.autoPlayAudioButton.mas_bottom).offset(self.verticalPadding); }]; [self.clearInputButton mas_remakeConstraints:^(MASConstraintMaker *make) { - make.left.equalTo(self.clearInputLabel.mas_right).offset(self.horizontalPadding); - make.centerY.equalTo(self.clearInputLabel); + make.left.equalTo(self.inputFieldLabel.mas_right).offset(self.horizontalPadding); + make.centerY.equalTo(self.inputFieldLabel); + }]; + [self.keepPrevResultButton mas_remakeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.clearInputButton); + make.top.equalTo(self.clearInputButton.mas_bottom).offset(self.verticalPadding); + }]; + [self.selectQueryTextWhenWindowActivateButton mas_remakeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.clearInputButton); + make.top.equalTo(self.keepPrevResultButton.mas_bottom).offset(self.verticalPadding); }]; - [self.autoQueryLabel mas_remakeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.autoGetSelectedTextLabel); - make.top.equalTo(self.clearInputButton.mas_bottom).offset(self.verticalPadding); + make.top.equalTo(self.selectQueryTextWhenWindowActivateButton.mas_bottom).offset(self.verticalPadding); }]; [self.autoQueryOCRTextButton mas_remakeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.autoQueryLabel.mas_right).offset(self.horizontalPadding); @@ -967,6 +987,14 @@ - (void)clearInputButtonClicked:(NSButton *)sender { self.config.clearInput = sender.mm_isOn; } +- (void)keepPrevResultButtonClicked:(NSButton *)sender { + self.config.keepPrevResultWhenEmpty = sender.mm_isOn; +} + +- (void)selectQueryTextWhenWindowActivateButtonClicked:(NSButton *)sender { + self.config.selectQueryTextWhenWindowActivate = sender.mm_isOn; +} + - (void)autoCopySelectedTextButtonClicked:(NSButton *)sender { self.config.autoCopySelectedText = sender.mm_isOn; } diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index c1bb5cc13..44a5a5e5b 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -527,6 +527,9 @@ - (void)focusInputTextView { [NSApp activateIgnoringOtherApps:YES]; [self.baseQueryWindow makeFirstResponder:self.queryView.textView]; + if (Configuration.shared.selectQueryTextWhenWindowActivate) { + self.queryView.textView.selectedRange = NSMakeRange(0, self.inputText.length); + } } } diff --git a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m index dd2c18664..05df14104 100644 --- a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m +++ b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m @@ -694,9 +694,14 @@ - (void)selectTextTranslate { NSLog(@"selectTextTranslate windowType: %@", @(windowType)); self.eventMonitor.actionType = EZActionTypeShortcutQuery; [self.eventMonitor getSelectedText:^(NSString *_Nullable text) { - // If text is nil, currently, we choose to clear input. - self.selectedText = [text trim] ?: @""; self.actionType = self.eventMonitor.actionType; + + // Clear query if text is nil and user don't want to keep the last result. + if (!text && !Configuration.shared.keepPrevResultWhenEmpty) { + text = @""; + } + self.selectedText = [text trim]; + [self showFloatingWindowType:windowType queryText:self.selectedText]; }]; } diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index 6b529ec6b..3b3305739 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -47,6 +47,8 @@ extension Defaults.Keys { static let allowCrashLog = Key("EZConfiguration_kAllowCrashLogKey", default: true) static let allowAnalytics = Key("EZConfiguration_kAllowAnalyticsKey", default: true) static let clearInput = Key("EZConfiguration_kClearInputKey", default: true) + static let keepPrevResultWhenEmpty = Key("EZConfiguration_kKeepPrevResultKey", default: true) + static let selectQueryTextWhenWindowActivate = Key("EZConfiguration_kSelectQueryTextWhenWindowActivate", default: false) static let enableBetaNewApp = Key("EZConfiguration_kEnableBetaNewAppKey", default: false) static let enableBetaFeature = Key("EZBetaFeatureKey", default: false) diff --git a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift index a1e59e04e..cfbf2f2dd 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/GeneralTab.swift @@ -78,6 +78,8 @@ struct GeneralTab: View { Section { Toggle("clear_input_when_translating", isOn: $clearInput) + Toggle("keep_prev_result_when_selected_text_is_empty", isOn: $keepPrevResultWhenEmpty) + Toggle("select_query_text_when_window_activate", isOn: $selectQueryTextWhenWindowActivate) } header: { Text("setting.general.input.header") } @@ -161,6 +163,8 @@ struct GeneralTab: View { @Default(.adjustPopButtonOrigin) private var adjustPopButtonOrigin @Default(.clearInput) private var clearInput + @Default(.keepPrevResultWhenEmpty) private var keepPrevResultWhenEmpty + @Default(.selectQueryTextWhenWindowActivate) private var selectQueryTextWhenWindowActivate @Default(.disableEmptyCopyBeep) private var disableEmptyCopyBeep @Default(.autoPlayAudio) private var autoPlayAudio From 4b2cfb1a6037719d7a71232faab4e0d04c7f5559 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Sun, 4 Feb 2024 12:36:10 +0800 Subject: [PATCH 52/72] fix: Setting window fails to appear front (#383) * fix: settings window fails to appear front in SwiftUI * fix: apply workaround below macOS 14 * fix: remove unused code --- Easydict.xcodeproj/project.pbxproj | 17 +++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++++++++ Easydict/NewApp/View/MenuItemView.swift | 11 ++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 9e8730358..12acca6f2 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -238,6 +238,7 @@ 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8685C72B552A590022534F /* DisabledAppTab.swift */; }; 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */; }; 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */; }; + 0AC8A84F2B6DFDD4006DA5CC /* SettingsAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 0AC8A84E2B6DFDD4006DA5CC /* SettingsAccess */; }; 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */; }; 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF62B0DFF9000A7D372 /* EZNiuTransTranslate.m */; }; 2721E4D02AFE920700A059AC /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 2721E4CF2AFE920700A059AC /* Alamofire */; }; @@ -826,6 +827,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 0AC8A84F2B6DFDD4006DA5CC /* SettingsAccess in Frameworks */, 03022F192B3591AE00B63209 /* GoogleGenerativeAI in Frameworks */, 2721E4D02AFE920700A059AC /* Alamofire in Frameworks */, 03022F1F2B36CF3100B63209 /* SwiftShell in Frameworks */, @@ -2463,6 +2465,7 @@ EA3B81FB2B52555C004C0E8B /* Defaults */, 967712E92B5B913600105E0F /* KeyHolder */, 03022F182B3591AE00B63209 /* GoogleGenerativeAI */, + 0AC8A84E2B6DFDD4006DA5CC /* SettingsAccess */, ); productName = Bob; productReference = C99EEB182385796700FEE666 /* Easydict-debug.app */; @@ -2524,6 +2527,7 @@ EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */, 967712E82B5B913600105E0F /* XCRemoteSwiftPackageReference "KeyHolder" */, 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */, + 0AC8A84D2B6DFDD4006DA5CC /* XCRemoteSwiftPackageReference "SettingsAccess" */, ); productRefGroup = C99EEB192385796700FEE666 /* Products */; projectDirPath = ""; @@ -3512,6 +3516,14 @@ minimumVersion = 1.8.0; }; }; + 0AC8A84D2B6DFDD4006DA5CC /* XCRemoteSwiftPackageReference "SettingsAccess" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/orchetect/SettingsAccess"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.4.0; + }; + }; 2721E4CE2AFE920700A059AC /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire.git"; @@ -3614,6 +3626,11 @@ package = 03FD68B92B1DC59600FD388E /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; + 0AC8A84E2B6DFDD4006DA5CC /* SettingsAccess */ = { + isa = XCSwiftPackageProductDependency; + package = 0AC8A84D2B6DFDD4006DA5CC /* XCRemoteSwiftPackageReference "SettingsAccess" */; + productName = SettingsAccess; + }; 2721E4CF2AFE920700A059AC /* Alamofire */ = { isa = XCSwiftPackageProductDependency; package = 2721E4CE2AFE920700A059AC /* XCRemoteSwiftPackageReference "Alamofire" */; diff --git a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0d2cecd07..6e22eb216 100644 --- a/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -234,6 +234,15 @@ "version" : "2.4.0" } }, + { + "identity" : "settingsaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/orchetect/SettingsAccess", + "state" : { + "revision" : "0fd73c8b5892e88acb13adb7f36a4ba9293a0061", + "version" : "1.4.0" + } + }, { "identity" : "snapkit", "kind" : "remoteSourceControl", diff --git a/Easydict/NewApp/View/MenuItemView.swift b/Easydict/NewApp/View/MenuItemView.swift index 2c5a6aca4..404b691bb 100644 --- a/Easydict/NewApp/View/MenuItemView.swift +++ b/Easydict/NewApp/View/MenuItemView.swift @@ -6,6 +6,7 @@ // Copyright © 2023 izual. All rights reserved. // +import SettingsAccess import Sparkle import SwiftUI import ZipArchive @@ -92,10 +93,18 @@ struct MenuItemView: View { @ViewBuilder private var settingItem: some View { if #available(macOS 14.0, *) { - SettingsLink() + SettingsLink { + Text("Settings...") + } preAction: { + NSLog("打开设置") + NSApp.activate(ignoringOtherApps: true) + } postAction: { + // nothing to do + } } else { Button("Settings...") { NSLog("打开设置") + NSApp.activate(ignoringOtherApps: true) NSApplication.shared.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) } } From 8251db7777a179a3a2772a5f2e4afa27981ff25c Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Sun, 4 Feb 2024 23:47:52 +0800 Subject: [PATCH 53/72] fix: query window dismiss in stage manager mode (#385) Co-authored-by: Tisfeng --- .../ViewController/Window/WindowManager/EZWindowManager.m | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m index 05df14104..1b098b8d5 100644 --- a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m +++ b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m @@ -435,11 +435,6 @@ - (void)showFloatingWindow:(EZBaseQueryWindow *)window atPoint:(CGPoint)point { [window.queryViewController focusInputTextView]; [self updateFloatingWindowType:window.windowType]; - - // mainWindow has been ordered out before, so we need to order back. - if ([EZMainQueryWindow isAlive]) { - [self.mainWindow orderBack:nil]; - } } - (void)updateFloatingWindowType:(EZWindowType)floatingWindowType { From 25ec93d44705f4aa9ec9e8484f7bd549007f672d Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Tue, 6 Feb 2024 11:13:15 +0800 Subject: [PATCH 54/72] translation service api key config (#379) * feat: add tencent service config * feat: add niuTrans api key config * feat: add caiyun token config * feat: update service config view * feat: update service localized strings * feat: update config view * feat: update validation disable according to input * feat: update some localized string keys * feat: update openAI config * feat: add bing and deepL config * fix: update view structure and fix reload issue * fix: remove unused code * fix: remove unused code * feat: add deepL api priority config * feat: add gemini key config * perf: adjust settings page window size * Update Easydict/App/Localizable.xcstrings Co-authored-by: Tisfeng * Update Easydict/App/Localizable.xcstrings Co-authored-by: Tisfeng * Update Easydict/App/Localizable.xcstrings Co-authored-by: Tisfeng * fix: remove openai domain config * fix: toggle configuration reset after switch tab issue * Update Easydict/App/Localizable.xcstrings Co-authored-by: Tisfeng * fix: change OpenAI UsageStatus option * fix: update OpenAI models * fix: validation warning * fix: `Publishing changes from within view updates is not allowed, this will cause undefined behavior.` in `ServiceConfigurationSecretSectionView` * fix: picker selection issue * fix: remove unused code * fix: optimize defaults with picker cell * Update Easydict/NewApp/Configuration/Configuration+Defaults.swift * Update Easydict/NewApp/Configuration/Configuration+Defaults.swift * Update Easydict/NewApp/Configuration/Configuration+Defaults.swift * Update Easydict/NewApp/Configuration/Configuration+Defaults.swift --------- Co-authored-by: Tisfeng Co-authored-by: Lava <34743145+CanglongCl@users.noreply.github.com> --- Easydict.xcodeproj/project.pbxproj | 44 ++ Easydict/App/Easydict-Bridging-Header.h | 3 + Easydict/App/Localizable.xcstrings | 462 +++++++++++++++++- Easydict/Feature/Service/Ali/AliService.swift | 6 + .../Service/Caiyun/CaiyunService.swift | 4 + .../Service/Gemini/GeminiService.swift | 16 +- .../Feature/Service/Model/EZQueryService.h | 2 + .../Feature/Service/Model/EZQueryService.m | 23 + .../Swift/Binding/Binding+DidSet.swift | 7 + .../EZBaseQueryViewController.m | 26 +- .../Configuration+Defaults.swift | 23 +- .../AliService+ConfigurableService.swift | 26 + .../BingService+ConfigurableService.swift | 22 + .../CaiyunService+ConfigurableService.swift | 22 + .../DeepLTranslate+ConfigurableService.swift | 65 +++ .../GeminiService+ConfigurableService.swift | 22 + ...iuTransTranslate+ConfigurableService.swift | 22 + .../OpenAIService+ConfigurableService.swift | 109 ++++- .../TencentService+ConfigurableService.swift | 27 + .../ServiceSecretConfigreValidatable.swift | 24 + .../NewApp/View/SettingView/SettingView.swift | 8 +- .../SecureTextField.swift | 83 ++++ .../ServiceConfigurationCells.swift | 126 +++++ ...erviceConfigurationSecretSectionView.swift | 116 +++++ .../ServiceConfigurationSection.swift | 5 +- 25 files changed, 1211 insertions(+), 82 deletions(-) create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/AliService+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/BingService+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/TencentService+ConfigurableService.swift create mode 100644 Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/SecureTextField.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 12acca6f2..e98fe4c7d 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -238,6 +238,17 @@ 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8685C72B552A590022534F /* DisabledAppTab.swift */; }; 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */; }; 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */; }; + 0AC8A8352B6641A7006DA5CC /* TencentService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8342B6641A7006DA5CC /* TencentService+ConfigurableService.swift */; }; + 0AC8A8372B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8362B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift */; }; + 0AC8A8392B666F07006DA5CC /* CaiyunService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8382B666F07006DA5CC /* CaiyunService+ConfigurableService.swift */; }; + 0AC8A83B2B6682D4006DA5CC /* AliService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A83A2B6682D4006DA5CC /* AliService+ConfigurableService.swift */; }; + 0AC8A83D2B6685EE006DA5CC /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */; }; + 0AC8A83F2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */; }; + 0AC8A8412B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8402B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift */; }; + 0AC8A8432B6957B0006DA5CC /* BingService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8422B6957B0006DA5CC /* BingService+ConfigurableService.swift */; }; + 0AC8A8452B6A4D97006DA5CC /* ServiceConfigurationCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8442B6A4D97006DA5CC /* ServiceConfigurationCells.swift */; }; + 0AC8A8472B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A8462B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift */; }; + 0AC8A84B2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC8A84A2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift */; }; 0AC8A84F2B6DFDD4006DA5CC /* SettingsAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 0AC8A84E2B6DFDD4006DA5CC /* SettingsAccess */; }; 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */; }; 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF62B0DFF9000A7D372 /* EZNiuTransTranslate.m */; }; @@ -730,6 +741,17 @@ 0A8685C72B552A590022534F /* DisabledAppTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppTab.swift; sourceTree = ""; }; 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapHandlerView.swift; sourceTree = ""; }; + 0AC8A8342B6641A7006DA5CC /* TencentService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TencentService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8362B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NiuTransTranslate+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8382B666F07006DA5CC /* CaiyunService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaiyunService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A83A2B6682D4006DA5CC /* AliService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AliService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; + 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceSecretConfigreValidatable.swift; sourceTree = ""; }; + 0AC8A8402B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeepLTranslate+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8422B6957B0006DA5CC /* BingService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BingService+ConfigurableService.swift"; sourceTree = ""; }; + 0AC8A8442B6A4D97006DA5CC /* ServiceConfigurationCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceConfigurationCells.swift; sourceTree = ""; }; + 0AC8A8462B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceConfigurationSecretSectionView.swift; sourceTree = ""; }; + 0AC8A84A2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GeminiService+ConfigurableService.swift"; sourceTree = ""; }; 17BCAEF32B0DFF9000A7D372 /* EZNiuTransTranslateResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslateResponse.h; sourceTree = ""; }; 17BCAEF42B0DFF9000A7D372 /* EZNiuTransTranslate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslate.h; sourceTree = ""; }; 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EZNiuTransTranslateResponse.m; sourceTree = ""; }; @@ -2350,7 +2372,10 @@ EAED41EA2B54A4900005FE0A /* ServiceConfiguration */ = { isa = PBXGroup; children = ( + 0AC8A83C2B6685EE006DA5CC /* SecureTextField.swift */, EAED41EB2B54AA920005FE0A /* ServiceConfigurationSection.swift */, + 0AC8A8462B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift */, + 0AC8A8442B6A4D97006DA5CC /* ServiceConfigurationCells.swift */, ); path = ServiceConfiguration; sourceTree = ""; @@ -2359,6 +2384,7 @@ isa = PBXGroup; children = ( EAED41EE2B54B1430005FE0A /* ConfigurableService.swift */, + 0AC8A83E2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift */, ); path = Protocol; sourceTree = ""; @@ -2367,6 +2393,13 @@ isa = PBXGroup; children = ( EAED41F12B54B39D0005FE0A /* OpenAIService+ConfigurableService.swift */, + 0AC8A8402B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift */, + 0AC8A8342B6641A7006DA5CC /* TencentService+ConfigurableService.swift */, + 0AC8A8362B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift */, + 0AC8A8382B666F07006DA5CC /* CaiyunService+ConfigurableService.swift */, + 0AC8A83A2B6682D4006DA5CC /* AliService+ConfigurableService.swift */, + 0AC8A8422B6957B0006DA5CC /* BingService+ConfigurableService.swift */, + 0AC8A84A2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift */, ); path = "QueryService+ConfigurableService"; sourceTree = ""; @@ -2735,6 +2768,7 @@ 03991158292927E000E1B06D /* EZTitlebar.m in Sources */, 03D8A65C2A433B4100D9A968 /* EZConfiguration+EZUserData.m in Sources */, 03BD282229486CF200F5891A /* EZBlueTextButton.m in Sources */, + 0AC8A8472B6A4E3F006DA5CC /* ServiceConfigurationSecretSectionView.swift in Sources */, 03BDA7C22A26DA280079D04F /* NSString+Indenter.m in Sources */, 03542A462937B4C300C34C33 /* EZBaiduTranslateResponse.m in Sources */, 0309E1F0292B4A5E00AFB76A /* NSView+EZGetViewController.m in Sources */, @@ -2761,6 +2795,7 @@ 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */, 03B0230729231FA6001C7E63 /* EZCommonView.m in Sources */, 03B0233329231FA6001C7E63 /* MMLog.m in Sources */, + 0AC8A8352B6641A7006DA5CC /* TencentService+ConfigurableService.swift in Sources */, DCF176F22B57CED700CA6026 /* Configuration+UserData.swift in Sources */, 0309E1F4292BD6A100AFB76A /* EZQueryModel.m in Sources */, 03BFFC7129612E10004E033E /* NSString+EZConvenience.m in Sources */, @@ -2797,6 +2832,7 @@ 276742082B3DC230002A2C75 /* PrivacyTab.swift in Sources */, 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */, 03882F8F29D95044005B5A52 /* CTScreen.m in Sources */, + 0AC8A8392B666F07006DA5CC /* CaiyunService+ConfigurableService.swift in Sources */, 27FE980B2B3DD5D1000AD654 /* MenuItemView.swift in Sources */, 03DC7C6A2A3CA852000BF7C9 /* EZAppCell.m in Sources */, 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */, @@ -2826,6 +2862,7 @@ 039CC914292FB3180037B91E /* EZPopUpButton.m in Sources */, 0399C6B829A9F4B800B4AFCC /* EZSchemeParser.m in Sources */, 03542A3A2937AE6400C34C33 /* EZQueryService.m in Sources */, + 0AC8A84B2B6A629D006DA5CC /* GeminiService+ConfigurableService.swift in Sources */, 03B0230529231FA6001C7E63 /* EZButton.m in Sources */, 03B0232329231FA6001C7E63 /* NSString+MM.m in Sources */, 036196772A000F5900806370 /* NSData+CommonCrypto.m in Sources */, @@ -2853,6 +2890,7 @@ 033C30FC2A7409C40095926A /* TTTDictionary.m in Sources */, 03B0232D29231FA6001C7E63 /* NSArray+MM.m in Sources */, 039E5021296E5D9900072344 /* EZScrollViewController.m in Sources */, + 0AC8A8412B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift in Sources */, 039CC90D292F664E0037B91E /* NSObject+EZWindowType.m in Sources */, 03B0232229231FA6001C7E63 /* NSImage+MM.m in Sources */, 03BB2DEF29F59C8A00447EDD /* EZSymbolImageButton.m in Sources */, @@ -2890,6 +2928,7 @@ 03D043522928935300E7559E /* EZMainQueryWindow.m in Sources */, 03D8B26E292DBD2000D5A811 /* EZCoordinateUtils.m in Sources */, 03B0232029231FA6001C7E63 /* NSWindow+MM.m in Sources */, + 0AC8A8432B6957B0006DA5CC /* BingService+ConfigurableService.swift in Sources */, 03542A30293645DF00C34C33 /* EZAppleService.m in Sources */, 03BB2DE329F5772F00447EDD /* EZAudioButton.m in Sources */, 03262C2529EFE97B00EFECA0 /* NSViewController+EZWindow.m in Sources */, @@ -2918,6 +2957,8 @@ 03DC38C1292CC97900922CB2 /* EZServiceInfo.m in Sources */, 03B0232A29231FA6001C7E63 /* NSColor+MyColors.m in Sources */, C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */, + 0AC8A83D2B6685EE006DA5CC /* SecureTextField.swift in Sources */, + 0AC8A8372B6659A8006DA5CC /* NiuTransTranslate+ConfigurableService.swift in Sources */, 03D043562928940500E7559E /* EZBaseQueryWindow.m in Sources */, 03BDA7B92A26DA280079D04F /* NSProcessInfo+XPMArgumentParser.m in Sources */, 03542A4F2937B64B00C34C33 /* EZYoudaoOCRResponse.m in Sources */, @@ -2930,6 +2971,7 @@ 03BDA7BA2A26DA280079D04F /* XPMMutableAttributedArray.m in Sources */, 037852B629588EDE00D0E2CF /* EZCustomTableRowView.m in Sources */, 03F0DB382953428300EBF9C1 /* EZLog.m in Sources */, + 0AC8A83B2B6682D4006DA5CC /* AliService+ConfigurableService.swift in Sources */, 03B0231429231FA6001C7E63 /* DarkModeManager.m in Sources */, 03BDA7C02A26DA280079D04F /* XPMArgumentPackage.m in Sources */, 2746AEC12AF95138005FE0A1 /* CaiyunService.swift in Sources */, @@ -2941,6 +2983,7 @@ 036E7D7B293F4FC8002675DF /* EZOpenLinkButton.m in Sources */, EAED41EC2B54AA920005FE0A /* ServiceConfigurationSection.swift in Sources */, 03832F542B5F6BE200D0DC64 /* AdvancedTab.swift in Sources */, + 0AC8A8452B6A4D97006DA5CC /* ServiceConfigurationCells.swift in Sources */, 276742092B3DC230002A2C75 /* AboutTab.swift in Sources */, 03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */, DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */, @@ -2948,6 +2991,7 @@ 03542A5E2938F05B00C34C33 /* EZLanguageModel.m in Sources */, EA9943E82B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift in Sources */, 03F639952AA6CFBB009B9914 /* EZBingConfig.m in Sources */, + 0AC8A83F2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift in Sources */, 03D2A3E329F4C6F50035CED4 /* EZNetworkManager.m in Sources */, 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */, 03B0232B29231FA6001C7E63 /* NSMutableAttributedString+MM.m in Sources */, diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index 66f840155..bfa0f2504 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -32,5 +32,8 @@ #import "DarkModeManager.h" #import "EZScriptExecutor.h" #import "EZOpenAIService.h" +#import "EZNiuTransTranslate.h" +#import "EZDeepLTranslate.h" +#import "EZBingService.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 257c47129..a2ab7b33c 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -2415,44 +2415,242 @@ } } }, - "service.configuration.openai.api_key.footer" : { + "service.configuration.ali.access_key_id.title" : { "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey ID" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey ID" + } + } + } + }, + "service.configuration.ali.access_key_secret.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey Secret" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "AccessKey Secret" + } + } + } + }, + "service.configuration.bing.cookie.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cookie Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cookie Key" + } + } + } + }, + "service.configuration.caiyun.token.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Token" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Token" + } + } + } + }, + "service.configuration.deepl.auth_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key" + } + } + } + }, + "service.configuration.deepl.authkey_first.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key First" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "优先使用 Auth Key" + } + } + } + }, + "service.configuration.deepl.authkey_only.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auth Key Only" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仅使用 Auth Key" + } + } + } + }, + "service.configuration.deepl.endpoint.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://api-free.deepl.com/v2/translate" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "API Key的一些说明或者加入链接" + "value" : "https://api-free.deepl.com/v2/translate" } } } }, - "service.configuration.openai.api_key.header" : { + "service.configuration.deepl.endpoint.title" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "API Endpoint" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "完整接口地址" } } } }, - "service.configuration.openai.api_key.prompt" : { + "service.configuration.deepl.translation.title" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "value" : "API Usage Priority" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "value" : "API 使用优先级" + } + } + } + }, + "service.configuration.deepl.web_first.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Web API first" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "优先使用 Web API" + } + } + } + }, + "service.configuration.gemini.api_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + } + } + }, + "service.configuration.input.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "xxxxxxxxxx" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "xxxxxxxxxx" + } + } + } + }, + "service.configuration.niutrans.api_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Key" + } + } + } + }, + "service.configuration.openai.api_key.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "sk-xxxxxxxxxx" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "sk-xxxxxxxxxx" } } } @@ -2462,30 +2660,178 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "API Key" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "OpenAI API Key" + "value" : "API Key" } } } }, - "service.configuration.openai.translation.footer" : { - + "service.configuration.openai.dictionary.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dictionary" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查单词" + } + } + } }, - "service.configuration.openai.translation.header" : { - + "service.configuration.openai.endpoint.placeholder" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://api.openai.com/v1/chat/completions" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://api.openai.com/v1/chat/completions" + } + } + } }, - "service.configuration.openai.translation.prompt" : { - + "service.configuration.openai.endpoint.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "API Endpoint" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完整接口地址" + } + } + } + }, + "service.configuration.openai.model.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Model" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "模型" + } + } + } + }, + "service.configuration.openai.sentence.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Sentence" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "句子分析" + } + } + } }, "service.configuration.openai.translation.title" : { - + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Translation" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "翻译" + } + } + } + }, + "service.configuration.openai.usage_status_always_off.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Always Off" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "始终关闭" + } + } + } + }, + "service.configuration.openai.usage_status_always_on.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Always On" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "始终打开" + } + } + } + }, + "service.configuration.openai.usage_status_default.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Default" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "默认" + } + } + } }, - "service.service_configuration.reset" : { + "service.configuration.openai.usage_status.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Usage Status" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "使用状态" + } + } + } + }, + "service.configuration.reset" : { "localizations" : { "en" : { "stringUnit" : { @@ -2501,6 +2847,86 @@ } } }, + "service.configuration.tencent.secret_id.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Id" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Id" + } + } + } + }, + "service.configuration.tencent.secret_key.title" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Key" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Secret Key" + } + } + } + }, + "service.configuration.validate" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validate" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证" + } + } + } + }, + "service.configuration.validation_fail" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validation failed!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证失败!" + } + } + } + }, + "service.configuration.validation_success" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Validation success!" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "验证成功!" + } + } + } + }, "setting_general" : { "localizations" : { "en" : { diff --git a/Easydict/Feature/Service/Ali/AliService.swift b/Easydict/Feature/Service/Ali/AliService.swift index 9788fc063..f783ac636 100644 --- a/Easydict/Feature/Service/Ali/AliService.swift +++ b/Easydict/Feature/Service/Ali/AliService.swift @@ -37,6 +37,12 @@ class AliService: QueryService { NSLocalizedString("ali_translate", comment: "The name of Ali Translate") } + override func hasPrivateAPIKey() -> Bool { + let id = Defaults[.aliAccessKeyId] ?? "" + let secret = Defaults[.aliAccessKeySecret] ?? "" + return !id.isEmpty && !secret.isEmpty + } + override public func supportLanguagesDictionary() -> MMOrderedDictionary { // TODO: Replace MMOrderedDictionary in the API let orderedDict = MMOrderedDictionary() diff --git a/Easydict/Feature/Service/Caiyun/CaiyunService.swift b/Easydict/Feature/Service/Caiyun/CaiyunService.swift index b36cfbdf8..55a2d8b74 100644 --- a/Easydict/Feature/Service/Caiyun/CaiyunService.swift +++ b/Easydict/Feature/Service/Caiyun/CaiyunService.swift @@ -38,6 +38,10 @@ public final class CaiyunService: QueryService { throw QueryServiceError.notSupported } + override public func hasPrivateAPIKey() -> Bool { + token != CaiyunService.defaultTestToken + } + private var apiEndPoint = "https://api.interpreter.caiyunai.com/v1/translator" /// Official Test Token for Caiyun diff --git a/Easydict/Feature/Service/Gemini/GeminiService.swift b/Easydict/Feature/Service/Gemini/GeminiService.swift index 6df5cfcff..43331a553 100644 --- a/Easydict/Feature/Service/Gemini/GeminiService.swift +++ b/Easydict/Feature/Service/Gemini/GeminiService.swift @@ -6,6 +6,7 @@ // Copyright © 2024 izual. All rights reserved. // +import Defaults import Foundation import GoogleGenerativeAI @@ -59,7 +60,7 @@ public final class GeminiService: QueryService { // easydict://writeKeyValue?EZGeminiAPIKey=xxx private var apiKey: String { - let apiKey = UserDefaults.standard.string(forKey: EZGeminiAPIKey) + let apiKey = Defaults[.geminiAPIKey] if let apiKey, !apiKey.isEmpty { return apiKey } else { @@ -107,7 +108,9 @@ public final class GeminiService: QueryService { resultString += line result.translatedResults = [resultString] - completion(result, nil) + await MainActor.run { + completion(result, nil) + } } } else { @@ -117,7 +120,9 @@ public final class GeminiService: QueryService { } result.translatedResults = [resultString] - completion(result, nil) + await MainActor.run { + completion(result, nil) + } } } catch { /** @@ -131,8 +136,9 @@ public final class GeminiService: QueryService { let errorString = String(describing: error) let errorMessage = errorString.extract(withPattern: "message: \"([^\"]*)\"") ?? errorString ezError?.errorDataMessage = errorMessage - - completion(result, ezError) + await MainActor.run { + completion(result, ezError) + } } } } diff --git a/Easydict/Feature/Service/Model/EZQueryService.h b/Easydict/Feature/Service/Model/EZQueryService.h index 8d5e1746e..f2616bfe4 100644 --- a/Easydict/Feature/Service/Model/EZQueryService.h +++ b/Easydict/Feature/Service/Model/EZQueryService.h @@ -61,6 +61,8 @@ NS_SWIFT_NAME(QueryService) /// Get TTS langauge code. - (NSString *)getTTSLanguageCode:(EZLanguage)language; +- (EZQueryResult *)resetServiceResult; + - (void)startQuery:(EZQueryModel *)queryModel completion:(void (^)(EZQueryResult *result, NSError *_Nullable error))completion; @end diff --git a/Easydict/Feature/Service/Model/EZQueryService.m b/Easydict/Feature/Service/Model/EZQueryService.m index a2a5f7734..2a1646fea 100644 --- a/Easydict/Feature/Service/Model/EZQueryService.m +++ b/Easydict/Feature/Service/Model/EZQueryService.m @@ -13,6 +13,7 @@ #import "NSString+EZUtils.h" #import "EZConfiguration.h" #import "Easydict-Swift.h" +#import "EZEventMonitor.h" #define MethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ @@ -84,6 +85,28 @@ - (void)setResult:(EZQueryResult *)translateResult { _result.queryText = self.queryModel.queryText; } +- (EZQueryResult *)resetServiceResult { + EZQueryResult *result = self.result; + [result reset]; + if (!result) { + result = [[EZQueryResult alloc] init]; + } + + NSArray *enabledReplaceTypes = @[ + EZActionTypeAutoSelectQuery, + EZActionTypeShortcutQuery, + EZActionTypeInvokeQuery, + ]; + if ([enabledReplaceTypes containsObject:self.queryModel.actionType]) { + result.showReplaceButton = EZEventMonitor.shared.isSelectedTextEditable; + } else { + result.showReplaceButton = NO; + } + + self.result = result; + return result; +} + - (MMOrderedDictionary *)langDict { if (!_langDict) { _langDict = [self supportLanguagesDictionary]; diff --git a/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift index 244b735cb..960afabdd 100644 --- a/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift +++ b/Easydict/Feature/Utility/Swift/Binding/Binding+DidSet.swift @@ -21,3 +21,10 @@ extension Binding { ) } } + +func ?? (lhs: Binding, rhs: T) -> Binding { + Binding( + get: { lhs.wrappedValue ?? rhs }, + set: { lhs.wrappedValue = $0 } + ) +} diff --git a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index 44a5a5e5b..5c753265a 100644 --- a/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/Feature/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -1024,34 +1024,12 @@ - (void)resetQueryAndResults { - (NSArray *)resetAllResults { NSMutableArray *allResults = [NSMutableArray array]; for (EZQueryService *service in self.services) { - EZQueryResult *result = [self resetServiceResult:service]; + EZQueryResult *result = [service resetServiceResult]; [allResults addObject:result]; } return allResults; } -- (EZQueryResult *)resetServiceResult:(EZQueryService *)service { - EZQueryResult *result = service.result; - [result reset]; - if (!result) { - result = [[EZQueryResult alloc] init]; - } - - NSArray *enabledReplaceTypes = @[ - EZActionTypeAutoSelectQuery, - EZActionTypeShortcutQuery, - EZActionTypeInvokeQuery, - ]; - if ([enabledReplaceTypes containsObject:self.queryModel.actionType]) { - result.showReplaceButton = EZEventMonitor.shared.isSelectedTextEditable; - } else { - result.showReplaceButton = NO; - } - - service.result = result; - return result; -} - - (nullable EZResultView *)resultCellOfResult:(EZQueryResult *)result { NSInteger index = [self.services indexOfObject:result.service]; NSInteger row = index + [self resultCellOffset]; @@ -1295,7 +1273,7 @@ - (void)setupResultCell:(EZResultView *)resultView { // Make enabledQuery = YES before retry, it may be closed manually. service.enabledQuery = YES; - EZQueryResult *newResult = [self resetServiceResult:service]; + EZQueryResult *newResult = [service resetServiceResult]; [self updateCellWithResult:newResult reloadData:YES completionHandler:^{ [self queryWithModel:self.queryModel service:service autoPlay:NO]; }]; diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index 3b3305739..214e1bb5c 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -134,21 +134,21 @@ class DefaultsWrapper { // Service Configuration extension Defaults.Keys { - // OPENAI + // 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 openAITranslation = Key("EZOpenAITranslationKey", default: "1") + static let openAIDictionary = Key("EZOpenAIDictionaryKey", default: "1") + static let openAISentence = Key("EZOpenAISentenceKey", default: "1") + static let openAIServiceUsageStatus = Key("EZOpenAIServiceUsageStatusKey", default: OpenAIUsageStats.default) static let openAIEndPoint = Key("EZOpenAIEndPointKey") - static let openAIModel = Key("EZOpenAIModelKey") + static let openAIModel = Key("EZOpenAIModelKey", default: OpenAIModels.gpt3_5_turbo_0125) - // DEEPL + // DeepL static let deepLAuth = Key("EZDeepLAuthKey") + static let deepLTranslation = Key("EZDeepLTranslationAPIKey", default: DeepLAPIUsagePriority.webFirst) static let deepLTranslateEndPointKey = Key("EZDeepLTranslateEndPointKey") - // BING + // Bing static let bingCookieKey = Key("EZBingCookieKey") // niu @@ -161,9 +161,12 @@ extension Defaults.Keys { static let tencentSecretId = Key("EZTencentSecretId") static let tencentSecretKey = Key("EZTencentSecretKey") - // ALI + // Ali static let aliAccessKeyId = Key("EZAliAccessKeyId") static let aliAccessKeySecret = Key("EZAliAccessKeySecret") + + // Gemni + static let geminiAPIKey = Key("EZGeminiAPIKey") } /// shortcut diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/AliService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/AliService+ConfigurableService.swift new file mode 100644 index 000000000..109501d18 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/AliService+ConfigurableService.swift @@ -0,0 +1,26 @@ +// +// AliService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension AliService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.aliAccessKeyId, .aliAccessKeySecret]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.ali.access_key_id.title", + key: .aliAccessKeyId + ) + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.ali.access_key_secret.title", + key: .aliAccessKeySecret + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/BingService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/BingService+ConfigurableService.swift new file mode 100644 index 000000000..be7d26361 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/BingService+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// BingService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension EZBingService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.bingCookieKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.bing.cookie.title", + key: .bingCookieKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift new file mode 100644 index 000000000..300fbf87b --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/CaiyunService+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// CaiyunService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension CaiyunService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.caiyunToken]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.caiyun.token.title", + key: .caiyunToken + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift new file mode 100644 index 000000000..3c6a7d17b --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/DeepLTranslate+ConfigurableService.swift @@ -0,0 +1,65 @@ +// +// DeepLTranslate+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/30. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension EZDeepLTranslate: ConfigurableService { + func configurationListItems() -> some View { + EZDeepLTranslateConfigurationView(service: self) + } +} + +@available(macOS 13.0, *) +private struct EZDeepLTranslateConfigurationView: View { + let service: EZDeepLTranslate + + var body: some View { + ServiceConfigurationSecretSectionView(service: service, observeKeys: [.deepLAuth]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.deepl.auth_key.title", + key: .deepLAuth + ) + + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.deepl.endpoint.title", + key: .deepLTranslateEndPointKey, + placeholder: "service.configuration.deepl.endpoint.placeholder" + ) + + ServiceConfigurationPickerCell( + titleKey: "service.configuration.deepl.translation.title", + key: .deepLTranslation, + values: DeepLAPIUsagePriority.allCases + ) + } + } +} + +enum DeepLAPIUsagePriority: String, CaseIterable { + case webFirst = "0" + case authKeyFirst = "1" + case authKeyOnly = "2" +} + +extension DeepLAPIUsagePriority: Defaults.Serializable {} + +extension DeepLAPIUsagePriority: EnumLocalizedStringConvertible { + var title: String { + switch self { + case .webFirst: + return NSLocalizedString("service.configuration.deepl.web_first.title", bundle: .main, comment: "") + case .authKeyFirst: + return NSLocalizedString("service.configuration.deepl.authkey_first.title", bundle: .main, comment: "") + case .authKeyOnly: + return NSLocalizedString("service.configuration.deepl.authkey_only.title", bundle: .main, comment: "") + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift new file mode 100644 index 000000000..3d0cdc9df --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/GeminiService+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// GeminiService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension GeminiService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.geminiAPIKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.gemini.api_key.title", + key: .geminiAPIKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift new file mode 100644 index 000000000..db033ff54 --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/NiuTransTranslate+ConfigurableService.swift @@ -0,0 +1,22 @@ +// +// NiuTransTranslate+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension EZNiuTransTranslate: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.niuTransAPIKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.niutrans.api_key.title", + key: .niuTransAPIKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift index 960cada25..091631ed8 100644 --- a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/OpenAIService+ConfigurableService.swift @@ -6,30 +6,99 @@ // Copyright © 2024 izual. All rights reserved. // +import Defaults import Foundation import SwiftUI -@available(macOS 12.0, *) +@available(macOS 13.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") - } - ) + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.openAIAPIKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.openai.api_key.title", + key: .openAIAPIKey, + placeholder: "service.configuration.openai.api_key.placeholder" + ) + // endpoint + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.openai.endpoint.title", + key: .openAIEndPoint, + placeholder: "service.configuration.openai.endpoint.placeholder" + ) + // model + ServiceConfigurationPickerCell( + titleKey: "service.configuration.openai.model.title", + key: .openAIModel, + values: OpenAIModels.allCases + ) + + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.translation.title", + key: .openAITranslation + ) + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.sentence.title", + key: .openAISentence + ) + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.dictionary.title", + key: .openAIDictionary + ) + ServiceConfigurationPickerCell( + titleKey: "service.configuration.openai.usage_status.title", + key: .openAIServiceUsageStatus, + values: OpenAIUsageStats.allCases + ) + } + } +} + +protocol EnumLocalizedStringConvertible { + var title: String { get } +} + +enum OpenAIModels: String, CaseIterable { + case gpt3_5_turbo_0125 = "gpt-3.5-turbo-0125" + case gpt4_0125_preview = "gpt-4-0125-preview" +} + +extension OpenAIModels: EnumLocalizedStringConvertible { + var title: String { + rawValue } } + +extension OpenAIModels: Defaults.Serializable {} + +enum OpenAIUsageStats: String, CaseIterable { + case `default` = "0" + case alwaysOff = "1" + case alwaysOn = "2" +} + +extension OpenAIUsageStats: EnumLocalizedStringConvertible { + var title: String { + switch self { + case .default: + return NSLocalizedString( + "service.configuration.openai.usage_status_default.title", + bundle: .main, + comment: "" + ) + case .alwaysOff: + return NSLocalizedString( + "service.configuration.openai.usage_status_always_off.title", + bundle: .main, + comment: "" + ) + case .alwaysOn: + return NSLocalizedString( + "service.configuration.openai.usage_status_always_on.title", + bundle: .main, + comment: "" + ) + } + } +} + +extension OpenAIUsageStats: Defaults.Serializable {} diff --git a/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/TencentService+ConfigurableService.swift b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/TencentService+ConfigurableService.swift new file mode 100644 index 000000000..032067baa --- /dev/null +++ b/Easydict/NewApp/Utility/Extensions/QueryService+ConfigurableService/TencentService+ConfigurableService.swift @@ -0,0 +1,27 @@ +// +// TencentService+ConfigurableService.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation +import SwiftUI + +@available(macOS 13.0, *) +extension TencentService: ConfigurableService { + func configurationListItems() -> some View { + ServiceConfigurationSecretSectionView(service: self, observeKeys: [.tencentSecretId, .tencentSecretKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.tencent.secret_id.title", + key: .tencentSecretId + ) + + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.tencent.secret_key.title", + key: .tencentSecretKey + ) + } + } +} diff --git a/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift new file mode 100644 index 000000000..89f3f21a5 --- /dev/null +++ b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift @@ -0,0 +1,24 @@ +// +// ServiceSecretConfigreValidatable.swift +// Easydict +// +// Created by phlpsong on 2024/1/30. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +protocol ServiceSecretConfigreValidatable { + func validate(completion: @escaping (EZQueryResult, Error?) -> Void) +} + +extension ServiceSecretConfigreValidatable { + func validate(completion _: @escaping (EZQueryResult, Error?) -> Void) {} +} + +extension QueryService: ServiceSecretConfigreValidatable { + func validate(completion: @escaping (EZQueryResult, Error?) -> Void) { + resetServiceResult() + translate("hello world!", from: .english, to: .simplifiedChinese, completion: completion) + } +} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index bb631a6e0..643ee1c23 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -69,11 +69,13 @@ struct SettingView: View { window.standardWindowButton(.zoomButton)?.isEnabled = false // Keep the settings page windows all the same width to avoid strange animations. - let maxWidth = 650 + let maxWidth = 750 let height = switch selection { case .general: - maxWidth - case .service, .disabled, .shortcut: + maxWidth - 100 + case .service: + 600 + case .disabled, .shortcut: 500 case .privacy: 320 diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/SecureTextField.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/SecureTextField.swift new file mode 100644 index 000000000..d2632a544 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/SecureTextField.swift @@ -0,0 +1,83 @@ +// +// SecureTextField.swift +// Easydict +// +// Created by phlpsong on 2024/1/28. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13.0, *) +struct SecureTextField: View { + let title: LocalizedStringKey + let placeholder: LocalizedStringKey + + @Binding var text: String? + + @State private var showText: Bool = false + + private enum Focus { + case secure, text + } + + @FocusState private var focus: Focus? + + @Environment(\.scenePhase) private var scenePhase + @Environment(\.lineLimit) private var lineLimit + + var body: some View { + HStack { + ZStack { + SecureField(title, text: $text ?? "") + .lineLimit(lineLimit) + .focused($focus, equals: .secure) + .opacity(showText ? 0 : 1) + TextField(title, text: $text ?? "", prompt: Text(placeholder)) + .lineLimit(lineLimit) + .focused($focus, equals: .text) + .opacity(showText || (text?.isEmpty ?? true) ? 1 : 0) + } + + Button(action: { + showText.toggle() + }) { + Image(systemName: showText ? "eye.slash.fill" : "eye.fill") + } + } + .onChange(of: focus) { newValue in + // if the PasswordField is focused externally, then make sure the correct field is actually focused + if newValue != nil { + focus = showText ? .text : .secure + } + } + .onChange(of: scenePhase) { newValue in + if newValue != .active { + showText = false + } + } + .onChange(of: showText) { newValue in + if focus != nil { // Prevents stealing focus to this field if another field is focused, or nothing is focused + DispatchQueue.main.async { // Needed for general iOS 16 bug with focus + focus = newValue ? .text : .secure + } + } + } + } +} + +@available(macOS 13.0, *) +struct SecureInput_Previews: PreviewProvider { + static var previews: some View { + Group { + SecureTextField(title: "caiyun_translate", placeholder: "service.configuration.input.placeholder", text: .constant("1234567")) + .padding() + .previewLayout(.fixed(width: 400, height: 100)) + + SecureTextField(title: "caiyun_translate", placeholder: "service.configuration.input.placeholder", text: .constant("")) + .padding() + .preferredColorScheme(.dark) + .previewLayout(.fixed(width: 400, height: 100)) + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift new file mode 100644 index 000000000..83fa05c33 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationCells.swift @@ -0,0 +1,126 @@ +// +// ServiceConfigurationCells.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import SwiftUI + +@available(macOS 13.0, *) +struct ServiceConfigurationSecureInputCell: View { + @Default var value: String? + let textFieldTitleKey: LocalizedStringKey + let placeholder: LocalizedStringKey + + init( + textFieldTitleKey: LocalizedStringKey, + key: Defaults.Key, + placeholder: LocalizedStringKey = "service.configuration.input.placeholder" + ) { + self.textFieldTitleKey = textFieldTitleKey + self.placeholder = placeholder + _value = .init(key) + } + + var body: some View { + SecureTextField(title: textFieldTitleKey, placeholder: placeholder, text: $value) + } +} + +@available(macOS 13.0, *) +struct ServiceConfigurationInputCell: View { + @Default var value: String? + let textFieldTitleKey: LocalizedStringKey + let placeholder: LocalizedStringKey + + init(textFieldTitleKey: LocalizedStringKey, key: Defaults.Key, placeholder: LocalizedStringKey) { + self.textFieldTitleKey = textFieldTitleKey + self.placeholder = placeholder + _value = .init(key) + } + + var body: some View { + TextField(textFieldTitleKey, text: $value ?? "", prompt: Text(placeholder)) + .padding(10.0) + } +} + +@available(macOS 13.0, *) +struct ServiceConfigurationPickerCell: View { + @Default var value: T + let titleKey: LocalizedStringKey + let values: [T] + + init(titleKey: LocalizedStringKey, key: Defaults.Key, values: [T]) { + self.titleKey = titleKey + self.values = values + _value = .init(key) + } + + var body: some View { + Picker(titleKey, selection: $value) { + ForEach(values, id: \.self) { value in + Text(value.title) + } + } + .padding(10.0) + } +} + +class ConfigurationToggleViewModel: ObservableObject { + @Published var isOn = false +} + +@available(macOS 13.0, *) +struct ServiceConfigurationToggleCell: View { + @Default var value: String + let titleKey: LocalizedStringKey + + @ObservedObject var viewModel = ConfigurationToggleViewModel() + + init(titleKey: LocalizedStringKey, key: Defaults.Key) { + self.titleKey = titleKey + _value = .init(key) + viewModel.isOn = value == "1" + } + + var body: some View { + Toggle(titleKey, isOn: $viewModel.isOn) + .padding(10.0) + .onChange(of: viewModel.isOn) { newValue in + value = newValue ? "1" : "0" + } + } +} + +@available(macOS 13.0, *) +#Preview { + Group { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.openai.api_key.title", + key: .openAIAPIKey, + placeholder: "service.configuration.openai.api_key.placeholder" + ) + + ServiceConfigurationInputCell( + textFieldTitleKey: "service.configuration.openai.endpoint.title", + key: .openAIEndPoint, + placeholder: "service.configuration.openai.endpoint.placeholder" + ) + + // model + ServiceConfigurationPickerCell( + titleKey: "service.configuration.openai.model.title", + key: .openAIModel, + values: OpenAIModels.allCases + ) + + ServiceConfigurationToggleCell( + titleKey: "service.configuration.openai.translation.title", + key: .openAITranslation + ) + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift new file mode 100644 index 000000000..7586cfdf7 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift @@ -0,0 +1,116 @@ +// +// ServiceConfigurationSecretSectionView.swift +// Easydict +// +// Created by phlpsong on 2024/1/31. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import Defaults +import SwiftUI + +@available(macOS 13.0, *) +struct ServiceConfigurationSecretSectionView: View { + var service: QueryService + let content: Content + + @StateObject private var viewModel: ServiceValidationViewModel + + init( + service: QueryService, + observeKeys: [Defaults.Key], + @ViewBuilder content: () -> Content + ) { + self.service = service + self.content = content() + _viewModel = .init(wrappedValue: ServiceValidationViewModel(observing: observeKeys)) + } + + var header: some View { + HStack(alignment: .lastTextBaseline) { + Text(service.name()) + Spacer() + } + } + + var footer: some View { + Button { + validate() + } label: { + Group { + if viewModel.isValidating { + ProgressView() + .controlSize(.small) + .progressViewStyle(.circular) + } else { + Text("service.configuration.validate") + } + } + } + .disabled(viewModel.isValidateBtnDisabled) + } + + var body: some View { + Section { + content + } header: { + header + } footer: { + footer + } + .alert(viewModel.alertMessage, isPresented: $viewModel.isAlertPresented, actions: { + Button("ok") { + viewModel.isAlertPresented = false + } + }) + } + + func validate() { + viewModel.isValidating.toggle() + service.validate { _, error in + DispatchQueue.main.async { + viewModel.alertMessage = error == nil ? "service.configuration.validation_success" : "service.configuration.validation_fail" + print("\(service.serviceType()) validate \(error == nil ? "success" : "fail")!") + viewModel.isValidating.toggle() + viewModel.isAlertPresented.toggle() + } + } + } +} + +@MainActor +private class ServiceValidationViewModel: ObservableObject { + @Published var isAlertPresented = false + + @Published var isValidating = false + + @Published var alertMessage: LocalizedStringKey = "" + + @Published var isValidateBtnDisabled = false + + var cancellables: [AnyCancellable] = [] + + init(observing keys: [Defaults.Key]) { + cancellables.append( + // check secret key empty input + Defaults.publisher(keys: keys) + .sink { [weak self] _ in + let hasEmptyInput = keys.contains(where: { (Defaults[$0] ?? "").isEmpty }) + DispatchQueue.main.async { + self?.isValidateBtnDisabled = hasEmptyInput + } + } + ) + } +} + +@available(macOS 13.0, *) +#Preview { + ServiceConfigurationSecretSectionView(service: EZBingService(), observeKeys: [.bingCookieKey]) { + ServiceConfigurationSecureInputCell( + textFieldTitleKey: "service.configuration.bing.cookie.title", + key: .bingCookieKey + ) + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift index 42c59d0ee..98a36f433 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSection.swift @@ -30,9 +30,10 @@ struct ServiceStringConfigurationSection: View { let value = Binding.init { value.wrappedValue ?? "" } set: { newValue in - value.wrappedValue = newValue + value.wrappedValue = newValue.trimmingCharacters(in: .whitespaces) } TextField(textFieldTitleKey, text: value, prompt: Text(prompt)) + .lineLimit(1) }, footer: footer ) @@ -67,7 +68,7 @@ struct ServiceConfigurationSection: HStack(alignment: .lastTextBaseline) { Text(titleKey) Spacer() - Button("service.service_configuration.reset") { + Button("service.configuration.reset") { _value.reset() } .buttonStyle(.plain) From ea3574eb98b563cf4494cd574e042cccd233d0bd Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 2 Feb 2024 12:58:10 +0800 Subject: [PATCH 55/72] docs: update sponsor list --- README.md | 1 + README_EN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 3e2f06b69..b53f1c3b4 100644 --- a/README.md +++ b/README.md @@ -825,6 +825,7 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | | 2024-01-23 | ㅤ | 5 | | | 2024-01-28 | ㅤ | 7 | | +| 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。|

diff --git a/README_EN.md b/README_EN.md index eeffe8858..7b6f14aa6 100644 --- a/README_EN.md +++ b/README_EN.md @@ -822,6 +822,7 @@ If you don't want your username to be displayed in the list, please choose anony | 2024-01-16 | ㅤ sd | 5 | 大佬牛逼🐂🍺 | | 2024-01-23 | ㅤ | 5 | | | 2024-01-28 | ㅤ | 7 | | +| 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。|

From ff9268d9ac00b1d6153a3e622694714ef8935687 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sun, 4 Feb 2024 11:31:45 +0800 Subject: [PATCH 56/72] docs: update sponsor list --- README.md | 1 + README_EN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index b53f1c3b4..e2891cc9b 100644 --- a/README.md +++ b/README.md @@ -826,6 +826,7 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2024-01-23 | ㅤ | 5 | | | 2024-01-28 | ㅤ | 7 | | | 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。| +| 2024-02-04 | ll | 20 | |

diff --git a/README_EN.md b/README_EN.md index 7b6f14aa6..40020a6ee 100644 --- a/README_EN.md +++ b/README_EN.md @@ -823,6 +823,7 @@ If you don't want your username to be displayed in the list, please choose anony | 2024-01-23 | ㅤ | 5 | | | 2024-01-28 | ㅤ | 7 | | | 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。| +| 2024-02-04 | ll | 20 | |

From cfe29c7a2a542bb9909f2874aaacdc9cb698fc15 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:06:41 +0800 Subject: [PATCH 57/72] fix: use default List selection behavior (#390) --- .../Contents.json | 38 ------------------- .../View/SettingView/Tabs/ServiceTab.swift | 27 +------------ 2 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json diff --git a/Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json deleted file mode 100644 index f6b481345..000000000 --- a/Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.847", - "red" : "0.706" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.251", - "green" : "0.251", - "red" : "0.251" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift index 83a5f6398..77a0f5a1c 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift @@ -13,33 +13,20 @@ import SwiftUI struct ServiceTab: View { @StateObject private var viewModel: ServiceTabViewModel = .init() - @Environment(\.colorScheme) private var colorScheme - - var bgColor: Color { - Color(nsColor: colorScheme == .light ? .windowBackgroundColor : .controlBackgroundColor) - } - - var tableColor: Color { - Color(nsColor: colorScheme == .light ? .ez_tableRowViewBgLight() : .ez_tableRowViewBgDark()) - } - var body: some View { HStack { VStack { WindowTypePicker(windowType: $viewModel.windowType) .padding() - List { + List(selection: $viewModel.selectedService) { ServiceItems() } - .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 @@ -172,18 +159,6 @@ private struct ServiceItemView: View { .listRowSeparator(.hidden) .listRowInsets(.init()) .padding(10) - .listRowBackground(viewModel.selectedService == service ? Color("service_cell_highlight_color") : tableColor) - .overlay { - TapHandler { - viewModel.selectedService = service - } - } - } - - @Environment(\.colorScheme) private var colorScheme - - private var tableColor: Color { - Color(nsColor: colorScheme == .light ? .ez_tableRowViewBgLight() : .ez_tableRowViewBgDark()) } } From 82e6961a8302714379f8663f66458caa35e0c8aa Mon Sep 17 00:00:00 2001 From: Sharker <1548742234@qq.com> Date: Wed, 7 Feb 2024 11:43:48 +0800 Subject: [PATCH 58/72] In app shortcut (#389) * feat: split global shortcut * fix: restore app shortcut * feat: add main menu shortcut part * feat: add default app shortcut * feat: change add default app shortcut place * feat: add Divider * fix: review localized string * fix: create keyCombo clearly * fix: optimize AppshortcutSetting and GlobalShortcut * fix: remove unused code * fix: refactor command * fix: UI optimize * fix: remove unused code * fix: remove unused code * fix: try fix bug * perf: adjust settings page size * perf: adjust shortcut view size * Apply suggestions from code review Co-authored-by: Tisfeng * Update Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift * fix: appshortcutsetting shortcut type * fix: review localizable * fix: optimize code * fix: optimize code --------- Co-authored-by: Tisfeng --- Easydict.xcodeproj/project.pbxproj | 70 +++++-- Easydict/App/Localizable.xcstrings | 173 +++++++++++++++++- .../Configuration+Defaults.swift | 34 +++- Easydict/NewApp/EasydictApp.swift | 6 +- .../Feature/Shortcut/Shortcut+Bind.swift | 96 ++++++++++ .../Feature/Shortcut/Shortcut+Default.swift | 31 ++++ ...lidator.swift => Shortcut+Validator.swift} | 2 +- .../NewApp/Feature/Shortcut/Shortcut.swift | 152 ++++++++++----- .../View/MenuView/MainMenuCommand.swift | 18 ++ .../MenuView/MainMenuShortcutCommand.swift | 40 ++++ .../MainMenuShortcutCommandItem.swift | 61 ++++++ .../NewApp/View/SettingView/SettingView.swift | 21 +-- .../View/SettingView/Tabs/ShortcutTab.swift | 6 +- .../View/Shortcut/AppShortcutSetting.swift | 59 ++++++ .../Shortcut/GeneralShortcutSetting.swift | 69 ------- .../View/Shortcut/GlobalShortcutSetting.swift | 49 +++++ .../View/Shortcut/KeyHolderAlterView.swift | 42 +++++ .../Tabs/View/Shortcut/KeyHolderRowView.swift | 24 +++ ...erWrapper.swift => KeyHolderWrapper.swift} | 71 ++++++- 19 files changed, 866 insertions(+), 158 deletions(-) create mode 100644 Easydict/NewApp/Feature/Shortcut/Shortcut+Bind.swift create mode 100644 Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift rename Easydict/NewApp/Feature/Shortcut/{ShortcutValidator.swift => Shortcut+Validator.swift} (99%) create mode 100644 Easydict/NewApp/View/MenuView/MainMenuCommand.swift create mode 100644 Easydict/NewApp/View/MenuView/MainMenuShortcutCommand.swift create mode 100644 Easydict/NewApp/View/MenuView/MainMenuShortcutCommandItem.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift delete mode 100644 Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GlobalShortcutSetting.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderAlterView.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderRowView.swift rename Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/{GeneralKeyHolderWrapper.swift => KeyHolderWrapper.swift} (62%) diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index e98fe4c7d..4f0d47908 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -271,10 +271,18 @@ 62E2BF4B2B4082BA00E42D38 /* AliResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E2BF482B4082BA00E42D38 /* AliResponse.swift */; }; 62E2BF4C2B4082BA00E42D38 /* AliTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E2BF492B4082BA00E42D38 /* AliTranslateType.swift */; }; 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 62ED29A12B15F1F500901F51 /* EZWrapView.m */; }; - 960835502B6791F200C6A931 /* ShortcutValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9608354F2B6791F200C6A931 /* ShortcutValidator.swift */; }; + 960835502B6791F200C6A931 /* Shortcut+Validator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */; }; 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96099AE12B5D40330055C4DD /* ShortcutTab.swift */; }; - 9627F9382B59956800B1E999 /* GeneralShortcutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9352B59956800B1E999 /* GeneralShortcutSetting.swift */; }; - 9627F9392B59956800B1E999 /* GeneralKeyHolderWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9362B59956800B1E999 /* GeneralKeyHolderWrapper.swift */; }; + 9627F9382B59956800B1E999 /* GlobalShortcutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9352B59956800B1E999 /* GlobalShortcutSetting.swift */; }; + 9627F9392B59956800B1E999 /* KeyHolderWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9627F9362B59956800B1E999 /* KeyHolderWrapper.swift */; }; + 9643D9392B6F49E0000FBEA6 /* AppShortcutSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9382B6F49E0000FBEA6 /* AppShortcutSetting.swift */; }; + 9643D93D2B6F829C000FBEA6 /* MainMenuCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */; }; + 9643D9402B6FC426000FBEA6 /* MainMenuShortcutCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */; }; + 9643D9422B6FE4AF000FBEA6 /* Shortcut+Bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */; }; + 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */; }; + 9643D9462B71D103000FBEA6 /* KeyHolderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */; }; + 9643D94A2B71EABE000FBEA6 /* KeyHolderAlterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */; }; + 9643D94C2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */; }; 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */; }; 967712EA2B5B913600105E0F /* KeyHolder in Frameworks */ = {isa = PBXBuildFile; productRef = 967712E92B5B913600105E0F /* KeyHolder */; }; 967712EE2B5B943400105E0F /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967712ED2B5B943400105E0F /* Shortcut.swift */; }; @@ -290,9 +298,8 @@ DC46DF802B4417B900DEAE3E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC46DF7F2B4417B900DEAE3E /* Configuration.swift */; }; DC6D9C872B352EBC0055EFFC /* FontSizeHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */; }; DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6D9C882B3969510055EFFC /* Appearance.swift */; }; - EA1013442B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */; }; - EA3B81F92B5254AA004C0E8B /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration.swift */; }; DCF176F22B57CED700CA6026 /* Configuration+UserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */; }; + EA1013442B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */; }; EA3B81F92B5254AA004C0E8B /* Configuration+Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */; }; EA3B81FC2B52555C004C0E8B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = EA3B81FB2B52555C004C0E8B /* Defaults */; }; EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */; }; @@ -787,10 +794,18 @@ 62ED29A12B15F1F500901F51 /* EZWrapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZWrapView.m; sourceTree = ""; }; 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Easydict.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 91E3E579C6DB88658B4BB102 /* Pods-Easydict.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.release.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.release.xcconfig"; sourceTree = ""; }; - 9608354F2B6791F200C6A931 /* ShortcutValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutValidator.swift; sourceTree = ""; }; + 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Validator.swift"; sourceTree = ""; }; 96099AE12B5D40330055C4DD /* ShortcutTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutTab.swift; sourceTree = ""; }; - 9627F9352B59956800B1E999 /* GeneralShortcutSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralShortcutSetting.swift; sourceTree = ""; }; - 9627F9362B59956800B1E999 /* GeneralKeyHolderWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralKeyHolderWrapper.swift; sourceTree = ""; }; + 9627F9352B59956800B1E999 /* GlobalShortcutSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalShortcutSetting.swift; sourceTree = ""; }; + 9627F9362B59956800B1E999 /* KeyHolderWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyHolderWrapper.swift; sourceTree = ""; }; + 9643D9382B6F49E0000FBEA6 /* AppShortcutSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutSetting.swift; sourceTree = ""; }; + 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuCommand.swift; sourceTree = ""; }; + 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommand.swift; sourceTree = ""; }; + 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Bind.swift"; sourceTree = ""; }; + 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Default.swift"; sourceTree = ""; }; + 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderRowView.swift; sourceTree = ""; }; + 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderAlterView.swift; sourceTree = ""; }; + 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommandItem.swift; sourceTree = ""; }; 9672D7D02B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MASShortcutBinder+EZMASShortcutBinder.h"; sourceTree = ""; }; 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutBinder+EZMASShortcutBinder.m"; sourceTree = ""; }; 967712ED2B5B943400105E0F /* Shortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcut.swift; sourceTree = ""; }; @@ -807,9 +822,8 @@ DC46DF7F2B4417B900DEAE3E /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; DC6D9C862B352EBC0055EFFC /* FontSizeHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeHintView.swift; sourceTree = ""; }; DC6D9C882B3969510055EFFC /* Appearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; - EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyCombo+Defaults.Serializable.swift"; sourceTree = ""; }; - EA3B81F82B5254AA004C0E8B /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; DCF176F12B57CED700CA6026 /* Configuration+UserData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+UserData.swift"; sourceTree = ""; }; + EA1013432B5DBDB1005E43F9 /* KeyCombo+Defaults.Serializable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyCombo+Defaults.Serializable.swift"; sourceTree = ""; }; EA3B81F82B5254AA004C0E8B /* Configuration+Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Configuration+Defaults.swift"; sourceTree = ""; }; EA9943E22B534C3300EE7B97 /* TTSServiceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTSServiceType.swift; sourceTree = ""; }; EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageDetectOptimizeExtensions.swift; sourceTree = ""; }; @@ -2133,6 +2147,7 @@ 27FE98062B3DD525000AD654 /* View */ = { isa = PBXGroup; children = ( + 9643D93E2B6FC405000FBEA6 /* MenuView */, 27FE980A2B3DD5D1000AD654 /* MenuItemView.swift */, 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */, 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */, @@ -2224,12 +2239,25 @@ 9627F9342B59956800B1E999 /* Shortcut */ = { isa = PBXGroup; children = ( - 9627F9352B59956800B1E999 /* GeneralShortcutSetting.swift */, - 9627F9362B59956800B1E999 /* GeneralKeyHolderWrapper.swift */, + 9627F9352B59956800B1E999 /* GlobalShortcutSetting.swift */, + 9627F9362B59956800B1E999 /* KeyHolderWrapper.swift */, + 9643D9382B6F49E0000FBEA6 /* AppShortcutSetting.swift */, + 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */, + 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */, ); path = Shortcut; sourceTree = ""; }; + 9643D93E2B6FC405000FBEA6 /* MenuView */ = { + isa = PBXGroup; + children = ( + 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */, + 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */, + 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */, + ); + path = MenuView; + sourceTree = ""; + }; 967712EB2B5B93E200105E0F /* Feature */ = { isa = PBXGroup; children = ( @@ -2242,7 +2270,9 @@ isa = PBXGroup; children = ( 967712ED2B5B943400105E0F /* Shortcut.swift */, - 9608354F2B6791F200C6A931 /* ShortcutValidator.swift */, + 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */, + 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */, + 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */, ); path = Shortcut; sourceTree = ""; @@ -2777,7 +2807,7 @@ 62E2BF4A2B4082BA00E42D38 /* AliService.swift in Sources */, 03B0233729231FA6001C7E63 /* MMMake.m in Sources */, 03B0232E29231FA6001C7E63 /* MMCrashSignalExceptionHandler.m in Sources */, - 9627F9382B59956800B1E999 /* GeneralShortcutSetting.swift in Sources */, + 9627F9382B59956800B1E999 /* GlobalShortcutSetting.swift in Sources */, 03BDA7C42A26DA280079D04F /* NSDictionary+RubyDescription.m in Sources */, 62ED29A22B15F1F500901F51 /* EZWrapView.m in Sources */, C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */, @@ -2803,7 +2833,7 @@ 03B3B8B22925D5B200168E8D /* EZPopButtonWindow.m in Sources */, 03B0231529231FA6001C7E63 /* SnipWindow.m in Sources */, 033363A0293A05D200FED9C8 /* EZSelectLanguageButton.m in Sources */, - 960835502B6791F200C6A931 /* ShortcutValidator.swift in Sources */, + 960835502B6791F200C6A931 /* Shortcut+Validator.swift in Sources */, 03542A522937B69200C34C33 /* EZYoudaoTranslateResponse.m in Sources */, 03B0230129231FA6001C7E63 /* EZQueryView.m in Sources */, 03542A3D2937AF4F00C34C33 /* EZQueryResult.m in Sources */, @@ -2835,6 +2865,7 @@ 0AC8A8392B666F07006DA5CC /* CaiyunService+ConfigurableService.swift in Sources */, 27FE980B2B3DD5D1000AD654 /* MenuItemView.swift in Sources */, 03DC7C6A2A3CA852000BF7C9 /* EZAppCell.m in Sources */, + 9643D9392B6F49E0000FBEA6 /* AppShortcutSetting.swift in Sources */, 96099AE22B5D40330055C4DD /* ShortcutTab.swift in Sources */, 0399C6AC29A860AA00B4AFCC /* EZOpenAIService.m in Sources */, 03542A432937B45E00C34C33 /* EZBaiduTranslate.m in Sources */, @@ -2855,6 +2886,7 @@ 039F5506294B6E29004AB940 /* EZSettingViewController.m in Sources */, 03BD281E29481C0400F5891A /* EZAudioPlayer.m in Sources */, 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */, + 9643D94A2B71EABE000FBEA6 /* KeyHolderAlterView.swift in Sources */, 03E02A2629250D1D00A10260 /* EZEventMonitor.m in Sources */, 03B0233429231FA6001C7E63 /* MMConsoleLogFormatter.m in Sources */, 037852B9295D49F900D0E2CF /* EZTableRowView.m in Sources */, @@ -2888,14 +2920,17 @@ 0396D615292CC4C3006A11D9 /* EZLocalStorage.m in Sources */, 0329CD6F29EE924500963F78 /* EZRightClickDetector.m in Sources */, 033C30FC2A7409C40095926A /* TTTDictionary.m in Sources */, + 9643D93D2B6F829C000FBEA6 /* MainMenuCommand.swift in Sources */, 03B0232D29231FA6001C7E63 /* NSArray+MM.m in Sources */, 039E5021296E5D9900072344 /* EZScrollViewController.m in Sources */, + 9643D94C2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift in Sources */, 0AC8A8412B695480006DA5CC /* DeepLTranslate+ConfigurableService.swift in Sources */, 039CC90D292F664E0037B91E /* NSObject+EZWindowType.m in Sources */, 03B0232229231FA6001C7E63 /* NSImage+MM.m in Sources */, 03BB2DEF29F59C8A00447EDD /* EZSymbolImageButton.m in Sources */, 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */, C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */, + 9643D9402B6FC426000FBEA6 /* MainMenuShortcutCommand.swift in Sources */, 62A2D03F2A82967F007EEB01 /* EZBingRequest.m in Sources */, 03BDA7BE2A26DA280079D04F /* XPMCountedArgument.m in Sources */, 038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */, @@ -2904,6 +2939,7 @@ DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */, 0396D611292C932F006A11D9 /* EZSelectLanguageCell.m in Sources */, 036196752A000F5900806370 /* FWEncryptorAES.m in Sources */, + 9643D9462B71D103000FBEA6 /* KeyHolderRowView.swift in Sources */, 0399C6A829A74E0F00B4AFCC /* EZQueryResult+EZDeepLTranslateResponse.m in Sources */, 039B694F2A9D9F370063709D /* EZWebViewManager.m in Sources */, 03B0231629231FA6001C7E63 /* SnipFocusView.m in Sources */, @@ -2936,6 +2972,7 @@ 27FE98052B3DCB09000AD654 /* NewAppManager.swift in Sources */, 0399116A292AA2EF00E1B06D /* EZLayoutManager.m in Sources */, 0320C5872B29F35700861B3D /* QueryServiceRecord.swift in Sources */, + 9643D9422B6FE4AF000FBEA6 /* Shortcut+Bind.swift in Sources */, 03FC699A2B39D13A0035D2DA /* EZOpenAIChatResponse.m in Sources */, 03B022FA29231FA6001C7E63 /* EZServiceTypes.m in Sources */, EAE3D3502B62E9DE001EE3E3 /* GlobalContext.swift in Sources */, @@ -2943,7 +2980,7 @@ 03B0233129231FA6001C7E63 /* MMCrash.m in Sources */, 03B0232629231FA6001C7E63 /* NSAttributedString+MM.m in Sources */, 03542A402937B3C900C34C33 /* EZOCRResult.m in Sources */, - 9627F9392B59956800B1E999 /* GeneralKeyHolderWrapper.swift in Sources */, + 9627F9392B59956800B1E999 /* KeyHolderWrapper.swift in Sources */, C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */, 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */, EA9943E32B534C3300EE7B97 /* TTSServiceType.swift in Sources */, @@ -2963,6 +3000,7 @@ 03BDA7B92A26DA280079D04F /* NSProcessInfo+XPMArgumentParser.m in Sources */, 03542A4F2937B64B00C34C33 /* EZYoudaoOCRResponse.m in Sources */, 03B0233929231FA6001C7E63 /* MMTool.m in Sources */, + 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */, 03542A552937B7DE00C34C33 /* EZError.m in Sources */, 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */, 03BDA7B82A26DA280079D04F /* XPMValuedArgument.m in Sources */, diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index a2ab7b33c..fe1356419 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -169,6 +169,23 @@ } } }, + "app_shortcut_setting" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "App Shortcut" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "应用内快捷键" + } + } + } + }, "appearenceType_dark" : { "localizations" : { "en" : { @@ -2376,7 +2393,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "打开窗口时自动选中查询文本" } } @@ -3563,6 +3580,40 @@ } } }, + "shortcut_clear_all" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clear All" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空所有" + } + } + } + }, + "shortcut_clear_input" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clear Input" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清空查询内容" + } + } + } + }, "shortcut_confict %@" : { "extractionState" : "manual", "localizations" : { @@ -3598,7 +3649,14 @@ } }, "shortcut_confict_message (Shortcut.shared.confictMenuItem?.title ?? \"\")" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "shortcut_confict_message %@" : { "extractionState" : "manual", @@ -3618,7 +3676,14 @@ } }, "shortcut_confict_title (keyCombo!.keyEquivalentModifierMaskString + keyCombo!.characters)" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "" + } + } + } }, "shortcut_confict_title %@" : { "extractionState" : "manual", @@ -3637,6 +3702,108 @@ } } }, + "shortcut_copy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制文本" + } + } + } + }, + "shortcut_copy_first_translated_text" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy first translated text" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制第一个翻译结果" + } + } + } + }, + "shortcut_decrease_font" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Decrease Font Size" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "减小字体" + } + } + } + }, + "shortcut_focus" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Focus" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "聚焦" + } + } + } + }, + "shortcut_increase_font" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Increase Font Size" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "增大字体" + } + } + } + }, + "shortcut_play" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Play" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "播放单词发音" + } + } + } + }, "shortcut_select_translate_window_type" : { "localizations" : { "en" : { diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index 214e1bb5c..240aed001 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -10,6 +10,12 @@ import Defaults import Foundation import Magnet +/// Utils +extension Defaults.Keys { + /// is first launch + static let firstLaunch = Key("EZConfiguration_kFirstLaunch", default: true) +} + // Setting extension Defaults.Keys { // rename `from` @@ -171,9 +177,27 @@ extension Defaults.Keys { /// shortcut extension Defaults.Keys { - static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder", default: nil) - static let snipShortcut = Key("EZSnipShortcutKey_keyHolder", default: nil) - static let inputShortcut = Key("EZInputShortcutKey_keyHolder", default: nil) - static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder", default: nil) - static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder", default: nil) + // Global + static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder") + static let snipShortcut = Key("EZSnipShortcutKey_keyHolder") + static let inputShortcut = Key("EZInputShortcutKey_keyHolder") + static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder") + static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder") + + // App + static let clearInputShortcut = Key("EZClearInputShortcutKey_keyHolder") + static let clearAllShortcut = Key("EZClearAllShortcutKey_keyHolder") + static let copyShortcut = Key("EZCopyShortcutKey_keyHolder") + static let copyFirstResultShortcut = Key("EZCopyFirstResultShortcutKey_keyHolder") + static let focusShortcut = Key("EZFocusShortcutKey_keyHolder") + static let playShortcut = Key("EZPlayShortcutKey_keyHolder") + static let retryShortcut = Key("EZRetryShortcutKey_keyHolder") + static let toggleShortcut = Key("EZToggleShortcutKey_keyHolder") + static let pinShortcut = Key("EZPinShortcutKey_keyHolder") + static let hideShortcut = Key("EZHideShortcutKey_keyHolder") + static let increaseFontSize = Key("EZIncreaseFontSizeShortcutKey_keyHolder") + static let decreaseFontSize = Key("EZDecreaseFontSizeShortcutKey_keyHolder") + static let googleShortcut = Key("EZGoogleShortcutKey_keyHolder") + static let eudicShortcut = Key("EZEudicShortcutKey_keyHolder") + static let appleDictionaryShortcut = Key("EZAppleDictionaryShortcutKey_keyHolder") } diff --git a/Easydict/NewApp/EasydictApp.swift b/Easydict/NewApp/EasydictApp.swift index 95e985b93..9743e4dc4 100644 --- a/Easydict/NewApp/EasydictApp.swift +++ b/Easydict/NewApp/EasydictApp.swift @@ -52,7 +52,11 @@ struct EasydictApp: App { .scaledToFit() } .help("Easydict 🍃") - }.menuBarExtraStyle(.menu) + } + .menuBarExtraStyle(.menu) + .commands { + EasyDictMainMenu() // main menu + } Settings { SettingView() } diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Bind.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Bind.swift new file mode 100644 index 000000000..37008348c --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Bind.swift @@ -0,0 +1,96 @@ +// +// Shortcut+Bind.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +// App shortcut binding func +extension Shortcut { + @objc func clearInput() { + EZWindowManager.shared().clearInput() + } + + @objc func clearAll() { + EZWindowManager.shared().clearAll() + } + + @objc func shortcutCopy() { + EZWindowManager.shared().copyQueryText() + } + + @objc func shortcutCopyFirstResult() { + EZWindowManager.shared().copyFirstTranslatedText() + } + + @objc func shortcutFocus() { + EZWindowManager.shared().focusInputTextView() + } + + @objc func shortcutPlay() { + EZWindowManager.shared().playOrStopQueryTextAudio() + } + + @objc func shortcutRetry() { + EZWindowManager.shared().rerty() + } + + @objc func shortcutToggle() { + EZWindowManager.shared().toggleTranslationLanguages() + } + + @objc func shortcutPin() { + EZWindowManager.shared().pin() + } + + @objc func shortcutHide() { + EZWindowManager.shared().closeWindowOrExitSreenshot() + } + + @objc func increaseFontSize() { + Configuration.shared.fontSizeIndex += 1 + } + + @objc func decreaseFontSize() { + Configuration.shared.fontSizeIndex -= 1 + } + + @objc func shortcutGoogle() { + let window = EZWindowManager.shared().floatingWindow + window?.titleBar.googleButton.openLink() + } + + @objc func shortcutEudic() { + let window = EZWindowManager.shared().floatingWindow + window?.titleBar.eudicButton.openLink() + } + + @objc func shortcutAppleDic() { + let window = EZWindowManager.shared().floatingWindow + window?.titleBar.appleDictionaryButton.openLink() + } +} + +// global shortcut binding func +extension Shortcut { + @objc func selectTextTranslate() { + EZWindowManager.shared().selectTextTranslate() + } + + @objc func snipTranslate() { + EZWindowManager.shared().snipTranslate() + } + + @objc func inputTranslate() { + EZWindowManager.shared().inputTranslate() + } + + @objc func showMiniFloatingWindow() { + EZWindowManager.shared().showMiniFloatingWindow() + } + + @objc func screenshotOCR() { + EZWindowManager.shared().screenshotOCR() + } +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift new file mode 100644 index 000000000..d48d74d1d --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift @@ -0,0 +1,31 @@ +// +// Shortcut+Default.swift +// Easydict +// +// Created by Sharker on 2024/2/5. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Magnet + +extension Shortcut { + // set defalut for app shortcut + func setDefaultForAppShortcut() { + Defaults[.clearInputShortcut] = KeyCombo(key: .k, cocoaModifiers: .command) + Defaults[.clearAllShortcut] = KeyCombo(key: .k, cocoaModifiers: [.command, .shift]) + Defaults[.copyShortcut] = KeyCombo(key: .c, cocoaModifiers: [.command, .shift]) + Defaults[.copyFirstResultShortcut] = KeyCombo(key: .j, cocoaModifiers: [.command, .shift]) + Defaults[.focusShortcut] = KeyCombo(key: .i, cocoaModifiers: .command) + Defaults[.playShortcut] = KeyCombo(key: .s, cocoaModifiers: .command) + Defaults[.retryShortcut] = KeyCombo(key: .r, cocoaModifiers: .command) + Defaults[.toggleShortcut] = KeyCombo(key: .t, cocoaModifiers: .command) + Defaults[.pinShortcut] = KeyCombo(key: .p, cocoaModifiers: .command) + Defaults[.hideShortcut] = KeyCombo(key: .y, cocoaModifiers: .command) + Defaults[.increaseFontSize] = KeyCombo(key: .keypadPlus, cocoaModifiers: .command) + Defaults[.decreaseFontSize] = KeyCombo(key: .keypadMinus, cocoaModifiers: .command) + Defaults[.googleShortcut] = KeyCombo(key: .return, cocoaModifiers: .command) + Defaults[.eudicShortcut] = KeyCombo(key: .return, cocoaModifiers: [.command, .shift]) + Defaults[.appleDictionaryShortcut] = KeyCombo(key: .d, cocoaModifiers: [.command, .shift]) + } +} diff --git a/Easydict/NewApp/Feature/Shortcut/ShortcutValidator.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Validator.swift similarity index 99% rename from Easydict/NewApp/Feature/Shortcut/ShortcutValidator.swift rename to Easydict/NewApp/Feature/Shortcut/Shortcut+Validator.swift index 0f818a228..f7590400e 100644 --- a/Easydict/NewApp/Feature/Shortcut/ShortcutValidator.swift +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Validator.swift @@ -1,5 +1,5 @@ // -// ShortcutValidator.swift +// Shortcut+Validator.swift // Easydict // // Created by Sharker on 2024/1/29. diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift index 434c33c77..c08c2dc2a 100644 --- a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift @@ -13,11 +13,75 @@ import SwiftUI /// Shortcut Service public enum ShortcutType: String { + // Global case inputTranslate = "EZInputShortcutKey" case snipTranslate = "EZSnipShortcutKey" case selectTranslate = "EZSelectionShortcutKey" case silentScreenshotOcr = "EZScreenshotOCRShortcutKey" case showMiniWindow = "EZShowMiniShortcutKey" + // In App + case clearInput = "EZClearInputShortcutKey" + case clearAll = "EZClearAllShortcutKey" + case copy = "EZCopyShortcutKey" + case copyFirstResult = "EZCopyFirstResultShortcutKey" + case focus = "EZFocusShortcutKey" + case play = "EZPlayShortcutKey" + case retry = "EZRetryShortcutKey" + case toggle = "EZToggleShortcutKey" + case pin = "EZPinShortcutKey" + case hide = "EZHideShortcutKey" + case increaseFontSize = "EZIncreaseFontSizeShortcutKey" + case decreaseFontSize = "EZDecreaseFontSizeShortcutKey" + case google = "EZGoogleShortcutKey" + case eudic = "EZEudicShortcutKey" + case appleDic = "EZAppleDicShortcutKey" +} + +extension ShortcutType { + func localizedStringKey() -> String { + switch self { + case .inputTranslate: + "input_translate" + case .snipTranslate: + "snip_translate" + case .selectTranslate: + "select_translate" + case .silentScreenshotOcr: + "silent_screenshot_ocr" + case .showMiniWindow: + "show_mini_window" + case .clearInput: + "shortcut_clear_input" + case .clearAll: + "shortcut_clear_all" + case .copy: + "shortcut_copy" + case .copyFirstResult: + "shortcut_copy_first_translated_text" + case .focus: + "shortcut_focus" + case .play: + "shortcut_play" + case .retry: + "retry" + case .toggle: + "toggle_languages" + case .pin: + "pin" + case .hide: + "hide" + case .increaseFontSize: + "shortcut_increase_font" + case .decreaseFontSize: + "shortcut_decrease_font" + case .google: + "open_in_google" + case .eudic: + "open_in_eudic" + case .appleDic: + "open_in_apple_dictionary" + } + } } // Confict Message @@ -35,6 +99,14 @@ class Shortcut: NSObject { @objc static func setupShortcut() { let shortcut = Shortcut.shared shortcut.restoreShortcut() + + if Defaults[.firstLaunch] { + Defaults[.firstLaunch] = false + // set defalut for app shortcut + shortcut.setDefaultForAppShortcut() + } else { + // do nothing + } } // Make sure the class has only one instance @@ -78,7 +150,7 @@ extension Shortcut { HotKeyCenter.shared.unregisterHotKey(with: type.rawValue) return } - var hotKey: HotKey + var hotKey: HotKey? switch type { case .inputTranslate: hotKey = HotKey(identifier: type.rawValue, @@ -105,54 +177,10 @@ extension Shortcut { keyCombo: keyCombo, target: Shortcut.shared, action: #selector(Shortcut.showMiniFloatingWindow)) + default: () } - hotKey.register() - } -} - -// shortcut binding func -extension Shortcut { - @objc func selectTextTranslate() { - EZWindowManager.shared().selectTextTranslate() - } - @objc func snipTranslate() { - EZWindowManager.shared().snipTranslate() - } - - @objc func inputTranslate() { - EZWindowManager.shared().inputTranslate() - } - - @objc func showMiniFloatingWindow() { - EZWindowManager.shared().showMiniFloatingWindow() - } - - @objc func screenshotOCR() { - EZWindowManager.shared().screenshotOCR() - } -} - -// fetch shortcut KeyCombo -extension Shortcut { - public func shortcutKeyCombo(_ type: ShortcutType) -> KeyCombo? { - switch type { - case .inputTranslate: - guard let keyCombo = Defaults[.inputShortcut] else { return nil } - return keyCombo - case .snipTranslate: - guard let keyCombo = Defaults[.snipShortcut] else { return nil } - return keyCombo - case .selectTranslate: - guard let keyCombo = Defaults[.selectionShortcut] else { return nil } - return keyCombo - case .silentScreenshotOcr: - guard let keyCombo = Defaults[.screenshotOCRShortcut] else { return nil } - return keyCombo - case .showMiniWindow: - guard let keyCombo = Defaults[.showMiniWindowShortcut] else { return nil } - return keyCombo - } + hotKey?.register() } } @@ -169,6 +197,36 @@ struct KeyboardShortcut: ViewModifier { .screenshotOCRShortcut case .showMiniWindow: .showMiniWindowShortcut + case .clearInput: + .clearInputShortcut + case .clearAll: + .clearAllShortcut + case .copy: + .copyShortcut + case .copyFirstResult: + .copyFirstResultShortcut + case .focus: + .focusShortcut + case .play: + .playShortcut + case .retry: + .retryShortcut + case .toggle: + .toggleShortcut + case .pin: + .pinShortcut + case .hide: + .hideShortcut + case .increaseFontSize: + .increaseFontSize + case .decreaseFontSize: + .decreaseFontSize + case .google: + .googleShortcut + case .eudic: + .eudicShortcut + case .appleDic: + .appleDictionaryShortcut } _shortcut = .init(key) diff --git a/Easydict/NewApp/View/MenuView/MainMenuCommand.swift b/Easydict/NewApp/View/MenuView/MainMenuCommand.swift new file mode 100644 index 000000000..84231b7bf --- /dev/null +++ b/Easydict/NewApp/View/MenuView/MainMenuCommand.swift @@ -0,0 +1,18 @@ +// +// MainMenuCommand.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +struct EasyDictMainMenu: Commands { + var body: some Commands { + // shortcut + MainMenuShortcutCommand() + } + + func showAPIWebSite() {} +} diff --git a/Easydict/NewApp/View/MenuView/MainMenuShortcutCommand.swift b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommand.swift new file mode 100644 index 000000000..82ff53423 --- /dev/null +++ b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommand.swift @@ -0,0 +1,40 @@ +// +// MainMenuShortcutCommand.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +extension EasyDictMainMenu { + struct MainMenuShortcutCommand: Commands { + @State private var appShortcutCommandList = [ + MainMenuShortcutCommandDataItem(type: .clearInput), + MainMenuShortcutCommandDataItem(type: .clearAll), + MainMenuShortcutCommandDataItem(type: .copy), + MainMenuShortcutCommandDataItem(type: .copyFirstResult), + MainMenuShortcutCommandDataItem(type: .focus), + MainMenuShortcutCommandDataItem(type: .play), + MainMenuShortcutCommandDataItem(type: .retry), + MainMenuShortcutCommandDataItem(type: .toggle), + MainMenuShortcutCommandDataItem(type: .pin), + MainMenuShortcutCommandDataItem(type: .hide), + MainMenuShortcutCommandDataItem(type: .increaseFontSize), + MainMenuShortcutCommandDataItem(type: .decreaseFontSize), + MainMenuShortcutCommandDataItem(type: .google), + MainMenuShortcutCommandDataItem(type: .eudic), + MainMenuShortcutCommandDataItem(type: .appleDic), + ] + + var body: some Commands { + // shortcut Commands + CommandMenu("shortcut") { + ForEach(appShortcutCommandList) { item in + MainMenuShortcutCommandItem(dataItem: item) + } + } + } + } +} diff --git a/Easydict/NewApp/View/MenuView/MainMenuShortcutCommandItem.swift b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommandItem.swift new file mode 100644 index 000000000..669057c9f --- /dev/null +++ b/Easydict/NewApp/View/MenuView/MainMenuShortcutCommandItem.swift @@ -0,0 +1,61 @@ +// +// MainMenuShortcutCommandItem.swift +// Easydict +// +// Created by Sharker on 2024/2/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +struct MainMenuShortcutCommandDataItem: Identifiable { + public var id: String { type.localizedStringKey() } + var type: ShortcutType +} + +struct MainMenuShortcutCommandItem: View { + public var dataItem: MainMenuShortcutCommandDataItem + + var body: some View { + Button(LocalizedStringKey(dataItem.type.localizedStringKey())) { + switch dataItem.type { + case .clearInput: + Shortcut.shared.clearInput() + case .clearAll: + Shortcut.shared.clearAll() + case .copy: + Shortcut.shared.shortcutCopy() + case .copyFirstResult: + Shortcut.shared.shortcutCopyFirstResult() + case .focus: + Shortcut.shared.shortcutFocus() + case .play: + Shortcut.shared.shortcutPlay() + case .retry: + Shortcut.shared.shortcutRetry() + case .toggle: + Shortcut.shared.shortcutToggle() + case .pin: + Shortcut.shared.shortcutPin() + case .hide: + Shortcut.shared.shortcutHide() + case .increaseFontSize: + Shortcut.shared.increaseFontSize() + case .decreaseFontSize: + Shortcut.shared.decreaseFontSize() + case .google: + Shortcut.shared.shortcutGoogle() + case .eudic: + Shortcut.shared.shortcutEudic() + case .appleDic: + Shortcut.shared.shortcutAppleDic() + default: () + } + } + .keyboardShortcut(dataItem.type) + + if dataItem.type == .toggle || dataItem.type == .decreaseFontSize { + Divider() + } + } +} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index 643ee1c23..2d73db6f0 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -36,13 +36,14 @@ struct SettingView: View { DisabledAppTab() .tabItem { Label("disabled_app_list", systemImage: "nosign") } .tag(SettingTab.disabled) - AdvancedTab() - .tabItem { Label("advanced", systemImage: "gearshape.2") } - .tag(SettingTab.advanced) ShortcutTab() .tabItem { Label("shortcut", systemImage: "command.square") } .tag(SettingTab.shortcut) + + AdvancedTab() + .tabItem { Label("advanced", systemImage: "gearshape.2") } + .tag(SettingTab.advanced) PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } .tag(SettingTab.privacy) @@ -65,24 +66,22 @@ struct SettingView: View { func resizeWindowFrame() { guard let window else { return } - // Disable zoom button, ref: https://stackoverflow.com/a/66039864/8378840 + // Disable zoom button, refer: https://stackoverflow.com/a/66039864/8378840 window.standardWindowButton(.zoomButton)?.isEnabled = false // Keep the settings page windows all the same width to avoid strange animations. - let maxWidth = 750 + let maxWidth = 780 let height = switch selection { - case .general: - maxWidth - 100 - case .service: - 600 - case .disabled, .shortcut: + case .disabled: 500 + case .advanced: + 400 case .privacy: 320 case .about: 450 default: - 400 + maxWidth - 110 } let newSize = CGSize(width: maxWidth, height: height) diff --git a/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift index 52daaabe6..558bcb5cc 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ShortcutTab.swift @@ -13,9 +13,11 @@ struct ShortcutTab: View { var body: some View { Form { // Global shortcut - GeneralShortcutSettingView() + GlobalShortcutSettingView() // In app shortcut - }.formStyle(.grouped) + AppShortcutSettingView() + } + .formStyle(.grouped) } } diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift new file mode 100644 index 000000000..62773bb6e --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/AppShortcutSetting.swift @@ -0,0 +1,59 @@ +// +// AppShortcutSetting.swift +// Easydict +// +// Created by Sharker on 2024/2/4. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +extension ShortcutTab { + struct AppShortcutSettingView: View { + @State private var shortcutDataList = [ + KeyHolderDataItem(type: .clearInput), + KeyHolderDataItem(type: .clearAll), + KeyHolderDataItem(type: .copy), + KeyHolderDataItem(type: .copyFirstResult), + KeyHolderDataItem(type: .focus), + KeyHolderDataItem(type: .play), + KeyHolderDataItem(type: .retry), + KeyHolderDataItem(type: .toggle), + KeyHolderDataItem(type: .pin), + KeyHolderDataItem(type: .hide), + KeyHolderDataItem(type: .increaseFontSize), + KeyHolderDataItem(type: .decreaseFontSize), + KeyHolderDataItem(type: .google), + KeyHolderDataItem(type: .eudic), + KeyHolderDataItem(type: .appleDic), + ] + @State var confictAlterMessage: ShortcutConfictAlertMessage = .init(title: "", message: "") + var body: some View { + let showAlter = Binding( + get: { + !confictAlterMessage.message.isEmpty + }, + set: { _ in } + ) + Section { + ForEach(shortcutDataList) { item in + KeyHolderRowView(title: item.type.localizedStringKey(), type: item.type, confictAlterMessage: $confictAlterMessage) + } + } header: { + Text("app_shortcut_setting") + } + + .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), + isPresented: showAlter, + presenting: confictAlterMessage) + { _ in + Button(String(localized: "shortcut_confict_confirm")) { + confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") + } + } message: { message in + Text(message.message) + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift deleted file mode 100644 index 7387a5b32..000000000 --- a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralShortcutSetting.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// GeneralShortcutSetting.swift -// Easydict -// -// Created by Sharker on 2024/1/1. -// Copyright © 2024 izual. All rights reserved. -// - -import SwiftUI - -@available(macOS 13, *) -extension ShortcutTab { - struct GeneralShortcutSettingView: View { - @State var confictAlterMessage: ShortcutConfictAlertMessage = .init(title: "", message: "") - - var body: some View { - let showAlter = Binding( - get: { - if !confictAlterMessage.message.isEmpty { - true - } else { - false - } - }, set: { _ in - } - ) - Section { - HStack { - Text("input_translate") - Spacer() - GeneralKeyHolderWrapper(shortcutType: .inputTranslate, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) - } - HStack { - Text("snip_translate") - Spacer() - GeneralKeyHolderWrapper(shortcutType: .snipTranslate, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) - } - HStack { - Text("select_translate") - Spacer() - GeneralKeyHolderWrapper(shortcutType: .selectTranslate, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) - } - HStack { - Text("show_mini_window") - Spacer() - GeneralKeyHolderWrapper(shortcutType: .showMiniWindow, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) - } - HStack { - Text("silent_screenshot_ocr") - Spacer() - GeneralKeyHolderWrapper(shortcutType: .silentScreenshotOcr, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 24) - } - } header: { - Text("global_shortcut_setting") - } - - .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), - isPresented: showAlter, - presenting: confictAlterMessage) - { _ in - Button(String(localized: "shortcut_confict_confirm")) { - confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") - } - } message: { message in - Text(message.message) - } - } - } -} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GlobalShortcutSetting.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GlobalShortcutSetting.swift new file mode 100644 index 000000000..0fdcb9083 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GlobalShortcutSetting.swift @@ -0,0 +1,49 @@ +// +// GlobalShortcutSetting.swift +// Easydict +// +// Created by Sharker on 2024/1/1. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +extension ShortcutTab { + struct GlobalShortcutSettingView: View { + @State var confictAlterMessage: ShortcutConfictAlertMessage = .init(title: "", message: "") + @State private var shortcutDataList = [ + KeyHolderDataItem(type: .inputTranslate), + KeyHolderDataItem(type: .snipTranslate), + KeyHolderDataItem(type: .selectTranslate), + KeyHolderDataItem(type: .showMiniWindow), + KeyHolderDataItem(type: .silentScreenshotOcr), + ] + var body: some View { + let showAlter = Binding( + get: { + !confictAlterMessage.message.isEmpty + }, set: { _ in + } + ) + Section { + ForEach(shortcutDataList) { item in + KeyHolderRowView(title: item.type.localizedStringKey(), type: item.type, confictAlterMessage: $confictAlterMessage) + } + } header: { + Text("global_shortcut_setting") + } + + .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), + isPresented: showAlter, + presenting: confictAlterMessage) + { _ in + Button(String(localized: "shortcut_confict_confirm")) { + confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") + } + } message: { message in + Text(message.message) + } + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderAlterView.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderAlterView.swift new file mode 100644 index 000000000..5fa9fe233 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderAlterView.swift @@ -0,0 +1,42 @@ +// +// KeyHolderAlterView.swift +// Easydict +// +// Created by Sharker on 2024/2/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +struct KeyHolderAlterView: ViewModifier { + init(showAlter: Binding, confictAlterMessage: Binding) { + _showAlter = showAlter + _confictAlterMessage = confictAlterMessage + } + + @Binding var showAlter: Bool + @Binding public var confictAlterMessage: ShortcutConfictAlertMessage + + func body(content: Content) -> some View { + content + .alert(String(localized: "shortcut_confict \(confictAlterMessage.title)"), + isPresented: $showAlter, + presenting: confictAlterMessage) + { _ in + Button(String(localized: "shortcut_confict_confirm")) { + confictAlterMessage = ShortcutConfictAlertMessage(title: "", message: "") + } + } message: { message in + Text(message.message) + } + } +} + +@available(macOS 13, *) +public extension View { + @ViewBuilder + func keyHolderConfictAlter(_ showAlter: Binding, _ confictAlterMessage: Binding) -> some View { + modifier(KeyHolderAlterView(showAlter: showAlter, confictAlterMessage: confictAlterMessage)) + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderRowView.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderRowView.swift new file mode 100644 index 000000000..42cbc41d2 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderRowView.swift @@ -0,0 +1,24 @@ +// +// KeyHolderRowView.swift +// Easydict +// +// Created by Sharker on 2024/2/6. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +@available(macOS 13, *) +struct KeyHolderRowView: View { + @State public var title: String + @State public var type: ShortcutType + @Binding public var confictAlterMessage: ShortcutConfictAlertMessage + + var body: some View { + HStack { + Text(LocalizedStringKey(title)) + Spacer() + KeyHolderWrapper(shortcutType: type, confictAlterMessage: $confictAlterMessage).frame(width: 180, height: 26) + } + } +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralKeyHolderWrapper.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift similarity index 62% rename from Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralKeyHolderWrapper.swift rename to Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift index e94dfda4e..80ef418be 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/GeneralKeyHolderWrapper.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift @@ -1,5 +1,5 @@ // -// GeneralKeyHolderWrapper.swift +// KeyHolderWrapper.swift // Easydict // // Created by Sharker on 2024/1/2. @@ -12,7 +12,12 @@ import KeyHolder import Magnet import SwiftUI -struct GeneralKeyHolderWrapper: NSViewRepresentable { +public struct KeyHolderDataItem: Identifiable { + public var id: String { type.localizedStringKey() } + var type: ShortcutType +} + +struct KeyHolderWrapper: NSViewRepresentable { func makeCoordinator() -> Coordinator { .init(shortcutType: type, confictAlterMessage: $confictAlterMessage) } @@ -39,7 +44,7 @@ struct GeneralKeyHolderWrapper: NSViewRepresentable { func updateNSView(_: NSViewType, context _: Context) {} } -extension GeneralKeyHolderWrapper { +extension KeyHolderWrapper { class Coordinator: NSObject, RecordViewDelegate { private var type: ShortcutType @Binding var confictAlterMessage: ShortcutConfictAlertMessage @@ -91,6 +96,36 @@ extension GeneralKeyHolderWrapper { keyCombo = Defaults[.screenshotOCRShortcut] case .showMiniWindow: keyCombo = Defaults[.showMiniWindowShortcut] + case .clearInput: + keyCombo = Defaults[.clearInputShortcut] + case .clearAll: + keyCombo = Defaults[.clearAllShortcut] + case .copy: + keyCombo = Defaults[.copyShortcut] + case .copyFirstResult: + keyCombo = Defaults[.copyFirstResultShortcut] + case .focus: + keyCombo = Defaults[.focusShortcut] + case .play: + keyCombo = Defaults[.playShortcut] + case .retry: + keyCombo = Defaults[.retryShortcut] + case .toggle: + keyCombo = Defaults[.toggleShortcut] + case .pin: + keyCombo = Defaults[.pinShortcut] + case .hide: + keyCombo = Defaults[.hideShortcut] + case .increaseFontSize: + keyCombo = Defaults[.increaseFontSize] + case .decreaseFontSize: + keyCombo = Defaults[.decreaseFontSize] + case .google: + keyCombo = Defaults[.googleShortcut] + case .eudic: + keyCombo = Defaults[.eudicShortcut] + case .appleDic: + keyCombo = Defaults[.appleDictionaryShortcut] } recordView.keyCombo = keyCombo Shortcut.shared.bindingShortcut(keyCombo: keyCombo, type: type) @@ -109,6 +144,36 @@ extension GeneralKeyHolderWrapper { Defaults[.screenshotOCRShortcut] = keyCombo case .showMiniWindow: Defaults[.showMiniWindowShortcut] = keyCombo + case .clearInput: + Defaults[.clearInputShortcut] = keyCombo + case .clearAll: + Defaults[.clearAllShortcut] = keyCombo + case .copy: + Defaults[.copyShortcut] = keyCombo + case .copyFirstResult: + Defaults[.copyFirstResultShortcut] = keyCombo + case .focus: + Defaults[.focusShortcut] = keyCombo + case .play: + Defaults[.playShortcut] = keyCombo + case .retry: + Defaults[.retryShortcut] = keyCombo + case .toggle: + Defaults[.toggleShortcut] = keyCombo + case .pin: + Defaults[.pinShortcut] = keyCombo + case .hide: + Defaults[.hideShortcut] = keyCombo + case .increaseFontSize: + Defaults[.increaseFontSize] = keyCombo + case .decreaseFontSize: + Defaults[.decreaseFontSize] = keyCombo + case .google: + Defaults[.googleShortcut] = keyCombo + case .eudic: + Defaults[.eudicShortcut] = keyCombo + case .appleDic: + Defaults[.appleDictionaryShortcut] = keyCombo } } } From 213f092e96dd2c959a8f7c0889f542c39bdeb642 Mon Sep 17 00:00:00 2001 From: Sharker <1548742234@qq.com> Date: Fri, 9 Feb 2024 09:29:54 +0800 Subject: [PATCH 59/72] fix: shortcut bug (#394) * fix: shortcut bug https://github.com/tisfeng/Easydict/issues/391 * pref: optimize code * fix: upate menu crash * fix: fix review issues and remvoe unused localizable --- Easydict.xcodeproj/project.pbxproj | 4 ++++ Easydict/App/Localizable.xcstrings | 20 ------------------- .../Feature/Shortcut/Shortcut+Menu.swift | 20 +++++++++++++++++++ .../View/MenuView/MainMenuCommand.swift | 2 -- .../Tabs/View/Shortcut/KeyHolderWrapper.swift | 18 +++++++++-------- 5 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 Easydict/NewApp/Feature/Shortcut/Shortcut+Menu.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 4f0d47908..c120c7813 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -283,6 +283,7 @@ 9643D9462B71D103000FBEA6 /* KeyHolderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */; }; 9643D94A2B71EABE000FBEA6 /* KeyHolderAlterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */; }; 9643D94C2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */; }; + 9643D9562B73B3CD000FBEA6 /* Shortcut+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */; }; 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */; }; 967712EA2B5B913600105E0F /* KeyHolder in Frameworks */ = {isa = PBXBuildFile; productRef = 967712E92B5B913600105E0F /* KeyHolder */; }; 967712EE2B5B943400105E0F /* Shortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967712ED2B5B943400105E0F /* Shortcut.swift */; }; @@ -806,6 +807,7 @@ 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderRowView.swift; sourceTree = ""; }; 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderAlterView.swift; sourceTree = ""; }; 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommandItem.swift; sourceTree = ""; }; + 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Menu.swift"; sourceTree = ""; }; 9672D7D02B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MASShortcutBinder+EZMASShortcutBinder.h"; sourceTree = ""; }; 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutBinder+EZMASShortcutBinder.m"; sourceTree = ""; }; 967712ED2B5B943400105E0F /* Shortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shortcut.swift; sourceTree = ""; }; @@ -2273,6 +2275,7 @@ 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */, 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */, 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */, + 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */, ); path = Shortcut; sourceTree = ""; @@ -3031,6 +3034,7 @@ 03F639952AA6CFBB009B9914 /* EZBingConfig.m in Sources */, 0AC8A83F2B689E68006DA5CC /* ServiceSecretConfigreValidatable.swift in Sources */, 03D2A3E329F4C6F50035CED4 /* EZNetworkManager.m in Sources */, + 9643D9562B73B3CD000FBEA6 /* Shortcut+Menu.swift in Sources */, 0309E1ED292B439A00AFB76A /* EZTextView.m in Sources */, 03B0232B29231FA6001C7E63 /* NSMutableAttributedString+MM.m in Sources */, 03B022E829231FA6001C7E63 /* entry.m in Sources */, diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index fe1356419..db79188c4 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -3648,16 +3648,6 @@ } } }, - "shortcut_confict_message (Shortcut.shared.confictMenuItem?.title ?? \"\")" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "" - } - } - } - }, "shortcut_confict_message %@" : { "extractionState" : "manual", "localizations" : { @@ -3675,16 +3665,6 @@ } } }, - "shortcut_confict_title (keyCombo!.keyEquivalentModifierMaskString + keyCombo!.characters)" : { - "localizations" : { - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "" - } - } - } - }, "shortcut_confict_title %@" : { "extractionState" : "manual", "localizations" : { diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Menu.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Menu.swift new file mode 100644 index 000000000..04f109aa6 --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Menu.swift @@ -0,0 +1,20 @@ +// +// Shortcut+Menu.swift +// Easydict +// +// Created by Sharker on 2024/2/7. +// Copyright © 2024 izual. All rights reserved. +// + +import SwiftUI + +extension Shortcut { + @available(macOS 13, *) + func updateMenu(_ type: ShortcutType) { // update shortcut menu + let shortcutTitle = String(localized: LocalizedStringResource(stringLiteral: type.localizedStringKey())) + let menuTitle = String(localized: LocalizedStringResource(stringLiteral: "shortcut")) + let shortcutMenu = NSApp.mainMenu?.items.first(where: { $0.title == menuTitle }) + let clearInput = shortcutMenu?.submenu?.items.first(where: { $0.title == shortcutTitle }) + clearInput?.keyEquivalent = "" + } +} diff --git a/Easydict/NewApp/View/MenuView/MainMenuCommand.swift b/Easydict/NewApp/View/MenuView/MainMenuCommand.swift index 84231b7bf..388caacd3 100644 --- a/Easydict/NewApp/View/MenuView/MainMenuCommand.swift +++ b/Easydict/NewApp/View/MenuView/MainMenuCommand.swift @@ -13,6 +13,4 @@ struct EasyDictMainMenu: Commands { // shortcut MainMenuShortcutCommand() } - - func showAPIWebSite() {} } diff --git a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift index 80ef418be..b366864cc 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/View/Shortcut/KeyHolderWrapper.swift @@ -17,6 +17,7 @@ public struct KeyHolderDataItem: Identifiable { var type: ShortcutType } +@available(macOS 13, *) struct KeyHolderWrapper: NSViewRepresentable { func makeCoordinator() -> Coordinator { .init(shortcutType: type, confictAlterMessage: $confictAlterMessage) @@ -44,6 +45,7 @@ struct KeyHolderWrapper: NSViewRepresentable { func updateNSView(_: NSViewType, context _: Context) {} } +@available(macOS 13, *) extension KeyHolderWrapper { class Coordinator: NSObject, RecordViewDelegate { private var type: ShortcutType @@ -67,17 +69,17 @@ extension KeyHolderWrapper { if let key = keyCombo { // shortcut validate confict if Shortcut.validateShortcut(key) { - if #available(macOS 12, *) { - confictAlterMessage = ShortcutConfictAlertMessage(title: String(localized: "shortcut_confict_title \(keyCombo!.keyEquivalentModifierMaskString + keyCombo!.characters)"), message: String(localized: "shortcut_confict_message \(Shortcut.shared.confictMenuItem?.title ?? "")")) - } else { - // Fallback on earlier versions - let title = NSLocalizedString("shortcut_confict_title \(keyCombo!.keyEquivalentModifierMaskString + keyCombo!.characters)", comment: "") - let msg = NSLocalizedString("shortcut_confict_message \(Shortcut.shared.confictMenuItem?.title ?? "")", comment: "") - confictAlterMessage = ShortcutConfictAlertMessage(title: title, message: msg) - } + let title = String(localized: "shortcut_confict_title \(key.keyEquivalentModifierMaskString + key.characters)") + let message = String(localized: "shortcut_confict_message \(Shortcut.shared.confictMenuItem?.title ?? "")") + confictAlterMessage = ShortcutConfictAlertMessage( + title: title, + message: message + ) recordView.clear() return } + } else { // clear shortcut + Shortcut.shared.updateMenu(type) } storeKeyCombo(with: keyCombo) Shortcut.shared.bindingShortcut(keyCombo: keyCombo, type: type) From adee5f71e2dc43f2e21c9867d18416191a49ca44 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:07:23 +0800 Subject: [PATCH 60/72] fix: stream service like OpenAI trigger multi Alert issue (#393) * fix: stream service like OpenAI trigger multi Alert issue * fix: stream validation issue * perf: improve validation request case --------- Co-authored-by: tisfeng --- .../Protocol/ServiceSecretConfigreValidatable.swift | 8 +++++++- .../ServiceConfigurationSecretSectionView.swift | 13 ++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift index 89f3f21a5..1a8f94f7e 100644 --- a/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift +++ b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift @@ -19,6 +19,12 @@ extension ServiceSecretConfigreValidatable { extension QueryService: ServiceSecretConfigreValidatable { func validate(completion: @escaping (EZQueryResult, Error?) -> Void) { resetServiceResult() - translate("hello world!", from: .english, to: .simplifiedChinese, completion: completion) + /** + To reduce output text, save cost, a simple translation example is enough. + + 1. use zh -> en to avoid analyze English sentence. + 2. if Chinese text length > 5, it won't query dict. + */ + translate("曾经沧海难为水", from: .simplifiedChinese, to: .english, completion: completion) } } diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift index 7586cfdf7..7827063b3 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceConfiguration/ServiceConfigurationSecretSectionView.swift @@ -61,7 +61,7 @@ struct ServiceConfigurationSecretSectionView: View { } .alert(viewModel.alertMessage, isPresented: $viewModel.isAlertPresented, actions: { Button("ok") { - viewModel.isAlertPresented = false + viewModel.reset() } }) } @@ -70,10 +70,11 @@ struct ServiceConfigurationSecretSectionView: View { viewModel.isValidating.toggle() service.validate { _, error in DispatchQueue.main.async { + guard viewModel.isValidating else { return } + viewModel.isValidating = false viewModel.alertMessage = error == nil ? "service.configuration.validation_success" : "service.configuration.validation_fail" print("\(service.serviceType()) validate \(error == nil ? "success" : "fail")!") - viewModel.isValidating.toggle() - viewModel.isAlertPresented.toggle() + viewModel.isAlertPresented = true } } } @@ -103,6 +104,12 @@ private class ServiceValidationViewModel: ObservableObject { } ) } + + func reset() { + isValidating = false + alertMessage = "" + isAlertPresented = false + } } @available(macOS 13.0, *) From 53aca1014e6f45784be6b4c53b50ecc43630a4ee Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 9 Feb 2024 10:15:33 +0800 Subject: [PATCH 61/72] chore: update app version to 2.6.0 --- Easydict.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index c120c7813..a56b655cb 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -3206,7 +3206,7 @@ CODE_SIGN_IDENTITY = $CODE_SIGN_IDENTITY; CODE_SIGN_STYLE = $CODE_SIGN_STYLE; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; @@ -3217,7 +3217,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.6.0; PRODUCT_BUNDLE_IDENTIFIER = "com.izual.EasydictHelper-debug"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3232,7 +3232,7 @@ CODE_SIGN_IDENTITY = $CODE_SIGN_IDENTITY; CODE_SIGN_STYLE = $CODE_SIGN_STYLE; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; @@ -3243,7 +3243,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.6.0; PRODUCT_BUNDLE_IDENTIFIER = com.izual.EasydictHelper; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3389,7 +3389,7 @@ CODE_SIGN_STYLE = $CODE_SIGN_STYLE; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; @@ -3404,7 +3404,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.6.0; PRODUCT_BUNDLE_IDENTIFIER = "com.izual.Easydict-debug"; PRODUCT_MODULE_NAME = Easydict; PRODUCT_NAME = "Easydict-debug"; @@ -3429,7 +3429,7 @@ CODE_SIGN_STYLE = $CODE_SIGN_STYLE; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 32; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; ENABLE_HARDENED_RUNTIME = YES; @@ -3444,7 +3444,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.6.0; PRODUCT_BUNDLE_IDENTIFIER = com.izual.Easydict; PRODUCT_MODULE_NAME = Easydict; PRODUCT_NAME = "$(TARGET_NAME)"; From 3c1a5dbde6be0676e349e87d9e348a90987c3ea5 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 9 Feb 2024 13:51:35 +0800 Subject: [PATCH 62/72] chore: update appcast.xml --- appcast.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/appcast.xml b/appcast.xml index 503f014d6..7489ebc58 100755 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,16 @@ Easydict + + 2.6.0 + Fri, 09 Feb 2024 12:16:49 +0800 + 32 + 2.6.0 + 11.0 + https://github.com/tisfeng/easydict/releases/tag/2.6.0 + + + 2.5.0 Fri, 05 Jan 2024 00:11:27 +0800 From 4f24a130f9ca921e8d087ea79b1e768241ea4fe1 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 9 Feb 2024 14:00:09 +0800 Subject: [PATCH 63/72] chore: update release sign DEVELOPMENT_TEAM --- Easydict.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict.xcconfig b/Easydict.xcconfig index 7913bef76..baa1af95d 100644 --- a/Easydict.xcconfig +++ b/Easydict.xcconfig @@ -1,3 +1,3 @@ -DEVELOPMENT_TEAM = 79NQA2XYHM +DEVELOPMENT_TEAM = 45Z6V4YD5U CODE_SIGN_IDENTITY = Apple Development CODE_SIGN_STYLE = Automatic From 0de18aeec9652cb4baa649944bb4fd228ff61a1d Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 9 Feb 2024 14:56:23 +0800 Subject: [PATCH 64/72] style: auto format --- .../Utility/Protocol/ServiceSecretConfigreValidatable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift index 1a8f94f7e..39f7e9adc 100644 --- a/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift +++ b/Easydict/NewApp/Utility/Protocol/ServiceSecretConfigreValidatable.swift @@ -21,7 +21,7 @@ extension QueryService: ServiceSecretConfigreValidatable { resetServiceResult() /** To reduce output text, save cost, a simple translation example is enough. - + 1. use zh -> en to avoid analyze English sentence. 2. if Chinese text length > 5, it won't query dict. */ From cba185dfa0037c9d8e874753cb1274d5fbe711b6 Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Thu, 8 Feb 2024 23:58:24 -0800 Subject: [PATCH 65/72] Implement global shortcuts default value in SwiftUI Mode (#395) * fix: default value in short cut * refactor: delete unused file --- Easydict.xcodeproj/project.pbxproj | 4 -- .../Configuration+Defaults.swift | 46 ++++++++----------- .../Feature/Shortcut/Shortcut+Default.swift | 31 ------------- .../NewApp/Feature/Shortcut/Shortcut.swift | 8 ---- 4 files changed, 20 insertions(+), 69 deletions(-) delete mode 100644 Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index a56b655cb..4c142dfe5 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -279,7 +279,6 @@ 9643D93D2B6F829C000FBEA6 /* MainMenuCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */; }; 9643D9402B6FC426000FBEA6 /* MainMenuShortcutCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */; }; 9643D9422B6FE4AF000FBEA6 /* Shortcut+Bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */; }; - 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */; }; 9643D9462B71D103000FBEA6 /* KeyHolderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */; }; 9643D94A2B71EABE000FBEA6 /* KeyHolderAlterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */; }; 9643D94C2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */; }; @@ -803,7 +802,6 @@ 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuCommand.swift; sourceTree = ""; }; 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommand.swift; sourceTree = ""; }; 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Bind.swift"; sourceTree = ""; }; - 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Default.swift"; sourceTree = ""; }; 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderRowView.swift; sourceTree = ""; }; 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderAlterView.swift; sourceTree = ""; }; 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommandItem.swift; sourceTree = ""; }; @@ -2272,7 +2270,6 @@ isa = PBXGroup; children = ( 967712ED2B5B943400105E0F /* Shortcut.swift */, - 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */, 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */, 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */, 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */, @@ -3003,7 +3000,6 @@ 03BDA7B92A26DA280079D04F /* NSProcessInfo+XPMArgumentParser.m in Sources */, 03542A4F2937B64B00C34C33 /* EZYoudaoOCRResponse.m in Sources */, 03B0233929231FA6001C7E63 /* MMTool.m in Sources */, - 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */, 03542A552937B7DE00C34C33 /* EZError.m in Sources */, 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */, 03BDA7B82A26DA280079D04F /* XPMValuedArgument.m in Sources */, diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index 240aed001..ea4839965 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -10,12 +10,6 @@ import Defaults import Foundation import Magnet -/// Utils -extension Defaults.Keys { - /// is first launch - static let firstLaunch = Key("EZConfiguration_kFirstLaunch", default: true) -} - // Setting extension Defaults.Keys { // rename `from` @@ -178,26 +172,26 @@ extension Defaults.Keys { /// shortcut extension Defaults.Keys { // Global - static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder") - static let snipShortcut = Key("EZSnipShortcutKey_keyHolder") - static let inputShortcut = Key("EZInputShortcutKey_keyHolder") - static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder") - static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder") + static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder", default: KeyCombo(key: .d, cocoaModifiers: .option)) + static let snipShortcut = Key("EZSnipShortcutKey_keyHolder", default: KeyCombo(key: .s, cocoaModifiers: .option)) + static let inputShortcut = Key("EZInputShortcutKey_keyHolder", default: KeyCombo(key: .a, cocoaModifiers: .option)) + static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder", default: KeyCombo(key: .f, cocoaModifiers: [.option, .shift])) + static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder", default: KeyCombo(key: .s, cocoaModifiers: .option)) // App - static let clearInputShortcut = Key("EZClearInputShortcutKey_keyHolder") - static let clearAllShortcut = Key("EZClearAllShortcutKey_keyHolder") - static let copyShortcut = Key("EZCopyShortcutKey_keyHolder") - static let copyFirstResultShortcut = Key("EZCopyFirstResultShortcutKey_keyHolder") - static let focusShortcut = Key("EZFocusShortcutKey_keyHolder") - static let playShortcut = Key("EZPlayShortcutKey_keyHolder") - static let retryShortcut = Key("EZRetryShortcutKey_keyHolder") - static let toggleShortcut = Key("EZToggleShortcutKey_keyHolder") - static let pinShortcut = Key("EZPinShortcutKey_keyHolder") - static let hideShortcut = Key("EZHideShortcutKey_keyHolder") - static let increaseFontSize = Key("EZIncreaseFontSizeShortcutKey_keyHolder") - static let decreaseFontSize = Key("EZDecreaseFontSizeShortcutKey_keyHolder") - static let googleShortcut = Key("EZGoogleShortcutKey_keyHolder") - static let eudicShortcut = Key("EZEudicShortcutKey_keyHolder") - static let appleDictionaryShortcut = Key("EZAppleDictionaryShortcutKey_keyHolder") + static let clearInputShortcut = Key("EZClearInputShortcutKey_keyHolder", default: KeyCombo(key: .k, cocoaModifiers: .command)) + static let clearAllShortcut = Key("EZClearAllShortcutKey_keyHolder", default: KeyCombo(key: .k, cocoaModifiers: [.command, .shift])) + static let copyShortcut = Key("EZCopyShortcutKey_keyHolder", default: KeyCombo(key: .c, cocoaModifiers: [.command, .shift])) + static let copyFirstResultShortcut = Key("EZCopyFirstResultShortcutKey_keyHolder", default: KeyCombo(key: .j, cocoaModifiers: [.command, .shift])) + static let focusShortcut = Key("EZFocusShortcutKey_keyHolder", default: KeyCombo(key: .i, cocoaModifiers: .command)) + static let playShortcut = Key("EZPlayShortcutKey_keyHolder", default: KeyCombo(key: .s, cocoaModifiers: .command)) + static let retryShortcut = Key("EZRetryShortcutKey_keyHolder", default: KeyCombo(key: .r, cocoaModifiers: .command)) + static let toggleShortcut = Key("EZToggleShortcutKey_keyHolder", default: KeyCombo(key: .t, cocoaModifiers: .command)) + static let pinShortcut = Key("EZPinShortcutKey_keyHolder", default: KeyCombo(key: .p, cocoaModifiers: .command)) + static let hideShortcut = Key("EZHideShortcutKey_keyHolder", default: KeyCombo(key: .y, cocoaModifiers: .command)) + static let increaseFontSize = Key("EZIncreaseFontSizeShortcutKey_keyHolder", default: KeyCombo(key: .keypadPlus, cocoaModifiers: .command)) + static let decreaseFontSize = Key("EZDecreaseFontSizeShortcutKey_keyHolder", default: KeyCombo(key: .keypadMinus, cocoaModifiers: .command)) + static let googleShortcut = Key("EZGoogleShortcutKey_keyHolder", default: KeyCombo(key: .return, cocoaModifiers: .command)) + static let eudicShortcut = Key("EZEudicShortcutKey_keyHolder", default: KeyCombo(key: .return, cocoaModifiers: [.command, .shift])) + static let appleDictionaryShortcut = Key("EZAppleDictionaryShortcutKey_keyHolder", default: KeyCombo(key: .d, cocoaModifiers: [.command, .shift])) } diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift deleted file mode 100644 index d48d74d1d..000000000 --- a/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Shortcut+Default.swift -// Easydict -// -// Created by Sharker on 2024/2/5. -// Copyright © 2024 izual. All rights reserved. -// - -import Defaults -import Magnet - -extension Shortcut { - // set defalut for app shortcut - func setDefaultForAppShortcut() { - Defaults[.clearInputShortcut] = KeyCombo(key: .k, cocoaModifiers: .command) - Defaults[.clearAllShortcut] = KeyCombo(key: .k, cocoaModifiers: [.command, .shift]) - Defaults[.copyShortcut] = KeyCombo(key: .c, cocoaModifiers: [.command, .shift]) - Defaults[.copyFirstResultShortcut] = KeyCombo(key: .j, cocoaModifiers: [.command, .shift]) - Defaults[.focusShortcut] = KeyCombo(key: .i, cocoaModifiers: .command) - Defaults[.playShortcut] = KeyCombo(key: .s, cocoaModifiers: .command) - Defaults[.retryShortcut] = KeyCombo(key: .r, cocoaModifiers: .command) - Defaults[.toggleShortcut] = KeyCombo(key: .t, cocoaModifiers: .command) - Defaults[.pinShortcut] = KeyCombo(key: .p, cocoaModifiers: .command) - Defaults[.hideShortcut] = KeyCombo(key: .y, cocoaModifiers: .command) - Defaults[.increaseFontSize] = KeyCombo(key: .keypadPlus, cocoaModifiers: .command) - Defaults[.decreaseFontSize] = KeyCombo(key: .keypadMinus, cocoaModifiers: .command) - Defaults[.googleShortcut] = KeyCombo(key: .return, cocoaModifiers: .command) - Defaults[.eudicShortcut] = KeyCombo(key: .return, cocoaModifiers: [.command, .shift]) - Defaults[.appleDictionaryShortcut] = KeyCombo(key: .d, cocoaModifiers: [.command, .shift]) - } -} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift index c08c2dc2a..d4be0b625 100644 --- a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift @@ -99,14 +99,6 @@ class Shortcut: NSObject { @objc static func setupShortcut() { let shortcut = Shortcut.shared shortcut.restoreShortcut() - - if Defaults[.firstLaunch] { - Defaults[.firstLaunch] = false - // set defalut for app shortcut - shortcut.setDefaultForAppShortcut() - } else { - // do nothing - } } // Make sure the class has only one instance From 1af77e58e620aed104b1a3e088efcbbfc912bde9 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Fri, 9 Feb 2024 16:02:12 +0800 Subject: [PATCH 66/72] chore: update appcast.xml --- appcast.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appcast.xml b/appcast.xml index 7489ebc58..3921ef52b 100755 --- a/appcast.xml +++ b/appcast.xml @@ -5,12 +5,12 @@ 2.6.0 - Fri, 09 Feb 2024 12:16:49 +0800 + Fri, 09 Feb 2024 15:42:20 +0800 32 2.6.0 11.0 https://github.com/tisfeng/easydict/releases/tag/2.6.0 - + From 033e391917820787a3108b1a01cdf291be0c197f Mon Sep 17 00:00:00 2001 From: tisfeng Date: Sat, 10 Feb 2024 18:00:30 +0800 Subject: [PATCH 67/72] chore: update issue templates --- .github/ISSUE_TEMPLATE/cn_bug_report_zh.yml | 4 +++- .github/ISSUE_TEMPLATE/en_bug_report.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml index c2d0f068a..e3de84e43 100644 --- a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml +++ b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml @@ -1,6 +1,6 @@ name: 反馈问题 description: 反馈问题 -title: "🐞 反馈问题:{{请填写标题,不要留空}}" +title: "🐞 反馈问题:请填写标题,不要留空" labels: ["bug"] body: @@ -9,6 +9,8 @@ body: attributes: label: 请先确认以下事项: options: + - label: 请务必查看 [常见问题](https://github.com/tisfeng/Easydict/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) + required: true - label: 已仔细阅读了 [README](https://github.com/tisfeng/Easydict#readme) required: true - label: 在 [issues](https://github.com/tisfeng/Easydict/issues) 页面搜索过问题(包括已关闭的 issue),但未能找到解决方法 diff --git a/.github/ISSUE_TEMPLATE/en_bug_report.yml b/.github/ISSUE_TEMPLATE/en_bug_report.yml index 2f87973a8..5b51b73a6 100644 --- a/.github/ISSUE_TEMPLATE/en_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/en_bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Report an issue -title: "🐞 Bug Report: {{Please fill in the title, don't leave it blank}} " +title: "🐞 Bug Report: Please fill in the title, don't leave it blank" labels: ["bug"] body: @@ -9,6 +9,8 @@ body: attributes: label: "Please confirm the following:" options: + - label: Be sure to check out [FAQ](https://github.com/tisfeng/Easydict/wiki/FAQ) + required: true - label: I have carefully read the [README](https://github.com/tisfeng/Easydict#readme) required: true - label: I have searched through the [issues](https://github.com/tisfeng/Easydict/issues) page but couldn't find a solution.(Including issue that has been closed) From 319fc48cbbc460bdbde87ad398e66d452cddcbb4 Mon Sep 17 00:00:00 2001 From: Lava <34743145+CanglongCl@users.noreply.github.com> Date: Mon, 12 Feb 2024 23:50:38 -0800 Subject: [PATCH 68/72] Revert #395 and implement with alter method (#398) * fix: default value in short cut * refactor: delete unused file * Revert "refactor: delete unused file" This reverts commit 3a2d7344f2733af6f1b0b3fc336260a85858b205. * Revert "fix: default value in short cut" This reverts commit fdfe49574f9b8e5f9d88b35a0dae655268a2360e. * Add default shortcut * split `setDefaultForAppShortcut` into 2 method * fix: default value in short cut * refactor: delete unused file * Revert "refactor: delete unused file" This reverts commit 3a2d7344f2733af6f1b0b3fc336260a85858b205. * Revert "fix: default value in short cut" This reverts commit fdfe49574f9b8e5f9d88b35a0dae655268a2360e. * Add default shortcut * split `setDefaultForAppShortcut` into 2 method * Revert "chore: update issue templates" This reverts commit 033e391917820787a3108b1a01cdf291be0c197f. * fix: correct default global shortcut keys --------- Co-authored-by: tisfeng --- .github/ISSUE_TEMPLATE/cn_bug_report_zh.yml | 4 +- .github/ISSUE_TEMPLATE/en_bug_report.yml | 4 +- Easydict.xcodeproj/project.pbxproj | 4 ++ .../Configuration+Defaults.swift | 46 +++++++++++-------- .../Feature/Shortcut/Shortcut+Default.swift | 46 +++++++++++++++++++ .../NewApp/Feature/Shortcut/Shortcut.swift | 8 ++++ 6 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift diff --git a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml index e3de84e43..c2d0f068a 100644 --- a/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml +++ b/.github/ISSUE_TEMPLATE/cn_bug_report_zh.yml @@ -1,6 +1,6 @@ name: 反馈问题 description: 反馈问题 -title: "🐞 反馈问题:请填写标题,不要留空" +title: "🐞 反馈问题:{{请填写标题,不要留空}}" labels: ["bug"] body: @@ -9,8 +9,6 @@ body: attributes: label: 请先确认以下事项: options: - - label: 请务必查看 [常见问题](https://github.com/tisfeng/Easydict/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) - required: true - label: 已仔细阅读了 [README](https://github.com/tisfeng/Easydict#readme) required: true - label: 在 [issues](https://github.com/tisfeng/Easydict/issues) 页面搜索过问题(包括已关闭的 issue),但未能找到解决方法 diff --git a/.github/ISSUE_TEMPLATE/en_bug_report.yml b/.github/ISSUE_TEMPLATE/en_bug_report.yml index 5b51b73a6..2f87973a8 100644 --- a/.github/ISSUE_TEMPLATE/en_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/en_bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Report an issue -title: "🐞 Bug Report: Please fill in the title, don't leave it blank" +title: "🐞 Bug Report: {{Please fill in the title, don't leave it blank}} " labels: ["bug"] body: @@ -9,8 +9,6 @@ body: attributes: label: "Please confirm the following:" options: - - label: Be sure to check out [FAQ](https://github.com/tisfeng/Easydict/wiki/FAQ) - required: true - label: I have carefully read the [README](https://github.com/tisfeng/Easydict#readme) required: true - label: I have searched through the [issues](https://github.com/tisfeng/Easydict/issues) page but couldn't find a solution.(Including issue that has been closed) diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 4c142dfe5..a56b655cb 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -279,6 +279,7 @@ 9643D93D2B6F829C000FBEA6 /* MainMenuCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */; }; 9643D9402B6FC426000FBEA6 /* MainMenuShortcutCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */; }; 9643D9422B6FE4AF000FBEA6 /* Shortcut+Bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */; }; + 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */; }; 9643D9462B71D103000FBEA6 /* KeyHolderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */; }; 9643D94A2B71EABE000FBEA6 /* KeyHolderAlterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */; }; 9643D94C2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */; }; @@ -802,6 +803,7 @@ 9643D93C2B6F829C000FBEA6 /* MainMenuCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuCommand.swift; sourceTree = ""; }; 9643D93F2B6FC426000FBEA6 /* MainMenuShortcutCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommand.swift; sourceTree = ""; }; 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Bind.swift"; sourceTree = ""; }; + 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Shortcut+Default.swift"; sourceTree = ""; }; 9643D9452B71D103000FBEA6 /* KeyHolderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderRowView.swift; sourceTree = ""; }; 9643D9492B71EABE000FBEA6 /* KeyHolderAlterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHolderAlterView.swift; sourceTree = ""; }; 9643D94B2B71F74D000FBEA6 /* MainMenuShortcutCommandItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuShortcutCommandItem.swift; sourceTree = ""; }; @@ -2270,6 +2272,7 @@ isa = PBXGroup; children = ( 967712ED2B5B943400105E0F /* Shortcut.swift */, + 9643D9432B6FEF5F000FBEA6 /* Shortcut+Default.swift */, 9643D9412B6FE4AF000FBEA6 /* Shortcut+Bind.swift */, 9608354F2B6791F200C6A931 /* Shortcut+Validator.swift */, 9643D9552B73B3CD000FBEA6 /* Shortcut+Menu.swift */, @@ -3000,6 +3003,7 @@ 03BDA7B92A26DA280079D04F /* NSProcessInfo+XPMArgumentParser.m in Sources */, 03542A4F2937B64B00C34C33 /* EZYoudaoOCRResponse.m in Sources */, 03B0233929231FA6001C7E63 /* MMTool.m in Sources */, + 9643D9442B6FEF5F000FBEA6 /* Shortcut+Default.swift in Sources */, 03542A552937B7DE00C34C33 /* EZError.m in Sources */, 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */, 03BDA7B82A26DA280079D04F /* XPMValuedArgument.m in Sources */, diff --git a/Easydict/NewApp/Configuration/Configuration+Defaults.swift b/Easydict/NewApp/Configuration/Configuration+Defaults.swift index ea4839965..240aed001 100644 --- a/Easydict/NewApp/Configuration/Configuration+Defaults.swift +++ b/Easydict/NewApp/Configuration/Configuration+Defaults.swift @@ -10,6 +10,12 @@ import Defaults import Foundation import Magnet +/// Utils +extension Defaults.Keys { + /// is first launch + static let firstLaunch = Key("EZConfiguration_kFirstLaunch", default: true) +} + // Setting extension Defaults.Keys { // rename `from` @@ -172,26 +178,26 @@ extension Defaults.Keys { /// shortcut extension Defaults.Keys { // Global - static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder", default: KeyCombo(key: .d, cocoaModifiers: .option)) - static let snipShortcut = Key("EZSnipShortcutKey_keyHolder", default: KeyCombo(key: .s, cocoaModifiers: .option)) - static let inputShortcut = Key("EZInputShortcutKey_keyHolder", default: KeyCombo(key: .a, cocoaModifiers: .option)) - static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder", default: KeyCombo(key: .f, cocoaModifiers: [.option, .shift])) - static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder", default: KeyCombo(key: .s, cocoaModifiers: .option)) + static let selectionShortcut = Key("EZSelectionShortcutKey_keyHolder") + static let snipShortcut = Key("EZSnipShortcutKey_keyHolder") + static let inputShortcut = Key("EZInputShortcutKey_keyHolder") + static let screenshotOCRShortcut = Key("EZScreenshotOCRShortcutKey_keyHolder") + static let showMiniWindowShortcut = Key("EZShowMiniShortcutKey_keyHolder") // App - static let clearInputShortcut = Key("EZClearInputShortcutKey_keyHolder", default: KeyCombo(key: .k, cocoaModifiers: .command)) - static let clearAllShortcut = Key("EZClearAllShortcutKey_keyHolder", default: KeyCombo(key: .k, cocoaModifiers: [.command, .shift])) - static let copyShortcut = Key("EZCopyShortcutKey_keyHolder", default: KeyCombo(key: .c, cocoaModifiers: [.command, .shift])) - static let copyFirstResultShortcut = Key("EZCopyFirstResultShortcutKey_keyHolder", default: KeyCombo(key: .j, cocoaModifiers: [.command, .shift])) - static let focusShortcut = Key("EZFocusShortcutKey_keyHolder", default: KeyCombo(key: .i, cocoaModifiers: .command)) - static let playShortcut = Key("EZPlayShortcutKey_keyHolder", default: KeyCombo(key: .s, cocoaModifiers: .command)) - static let retryShortcut = Key("EZRetryShortcutKey_keyHolder", default: KeyCombo(key: .r, cocoaModifiers: .command)) - static let toggleShortcut = Key("EZToggleShortcutKey_keyHolder", default: KeyCombo(key: .t, cocoaModifiers: .command)) - static let pinShortcut = Key("EZPinShortcutKey_keyHolder", default: KeyCombo(key: .p, cocoaModifiers: .command)) - static let hideShortcut = Key("EZHideShortcutKey_keyHolder", default: KeyCombo(key: .y, cocoaModifiers: .command)) - static let increaseFontSize = Key("EZIncreaseFontSizeShortcutKey_keyHolder", default: KeyCombo(key: .keypadPlus, cocoaModifiers: .command)) - static let decreaseFontSize = Key("EZDecreaseFontSizeShortcutKey_keyHolder", default: KeyCombo(key: .keypadMinus, cocoaModifiers: .command)) - static let googleShortcut = Key("EZGoogleShortcutKey_keyHolder", default: KeyCombo(key: .return, cocoaModifiers: .command)) - static let eudicShortcut = Key("EZEudicShortcutKey_keyHolder", default: KeyCombo(key: .return, cocoaModifiers: [.command, .shift])) - static let appleDictionaryShortcut = Key("EZAppleDictionaryShortcutKey_keyHolder", default: KeyCombo(key: .d, cocoaModifiers: [.command, .shift])) + static let clearInputShortcut = Key("EZClearInputShortcutKey_keyHolder") + static let clearAllShortcut = Key("EZClearAllShortcutKey_keyHolder") + static let copyShortcut = Key("EZCopyShortcutKey_keyHolder") + static let copyFirstResultShortcut = Key("EZCopyFirstResultShortcutKey_keyHolder") + static let focusShortcut = Key("EZFocusShortcutKey_keyHolder") + static let playShortcut = Key("EZPlayShortcutKey_keyHolder") + static let retryShortcut = Key("EZRetryShortcutKey_keyHolder") + static let toggleShortcut = Key("EZToggleShortcutKey_keyHolder") + static let pinShortcut = Key("EZPinShortcutKey_keyHolder") + static let hideShortcut = Key("EZHideShortcutKey_keyHolder") + static let increaseFontSize = Key("EZIncreaseFontSizeShortcutKey_keyHolder") + static let decreaseFontSize = Key("EZDecreaseFontSizeShortcutKey_keyHolder") + static let googleShortcut = Key("EZGoogleShortcutKey_keyHolder") + static let eudicShortcut = Key("EZEudicShortcutKey_keyHolder") + static let appleDictionaryShortcut = Key("EZAppleDictionaryShortcutKey_keyHolder") } diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift new file mode 100644 index 000000000..685cfa026 --- /dev/null +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut+Default.swift @@ -0,0 +1,46 @@ +// +// Shortcut+Default.swift +// Easydict +// +// Created by Sharker on 2024/2/5. +// Copyright © 2024 izual. All rights reserved. +// + +import Defaults +import Magnet + +extension Shortcut { + // set defalut for app shortcut + func setDefaultForShortcut() { + setDefaultForGlobalShortcut() + setDefaultForAppShortcut() + } + + private func setDefaultForGlobalShortcut() { + Defaults[.inputShortcut] = KeyCombo(key: .a, cocoaModifiers: .option) + Defaults[.snipShortcut] = KeyCombo(key: .s, cocoaModifiers: .option) + Defaults[.selectionShortcut] = KeyCombo(key: .d, cocoaModifiers: .option) + Defaults[.showMiniWindowShortcut] = KeyCombo(key: .f, cocoaModifiers: .option) + Defaults[.screenshotOCRShortcut] = KeyCombo(key: .s, cocoaModifiers: [.option, .shift]) + } + + private func setDefaultForAppShortcut() { + setDefaultForGlobalShortcut() + + Defaults[.clearInputShortcut] = KeyCombo(key: .k, cocoaModifiers: .command) + Defaults[.clearAllShortcut] = KeyCombo(key: .k, cocoaModifiers: [.command, .shift]) + Defaults[.copyShortcut] = KeyCombo(key: .c, cocoaModifiers: [.command, .shift]) + Defaults[.copyFirstResultShortcut] = KeyCombo(key: .j, cocoaModifiers: [.command, .shift]) + Defaults[.focusShortcut] = KeyCombo(key: .i, cocoaModifiers: .command) + Defaults[.playShortcut] = KeyCombo(key: .s, cocoaModifiers: .command) + Defaults[.retryShortcut] = KeyCombo(key: .r, cocoaModifiers: .command) + Defaults[.toggleShortcut] = KeyCombo(key: .t, cocoaModifiers: .command) + Defaults[.pinShortcut] = KeyCombo(key: .p, cocoaModifiers: .command) + Defaults[.hideShortcut] = KeyCombo(key: .y, cocoaModifiers: .command) + Defaults[.increaseFontSize] = KeyCombo(key: .keypadPlus, cocoaModifiers: .command) + Defaults[.decreaseFontSize] = KeyCombo(key: .keypadMinus, cocoaModifiers: .command) + Defaults[.googleShortcut] = KeyCombo(key: .return, cocoaModifiers: .command) + Defaults[.eudicShortcut] = KeyCombo(key: .return, cocoaModifiers: [.command, .shift]) + Defaults[.appleDictionaryShortcut] = KeyCombo(key: .d, cocoaModifiers: [.command, .shift]) + } +} diff --git a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift index d4be0b625..78b54d58f 100644 --- a/Easydict/NewApp/Feature/Shortcut/Shortcut.swift +++ b/Easydict/NewApp/Feature/Shortcut/Shortcut.swift @@ -99,6 +99,14 @@ class Shortcut: NSObject { @objc static func setupShortcut() { let shortcut = Shortcut.shared shortcut.restoreShortcut() + + if Defaults[.firstLaunch] { + Defaults[.firstLaunch] = false + // set defalut for app shortcut + shortcut.setDefaultForShortcut() + } else { + // do nothing + } } // Make sure the class has only one instance From 34f75ff02079ec2dd5a670fd1a8b7657a28b7638 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Tue, 13 Feb 2024 21:15:09 +0800 Subject: [PATCH 69/72] perf: do not clear result if text length is 0, means @"" --- .../Window/WindowManager/EZWindowManager.m | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m index 1b098b8d5..7eb580313 100644 --- a/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m +++ b/Easydict/Feature/ViewController/Window/WindowManager/EZWindowManager.m @@ -691,9 +691,13 @@ - (void)selectTextTranslate { [self.eventMonitor getSelectedText:^(NSString *_Nullable text) { self.actionType = self.eventMonitor.actionType; - // Clear query if text is nil and user don't want to keep the last result. - if (!text && !Configuration.shared.keepPrevResultWhenEmpty) { - text = @""; + /** + Clear query if text is nil and user don't want to keep the last result. + + !!!: text may be @"" when no selected text in Chrome, so we need to handle it. + */ + if (text.length == 0) { + text = Configuration.shared.keepPrevResultWhenEmpty ? nil : @""; } self.selectedText = [text trim]; From 3535dd1d67ea21b40ebd86214cfc7a7cdb0157b0 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Tue, 13 Feb 2024 21:50:32 +0800 Subject: [PATCH 70/72] docs: update sponsor list --- README.md | 1 + README_EN.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 2e5b2fa26..0f311a085 100644 --- a/README.md +++ b/README.md @@ -827,6 +827,7 @@ Easydict 作为一个免费开源的非盈利项目,目前主要是作者个 | 2024-01-28 | ㅤ | 7 | | | 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。| | 2024-02-04 | ll | 20 | | +| 2024-02-10 | 盒子哥 | 100 | |

diff --git a/README_EN.md b/README_EN.md index ebf43222c..b841325fa 100644 --- a/README_EN.md +++ b/README_EN.md @@ -824,6 +824,7 @@ If you don't want your username to be displayed in the list, please choose anony | 2024-01-28 | ㅤ | 7 | | | 2024-01-29 | 大帅ㅤ | 5 | 还没有,但是感受到了用心。| | 2024-02-04 | ll | 20 | | +| 2024-02-10 | 盒子哥 | 100 | |

From 473200b27a585c4ab723e77376520e73e6a0dd80 Mon Sep 17 00:00:00 2001 From: tisfeng Date: Thu, 15 Feb 2024 09:45:04 +0800 Subject: [PATCH 71/72] chore: remove unuse github actions --- .github/workflows/objective-c-xcode.yml | 30 ------------------------- .github/workflows/swift.yml | 22 ------------------ 2 files changed, 52 deletions(-) delete mode 100644 .github/workflows/objective-c-xcode.yml delete mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/objective-c-xcode.yml b/.github/workflows/objective-c-xcode.yml deleted file mode 100644 index be710f7d4..000000000 --- a/.github/workflows/objective-c-xcode.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Xcode - Build and Analyze - -on: - push: - branches: [ "main", "dev" ] - pull_request: - branches: [ "main", "dev" ] - -jobs: - build: - name: Build and analyse default scheme using xcodebuild command - runs-on: macos-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set Default Scheme - run: | - scheme_list=$(xcodebuild -list -json | tr -d "\n") - default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") - echo $default | cat >default - echo Using default scheme: $default - - name: Build - env: - scheme: ${{ 'default' }} - run: | - if [ $scheme = default ]; then scheme=$(cat default); fi - if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi - file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` - xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" | xcpretty && exit ${PIPESTATUS[0]} diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml deleted file mode 100644 index b2ab57fbe..000000000 --- a/.github/workflows/swift.yml +++ /dev/null @@ -1,22 +0,0 @@ -# This workflow will build a Swift project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift - -name: Swift - -on: - push: - branches: [ "main", "dev" ] - pull_request: - branches: [ "main", "dev" ] - -jobs: - build: - - runs-on: macos-latest - - steps: - - uses: actions/checkout@v3 - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v From 1202c3f95be97208e41da6b042b763fac6643d2f Mon Sep 17 00:00:00 2001 From: tisfeng Date: Thu, 15 Feb 2024 10:30:32 +0800 Subject: [PATCH 72/72] chore: update codeql.yml, change image to macos-14.2 and xcode-version to 15.2 --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 047dd13b3..158c1d173 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-13') || 'ubuntu-latest' }} + runs-on: ${{ (matrix.language == 'swift' && 'macos-14.2') || 'ubuntu-latest' }} timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: actions: read @@ -48,7 +48,7 @@ jobs: if: matrix.language == 'swift' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.1' + xcode-version: '15.2' - name: Checkout repository uses: actions/checkout@v4