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) }