diff --git a/.gitignore b/.gitignore index 6900d6a..f60e8d5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ xcuserdata version.txt appcast_tmp.txt -# *.xcodeproj \ No newline at end of file +# *.xcodeproj +.cosine \ No newline at end of file diff --git a/liltr.xcodeproj/project.pbxproj b/liltr.xcodeproj/project.pbxproj index 20b2692..bdb7a24 100644 --- a/liltr.xcodeproj/project.pbxproj +++ b/liltr.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 2B0437D22B79D5D800792E8B /* AppleScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B0437D12B79D5D800792E8B /* AppleScript.swift */; }; + 2B0437D42B79E2F000792E8B /* Pasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B0437D32B79E2F000792E8B /* Pasteboard.swift */; }; + 2B0437D62B79E34900792E8B /* SelectedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B0437D52B79E34900792E8B /* SelectedText.swift */; }; 2B59A3902B75B92F005DB8B1 /* liltrApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B59A38F2B75B92F005DB8B1 /* liltrApp.swift */; }; 2B59A3972B75B930005DB8B1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B59A3962B75B930005DB8B1 /* Preview Assets.xcassets */; }; 2B59A3A02B75B9C7005DB8B1 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 2B59A39F2B75B9C7005DB8B1 /* KeyboardShortcuts */; }; @@ -56,6 +59,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 2B0437D12B79D5D800792E8B /* AppleScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleScript.swift; sourceTree = ""; }; + 2B0437D32B79E2F000792E8B /* Pasteboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pasteboard.swift; sourceTree = ""; }; + 2B0437D52B79E34900792E8B /* SelectedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedText.swift; sourceTree = ""; }; 2B59A38C2B75B92F005DB8B1 /* liltr.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = liltr.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2B59A38F2B75B92F005DB8B1 /* liltrApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = liltrApp.swift; sourceTree = ""; }; 2B59A3962B75B930005DB8B1 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -239,8 +245,11 @@ 2B7CF3062B75BCB400BFA0BA /* Common */ = { isa = PBXGroup; children = ( + 2B0437D12B79D5D800792E8B /* AppleScript.swift */, 2B7CF3072B75BCB400BFA0BA /* Common.swift */, 2B7CF3082B75BCB400BFA0BA /* Debouncer.swift */, + 2B0437D32B79E2F000792E8B /* Pasteboard.swift */, + 2B0437D52B79E34900792E8B /* SelectedText.swift */, ); path = Common; sourceTree = ""; @@ -326,6 +335,7 @@ 2B59A3882B75B92F005DB8B1 /* Sources */, 2B59A3892B75B92F005DB8B1 /* Frameworks */, 2B59A38A2B75B92F005DB8B1 /* Resources */, + 2B0437D72B79F3CA00792E8B /* ShellScript */, ); buildRules = ( ); @@ -391,6 +401,26 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 2B0437D72B79F3CA00792E8B /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 2B59A3882B75B92F005DB8B1 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -403,11 +433,13 @@ 2B7CF3262B75BCB400BFA0BA /* Debouncer.swift in Sources */, 2B7CF32B2B75BCB400BFA0BA /* TextHandler.swift in Sources */, 2B59A3A22B75BA07005DB8B1 /* userDefaults.swift in Sources */, + 2B0437D62B79E34900792E8B /* SelectedText.swift in Sources */, 2B7CF38E2B75C00400BFA0BA /* TranslateFieldView.swift in Sources */, 2B7CF31C2B75BCB400BFA0BA /* NiuTrans.swift in Sources */, 2B7CF31F2B75BCB400BFA0BA /* ProviderManager.swift in Sources */, 2B7CF31A2B75BCB400BFA0BA /* BigHugeThesaurus.swift in Sources */, 2B7CF3242B75BCB400BFA0BA /* extensions.swift in Sources */, + 2B0437D42B79E2F000792E8B /* Pasteboard.swift in Sources */, 2B7CF3282B75BCB400BFA0BA /* views.swift in Sources */, 2B7CF3772B75BFE400BFA0BA /* ToolbarItem.swift in Sources */, 2B7CF31B2B75BCB400BFA0BA /* Baidu.swift in Sources */, @@ -419,6 +451,7 @@ 2B7CF3902B75C00400BFA0BA /* MidBarView.swift in Sources */, 2B7CF3202B75BCB400BFA0BA /* Volcengine.swift in Sources */, 2B7CF37B2B75BFE400BFA0BA /* LanguagePicker.swift in Sources */, + 2B0437D22B79D5D800792E8B /* AppleScript.swift in Sources */, 2B7CF3832B75BFF000BFA0BA /* Hotkeys.swift in Sources */, 2B7CF3702B75BF2300BFA0BA /* TTTDictionary.m in Sources */, 2B7CF3162B75BCB400BFA0BA /* Encoder.swift in Sources */, diff --git a/liltr/Components/LanguagePicker.swift b/liltr/Components/LanguagePicker.swift index 5b4923f..3a25ff6 100644 --- a/liltr/Components/LanguagePicker.swift +++ b/liltr/Components/LanguagePicker.swift @@ -3,7 +3,7 @@ import SwiftUI struct LanguagePicker: View { @Binding var languageCode: String - + var withLabel: Bool var body: some View { diff --git a/liltr/Components/TabItem.swift b/liltr/Components/TabItem.swift index af463cd..46c665f 100644 --- a/liltr/Components/TabItem.swift +++ b/liltr/Components/TabItem.swift @@ -4,20 +4,20 @@ struct TabItem: View { private let label: String private let icon: String private let active: Bool - + private let itemWidth = 81 private let itemHeight = 50 private let sizeHolder: SizeHolder - + @State private var hovering: Bool = false - + init(label: String, icon: String, active: Bool, sizeBase: Float? = nil) { self.label = label self.icon = icon self.active = active self.sizeHolder = SizeHolder(base: sizeBase) } - + var body: some View { VStack { Image(systemName: icon) @@ -37,14 +37,14 @@ struct TabItem: View { self.hovering = hovering }) } - - private func getBackgroundColor()-> Color { + + private func getBackgroundColor() -> Color { let colorActive = Color.secondary.opacity(0.18) let colorHover = Color.secondary.opacity(0.1) let colorDefault = Color.clear - if (self.active) { + if self.active { return colorActive - } else if (self.hovering) { + } else if self.hovering { return colorHover } else { return colorDefault @@ -52,7 +52,6 @@ struct TabItem: View { } } - #Preview { HStack { TabItem(label: "General", icon: "gear", active: false) diff --git a/liltr/Components/ToolbarItem.swift b/liltr/Components/ToolbarItem.swift index c8e02f7..1b95611 100644 --- a/liltr/Components/ToolbarItem.swift +++ b/liltr/Components/ToolbarItem.swift @@ -3,9 +3,9 @@ import SwiftUI struct ToolbarItem: View { var systemName: String var action: () -> Void - + @State var hovering: Bool = false - + var body: some View { Button(action: action, label: { Image(systemName: systemName) diff --git a/liltr/Components/TopTabView.swift b/liltr/Components/TopTabView.swift index 71a6329..6648566 100644 --- a/liltr/Components/TopTabView.swift +++ b/liltr/Components/TopTabView.swift @@ -4,7 +4,7 @@ class TabPane: Identifiable { public let label: String public let icon: String public let view: AnyView - + init(label: String, icon: String, view: AnyView) { self.label = label self.icon = icon @@ -14,16 +14,16 @@ class TabPane: Identifiable { public struct TopTabView: View { private let tabPanes: [TabPane] - + private let sizeHolder = SizeHolder() - + @State private var activeTabLabel: String - + init(tabPanes: [TabPane], defaultActiveTabLabel: String? = nil) { self.tabPanes = tabPanes self.activeTabLabel = defaultActiveTabLabel ?? tabPanes.first!.label } - + public var tabBar: some View { HStack(spacing: CGFloat(sizeHolder.outerGapSize)) { Spacer() @@ -36,7 +36,7 @@ public struct TopTabView: View { Spacer() } } - + public var body: some View { ZStack(alignment: .top) { Color.clear diff --git a/liltr/Info.plist b/liltr/Info.plist index 9f989b3..0d3102b 100644 --- a/liltr/Info.plist +++ b/liltr/Info.plist @@ -2,6 +2,8 @@ + LSUIElement + APP_NAME liltr AliAK diff --git a/liltr/Settings/AboutView.swift b/liltr/Settings/AboutView.swift index 22d7d11..67842ff 100644 --- a/liltr/Settings/AboutView.swift +++ b/liltr/Settings/AboutView.swift @@ -14,28 +14,28 @@ struct AboutView: View { @ObservedObject private var _checkForUpdatesViewModel: CheckForUpdatesViewModel private let _gapSize: CGFloat = 8 private let _updater: SPUUpdater - + init(updater: SPUUpdater) { self._updater = updater self._checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) } - + var body: some View { VStack(alignment: .center) { Spacer() .frame(height: _gapSize * 2) - + HStack(alignment: .center) { Image(nsImage: NSImage(named: "AppIcon")!) .resizable() .frame(width: /*@START_MENU_TOKEN@*/100/*@END_MENU_TOKEN@*/, height: 100) - + VStack(alignment: .leading, spacing: 2) { Text("\(APP_NAME)") .font(.system(size: 18)) .fontWeight(/*@START_MENU_TOKEN@*/.bold/*@END_MENU_TOKEN@*/) .foregroundStyle(.primary) - + VStack(alignment: .leading) { Text("Version \(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)") .foregroundStyle(.primary) @@ -49,30 +49,31 @@ struct AboutView: View { } .frame(height: 80) } - + Divider() .padding(EdgeInsets(top: _gapSize, leading: 0, bottom: _gapSize, trailing: 0)) - + HStack { Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, label: { Text("Acknowledgements") }) - + Spacer() - + Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/, label: { Text("Visit Website") }) - + Button(action: { try! _updater.start() - _updater.checkForUpdates(); + _updater.checkForUpdates() }, label: { Text("Check Updates...") }) } .padding(EdgeInsets(top: 0, leading: _gapSize, bottom: 0, trailing: _gapSize)) - + .opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/) + } .frame(width: 450, height: 140) } diff --git a/liltr/Settings/GeneralView.swift b/liltr/Settings/GeneralView.swift index 6c40df1..358a080 100644 --- a/liltr/Settings/GeneralView.swift +++ b/liltr/Settings/GeneralView.swift @@ -7,13 +7,13 @@ struct AlignedText: View { let text: String let width: Float let alignment: Alignment - + init(text: String, width: Float = 135, alignment: Alignment = .trailing) { self.text = text self.width = width self.alignment = alignment } - + var body: some View { Text(text) .frame(width: CGFloat(width), alignment: alignment) @@ -30,7 +30,7 @@ struct GeneralView: View { @Default(\.secondaryLanguage) var secondaryLanguage @Default(\.menuIconSymbol) var menuIconSymbol @Default(\.preProcessSource) var preProcessSource - + var body: some View { VStack(alignment: .leading) { HStack(alignment: .center) { @@ -42,31 +42,30 @@ struct GeneralView: View { setLaunchAtLogin(newValue) } } - + Spacer() .frame(height: 20) - + HStack { AlignedText(text: "Translate HotKey") KeyboardShortcuts.Recorder(for: .translate, onChange: onHotkeyChange) } - + HStack { AlignedText(text: "OCR HotKey") KeyboardShortcuts.Recorder(for: .ocr, onChange: onOCRHotkeyChange) } - + HStack { AlignedText(text: "HotKey Action") Toggle(isOn: $hotKeyTriggerInNotification, label: { Text("In-Notification Mode") }).toggleStyle(.checkbox) } - + Spacer() .frame(height: 20) - - + HStack { AlignedText(text: "Primary Language") LanguagePicker(languageCode: $primaryLanguage, withLabel: true) @@ -75,18 +74,17 @@ struct GeneralView: View { AlignedText(text: "Secondary Language") LanguagePicker(languageCode: $secondaryLanguage, withLabel: true) } - + Spacer() .frame(height: 20) - - + HStack { AlignedText(text: "Preprocess") Toggle(isOn: $preProcessSource, label: { Text("Preprocess Source Text") }).toggleStyle(.checkbox) } - + // HStack { // AlignedText(text: "Icon Symbol") // TextField("Icon Symbol", text: $menuIconSymbol) @@ -95,14 +93,14 @@ struct GeneralView: View { } .frame(width: 400, height: 230) } - + func setLaunchAtLogin(_ enable: Bool) { do { if enable { if SMAppService.mainApp.status == .enabled { try? SMAppService.mainApp.unregister() } - + try SMAppService.mainApp.register() } else { try SMAppService.mainApp.unregister() @@ -111,7 +109,7 @@ struct GeneralView: View { debugPrint("[Settings] Failed to \(enable ? "enable" : "disable") launch at login: \(error.localizedDescription)") } } - + func onHotkeyChange(hotkey: KeyboardShortcuts.Shortcut?) { if hotkey != nil { self.hotKey = hotkey!.description @@ -119,7 +117,7 @@ struct GeneralView: View { self.hotKey = "" } } - + func onOCRHotkeyChange(hotkey: KeyboardShortcuts.Shortcut?) { if hotkey != nil { self.ocrHotKey = hotkey!.description diff --git a/liltr/Settings/ProvidersView.swift b/liltr/Settings/ProvidersView.swift index 529ec32..d54e225 100644 --- a/liltr/Settings/ProvidersView.swift +++ b/liltr/Settings/ProvidersView.swift @@ -3,10 +3,10 @@ import SwiftUI struct ProviderKeyField: View { let label: String let icon: String - + @Binding var ak: String @Binding var sk: String - + var body: some View { VStack(alignment: .leading) { HStack { @@ -19,7 +19,7 @@ struct ProviderKeyField: View { AlignedText(text: "Secret Key (SK)", width: 120) SecureField("Secret Key", text: $sk) .frame(width: 200) - + } Spacer() }.tabItem { @@ -31,23 +31,23 @@ struct ProviderKeyField: View { struct ProvidersView: View { @Default(\.primaryProvider) var primaryProvider @Default(\.secondaryProvider) var secondaryProvider - + @Default(\.NiuTransAK) var niuTransAK @Default(\.NiuTransSK) var niuTransSK - + @Default(\.BaiduAK) var baiduAK @Default(\.BaiduSK) var baiduSK - + @Default(\.VolcengineAK) var volcengineAK @Default(\.VolcengineSK) var volcengineSK - + @Default(\.AliAK) var aliAK @Default(\.AliSK) var aliSK - + @Default(\.dictionary) var dictionary - + private let _gapSize: CGFloat = 8 - + var body: some View { VStack(alignment: .center) { VStack(alignment: .center) { @@ -68,19 +68,19 @@ struct ProvidersView: View { } label: {} .frame(width: 200) } - + }.padding(EdgeInsets(top: _gapSize * 2, leading: _gapSize * 2, bottom: 0, trailing: _gapSize * 2)) - + Divider() .padding(EdgeInsets(top: _gapSize, leading: 0, bottom: _gapSize, trailing: 0)) - + TabView(content: { ProviderKeyField(label: "NiuTrans", icon: "1.square", ak: $niuTransAK, sk: $niuTransSK) - + ProviderKeyField(label: "Volcengine", icon: "2.square", ak: $volcengineAK, sk: $volcengineSK) - + ProviderKeyField(label: "Ali", icon: "3.square", ak: $aliAK, sk: $aliSK) - + ProviderKeyField(label: "Baidu", icon: "4.square", ak: $baiduAK, sk: $baiduSK) }).padding(EdgeInsets(top: 0, leading: _gapSize * 2, bottom: 0, trailing: _gapSize * 2)) } diff --git a/liltr/Settings/SettingsView.swift b/liltr/Settings/SettingsView.swift index 1df7129..3da85f7 100644 --- a/liltr/Settings/SettingsView.swift +++ b/liltr/Settings/SettingsView.swift @@ -4,26 +4,26 @@ import Sparkle struct SettingsView: View { private let _updater: SPUUpdater - + init(updater: SPUUpdater) { self._updater = updater } - + private func _handleIncomingURL(_ url: URL) { guard url.scheme == APP_NAME else { return } - + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } - + let action = components.host guard action == SchemeAction.settings.rawValue else { return } } - + var body: some View { VStack { let generalTabPane = TabPane(label: "General", icon: "gear", view: AnyView(GeneralView())) @@ -37,7 +37,7 @@ struct SettingsView: View { Spacer() .frame(height: 6) } - + TopTabView(tabPanes: [generalTabPane, providersTabPane, aboutTabPane]) } .foregroundColor(.secondary) diff --git a/liltr/Translate/BottomBarView.swift b/liltr/Translate/BottomBarView.swift index b69c551..c647220 100644 --- a/liltr/Translate/BottomBarView.swift +++ b/liltr/Translate/BottomBarView.swift @@ -9,30 +9,30 @@ struct BottomBarView: View { @Binding var languageCode: String @Binding var isDictionaryMode: Bool @ObservedObject var provider = ProviderManager.shared - + var getCopyText: () -> String var getSpeechText: () -> String var onChangeProvider: () -> Void - + var itemSwitchProvider: some View { return ToolbarItem(systemName: provider.usePrimary ? "circle.grid.2x1.left.filled" : "circle.grid.2x1.right.filled") { provider.switchProvider() onChangeProvider() } } - + var itemDictionary: some View { return ToolbarItem(systemName: isDictionaryMode ? "escape" : "character.magnify") { isDictionaryMode = !isDictionaryMode } } - + var itemCopy: some View { return ToolbarItem(systemName: "square.on.square") { - copyToPasteboard(getCopyText()) + PasteboardManager.shared.copy(getCopyText()) } } - + var itemSpeech: some View { return ToolbarItem(systemName: "speaker.2") { let text = getSpeechText() @@ -40,21 +40,21 @@ struct BottomBarView: View { SpeechManager.start(text, language) } } - + var body: some View { HStack { if isDictionaryMode { itemDictionary - + Spacer() - - HStack() { + + HStack { Spacer() itemCopy itemSpeech } .frame(width: PART_WIDTH) - + } else { // MARK: left HStack { @@ -64,14 +64,14 @@ struct BottomBarView: View { .frame(width: PART_WIDTH) Spacer() - + // MARK: mid itemSwitchProvider - + Spacer() - + // MARK: right - HStack() { + HStack { Spacer() itemDictionary itemCopy diff --git a/liltr/Translate/MidBarView.swift b/liltr/Translate/MidBarView.swift index 4b3a9d4..f60b54f 100644 --- a/liltr/Translate/MidBarView.swift +++ b/liltr/Translate/MidBarView.swift @@ -3,16 +3,17 @@ import SwiftData import UniformTypeIdentifiers struct MidBarView: View { + var showButton: Bool var isLoading: Bool var onSwap: () -> Void - var onDrag: (_ height: CGFloat) -> Void = { height in } - + var onDrag: (_ height: CGFloat) -> Void = { _ in } + private let height = CGFloat(25) - + var body: some View { ZStack { Divider() - + Rectangle() .fill(.clear) .cursor(.resizeUpDown) @@ -22,28 +23,29 @@ struct MidBarView: View { onDrag(value.translation.height) } ) - - if isLoading { - ProgressView() - .scaleEffect(CGSize(width: 0.5, height: 0.5)) - } else { - Button { - onSwap() - } label: { - Image(systemName: "arrow.up.arrow.down") + + if showButton { + if isLoading { + ProgressView() + .scaleEffect(CGSize(width: 0.5, height: 0.5)) + } else { + Button { + onSwap() + } label: { + Image(systemName: "arrow.up.arrow.down") + } + .background(Color.backgroundColor) } - .background(Color.backgroundColor) } }.frame(height: height) } - + } #Preview("MidBar isLoading", traits: .fixedLayout(width: 300, height: 200)) { - MidBarView(isLoading: true, onSwap: {}) + MidBarView(showButton: true, isLoading: true, onSwap: {}) } #Preview("MidBar", traits: .fixedLayout(width: 300, height: 200)) { - MidBarView(isLoading: false, onSwap: {}) + MidBarView(showButton: true, isLoading: false, onSwap: {}) } - diff --git a/liltr/Translate/TopBarView.swift b/liltr/Translate/TopBarView.swift index 2a9c78b..ab7c74d 100644 --- a/liltr/Translate/TopBarView.swift +++ b/liltr/Translate/TopBarView.swift @@ -5,7 +5,7 @@ import UniformTypeIdentifiers struct TopBarView: View { @Environment(\.openWindow) private var openWindow @Default(\.floatOnTop) var floatOnTop - + var body: some View { HStack { Spacer() @@ -22,14 +22,13 @@ struct TopBarView: View { float() } } - + func float() { WindowManager.float(id: .translate, enable: floatOnTop) } - + } #Preview("TopBar", traits: .fixedLayout(width: 300, height: 200)) { TopBarView() } - diff --git a/liltr/Translate/TranslateFieldView.swift b/liltr/Translate/TranslateFieldView.swift index f4c76fd..6b705ed 100644 --- a/liltr/Translate/TranslateFieldView.swift +++ b/liltr/Translate/TranslateFieldView.swift @@ -9,14 +9,14 @@ struct TranslateFieldView: View { var onChange: (() -> Void)? var readOnly: Bool = false var maxLength: Int = 5000 - + @FocusState private var focused: Bool @State private var triggered = false let debouncer = PassthroughSubject() - + private let fontSize = CGFloat(14) private let paddingSize = CGFloat(10) - + var body: some View { ZStack(alignment: .topLeading) { ZStack(alignment: .bottomTrailing) { @@ -24,23 +24,22 @@ struct TranslateFieldView: View { .font(.system(size: fontSize)) .scrollContentBackground(.hidden) .padding(EdgeInsets(top: 0, leading: paddingSize, bottom: 0, trailing: paddingSize)) - .onReceive(debouncer.debounce(for: .milliseconds(triggered ? 300 : 500), scheduler: RunLoop.main)) { input in + .onReceive(debouncer.debounce(for: .milliseconds(triggered ? 300 : 500), scheduler: RunLoop.main)) { _ in onChange?() } .onChange(of: text) { - if (text.isEmpty) { + if text.isEmpty { triggered = false - } else if (!triggered && CharacterSet.whitespacesAndNewlines.contains(text.last!.unicodeScalars.first!)) { + } else if !triggered && CharacterSet.whitespacesAndNewlines.contains(text.last!.unicodeScalars.first!) { triggered = true - + } debouncer.send(text) } -// .onChange(of: text, Debouncer.debounce(delay: .milliseconds(300), action: onChange ?? {})) .focused($focused) .bottomFade() .onAppear { - if (!readOnly) { + if !readOnly { focused = true } } @@ -49,7 +48,7 @@ struct TranslateFieldView: View { text = String(text.prefix(maxLength)) } } - + if !readOnly && !text.isEmpty { Image(systemName: "xmark.circle.fill") .onTapGesture { @@ -60,7 +59,7 @@ struct TranslateFieldView: View { .padding(EdgeInsets(top: 0, leading: paddingSize, bottom: 0, trailing: paddingSize)) } } - + if text.isEmpty && !placeholder.isEmpty { TextEditor(text: .constant(placeholder)) .font(.system(size: fontSize)) diff --git a/liltr/Translate/TranslateView.swift b/liltr/Translate/TranslateView.swift index b764bb2..72f1f05 100644 --- a/liltr/Translate/TranslateView.swift +++ b/liltr/Translate/TranslateView.swift @@ -5,15 +5,15 @@ import WebKit struct HTMLStringView: NSViewRepresentable { typealias NSViewType = WKWebView - + let htmlContent: String - + func makeNSView(context: Context) -> WKWebView { var view = WKWebView() view.setValue(false, forKey: "drawsBackground") return view } - + func updateNSView(_ nsView: WKWebView, context: Context) { nsView.loadHTMLString(htmlContent, baseURL: nil) } @@ -21,56 +21,56 @@ struct HTMLStringView: NSViewRepresentable { struct TranslateView: View { @ObservedObject private var provider = ProviderManager.shared - + @State private var sourceText: String = "" @State private var targetText: String = "" @State private var targetLanguageCode: String = Defaults.shared.primaryLanguage @State private var height: CGFloat = 100 @State private var isDictionaryMode: Bool = false - + private let MIN_HEIGHT: CGFloat = 40 - + private func _handleIncomingURL(_ url: URL) { guard url.scheme == APP_NAME else { return } - + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } - + let action = components.host guard action == SchemeAction.translateInWindow.rawValue else { return } - + let src = components.queryItems?.first(where: { $0.name == "src" })?.value ?? "" guard !src.isEmpty else { return } - + sourceText = src.removingPercentEncoding! } - + var body: some View { GeometryReader {geometry in VStack(alignment: .leading) { // MARK: top bar TopBarView() .padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 4)) - + VStack(alignment: .leading, spacing: 0) { // MARK: source text field TranslateFieldView(text: $sourceText, placeholder: "Type any words to start...", onChange: onSourceInput) .frame(height: self.height) - + // MARK: mid bar - MidBarView(isLoading: provider.isTranslating, onSwap: onSwap) { height in + MidBarView(showButton: !isDictionaryMode, isLoading: provider.isTranslating, onSwap: onSwap) { height in self.height = floor(minMax(self.height + height, min: MIN_HEIGHT, max: geometry.size.height - 100)) } - + // MARK: target text field - if (targetText.starts(with: " String { return sourceText } - + func getTargetText() -> String { return targetText } - + func onSwap() { sourceText = targetText targetLanguageCode = targetLanguageCode == Defaults.shared.primaryLanguage ? Defaults.shared.secondaryLanguage : Defaults.shared.primaryLanguage } - + func onSourceInput() { - if (sourceText.isEmpty) { + if sourceText.isEmpty { targetText = "" } else { provider.translate(sourceText, isDictionaryMode ? nil : LanguageManager.getLanguageByCode(targetLanguageCode)!, updateTargetText) } } - + func updateTargetText(_ result: ProviderCallbackData) { self.targetText = result.target self.isDictionaryMode = result.isDictionary - if (result.targetLanguage != nil) { + if result.targetLanguage != nil { self.targetLanguageCode = result.targetLanguage!.code } } diff --git a/liltr/Utils/Common/AppleScript.swift b/liltr/Utils/Common/AppleScript.swift new file mode 100644 index 0000000..fc29c9a --- /dev/null +++ b/liltr/Utils/Common/AppleScript.swift @@ -0,0 +1,15 @@ +import Foundation + +func executeAppleScript(_ appleScript: String, completion: @escaping (String?, Error?) -> Void) { + let script = NSAppleScript(source: appleScript) + var error: NSDictionary? + if let resultDescriptor = script?.executeAndReturnError(&error) { + if let resultString = resultDescriptor.stringValue { + completion(resultString, nil) + } else { + completion(nil, NSError(domain: "ScriptError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Script did not return a string."])) + } + } else { + completion(nil, error as? Error) + } +} diff --git a/liltr/Utils/Common/Common.swift b/liltr/Utils/Common/Common.swift index c9332dc..7ff2a4c 100644 --- a/liltr/Utils/Common/Common.swift +++ b/liltr/Utils/Common/Common.swift @@ -11,73 +11,21 @@ func dict2headers(dict: [String: String]) -> HTTPHeaders { } func minMax(_ x: CGFloat, min: CGFloat, max: CGFloat) -> CGFloat { - if (x < min) { + if x < min { return min - } else if (x > max) { + } else if x > max { return max } else { return x } } -func copyToPasteboard(_ string: String) { - let pasteBoard = NSPasteboard.general - pasteBoard.clearContents() - pasteBoard.setString(string, forType: .string) -} - -func getSelectedText() -> String? { - let systemWideElement = AXUIElementCreateSystemWide() - - let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String : true] - let accessEnabled = AXIsProcessTrustedWithOptions(options) - - var selectedTextValue: AnyObject? - let errorCode = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &selectedTextValue) - - if errorCode == .success { - let selectedTextElement = selectedTextValue as! AXUIElement - var selectedText: AnyObject? - let textErrorCode = AXUIElementCopyAttributeValue(selectedTextElement, kAXSelectedTextAttribute as CFString, &selectedText) - - if textErrorCode == .success, let selectedTextString = selectedText as? String { - return selectedTextString - } else { - debugPrint("[getSelectedText] AXUIElementCopyAttributeValue errorCode invalid:", textErrorCode) - return nil - } - } else { - debugPrint("[getSelectedText] errorCode invalid:", errorCode.rawValue.description) - return nil - } -} - -class MatchManager { - private var info: [String] = [] - - func set(_ info: [String]) { - self.info = info - } - - func match(_ info: [String]) -> Bool { - if info.count != self.info.count { - return false - } else { - return info == self.info - } - } - - func reset() { - self.info = [] - } -} - func regexMatched(_ string: String, _ regex: String) -> Bool { do { let regex = try NSRegularExpression(pattern: regex) let range = NSRange(location: 0, length: string.utf16.count) let match = regex.firstMatch(in: string, options: [], range: range) - + return match != nil } catch let error { return false diff --git a/liltr/Utils/Common/Debouncer.swift b/liltr/Utils/Common/Debouncer.swift index 72a7f7d..86ce378 100644 --- a/liltr/Utils/Common/Debouncer.swift +++ b/liltr/Utils/Common/Debouncer.swift @@ -1,7 +1,7 @@ import Foundation extension TimeInterval { - + /** Checks if `since` has passed since `self`. @@ -11,16 +11,16 @@ extension TimeInterval { func hasPassed(since: TimeInterval) -> Bool { return Date().timeIntervalSinceReferenceDate - self > since } - + } // https://gist.github.com/simme/b78d10f0b29325743a18c905c5512788 class Throttler { static var currentWorkItem: DispatchWorkItem? static var lastFire: TimeInterval = 0 - + static func throttle(delay: TimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { - + return { [] in guard Throttler.currentWorkItem == nil else { return } Throttler.currentWorkItem = DispatchWorkItem { @@ -28,7 +28,7 @@ class Throttler { self.lastFire = Date().timeIntervalSinceReferenceDate self.currentWorkItem = nil } - + delay.hasPassed(since: self.lastFire) ? queue.async(execute: self.currentWorkItem!) : queue.asyncAfter(deadline: .now() + delay, execute: self.currentWorkItem!) } } @@ -36,7 +36,7 @@ class Throttler { class Debouncer { static var currentWorkItem: DispatchWorkItem? - + static func debounce(delay: DispatchTimeInterval, queue: DispatchQueue = .main, action: @escaping (() -> Void)) -> () -> Void { return { [] in Debouncer.currentWorkItem?.cancel() @@ -45,4 +45,3 @@ class Debouncer { } } } - diff --git a/liltr/Utils/Common/Pasteboard.swift b/liltr/Utils/Common/Pasteboard.swift new file mode 100644 index 0000000..49f3dcc --- /dev/null +++ b/liltr/Utils/Common/Pasteboard.swift @@ -0,0 +1,22 @@ +import Foundation +import Carbon.HIToolbox + +class PasteboardManager { + public static let shared = PasteboardManager() + + private let _pasteboard = NSPasteboard.general + + var changeCount: Int { + return _pasteboard.changeCount + } + + var content: String { + return NSPasteboard.general.readObjects(forClasses: [NSString.self], options: nil)?.first as? String ?? "" + } + + func copy(_ string: String) { + let pasteBoard = NSPasteboard.general + pasteBoard.clearContents() + pasteBoard.setString(string, forType: .string) + } +} diff --git a/liltr/Utils/Common/SelectedText.swift b/liltr/Utils/Common/SelectedText.swift new file mode 100644 index 0000000..2e64230 --- /dev/null +++ b/liltr/Utils/Common/SelectedText.swift @@ -0,0 +1,114 @@ +import Foundation +import Carbon.HIToolbox + +class SelectedTextManager { + public static let shared = SelectedTextManager() + + func getText(_ completion: @escaping (String?, Error?) -> Void) { + let textByAX = _getByAX() + debugPrint("[SelectedTextManager#_getByAX] ->", textByAX) + if textByAX != nil && !textByAX!.isEmpty { + completion(textByAX, nil) + return + } + + _getByAppleScript { textByAS, _ in + debugPrint("[SelectedTextManager#_getByAppleScript] ->", textByAS) + if textByAS != nil && !textByAX!.isEmpty { + completion(textByAS, nil) + return + } + } + + _getByCopy { textByCopy, error in + debugPrint("[SelectedTextManager#_getByCopy] ->", textByCopy) + completion(textByCopy, error) + } + } + + private func _getByAX() -> String? { + let systemWideElement = AXUIElementCreateSystemWide() + + let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] + let accessEnabled = AXIsProcessTrustedWithOptions(options) + + var selectedTextValue: AnyObject? + let errorCode = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &selectedTextValue) + + if errorCode == .success { + let selectedTextElement = selectedTextValue as! AXUIElement + var selectedText: AnyObject? + let textErrorCode = AXUIElementCopyAttributeValue(selectedTextElement, kAXSelectedTextAttribute as CFString, &selectedText) + + if textErrorCode == .success, let selectedTextString = selectedText as? String { + return selectedTextString + } else { + debugPrint("[getSelectedText] AXUIElementCopyAttributeValue errorCode invalid:", textErrorCode) + return nil + } + } else { + debugPrint("[getSelectedText] errorCode invalid:", errorCode.rawValue.description) + return nil + } + } + + private func _getByAppleScript(_ completion: @escaping (String?, Error?) -> Void) { + let bundleID = NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "" + func isSafari(_ bundleId: String) -> Bool { + return bundleId == "com.apple.Safari" + } + + func isChromium(_ bundleId: String) -> Bool { + return bundleId == "com.google.Chrome" || bundleId == "com.microsoft.edgemac" + } + + if isSafari(bundleID) { + let source = """ + tell application id "\(bundleID)" + tell front document + set selection_text to do JavaScript "window.getSelection().toString();" + end tell + end tell + """ + executeAppleScript(source, completion: completion) + } else if isChromium(bundleID) { + let source = """ + tell application id "\(bundleID)" + tell active tab of front window + set selection_text to execute javascript "window.getSelection().toString();" + end tell + end tell + """ + executeAppleScript(source, completion: completion) + } else { + completion(nil, nil) + } + } + + // https://stackoverflow.com/a/49502614 + private func _getByCopy(_ completion: @escaping (String?, Error?) -> Void) { + let oldChangeCount = PasteboardManager.shared.changeCount + let oldText = PasteboardManager.shared.content + let src = CGEventSource(stateID: .combinedSessionState)! + let cKeyCode: CGKeyCode = 0x08 + let keyDownEvent = CGEvent(keyboardEventSource: src, virtualKey: cKeyCode, keyDown: true) + keyDownEvent?.flags = .maskCommand + let keyUpEvent = CGEvent(keyboardEventSource: src, virtualKey: cKeyCode, keyDown: false) + keyDownEvent?.post(tap: .cghidEventTap) + keyUpEvent?.post(tap: .cghidEventTap) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // wait 0.05s for copy. + let newChangeCount = PasteboardManager.shared.changeCount + let newText = PasteboardManager.shared.content + + if oldChangeCount == newChangeCount { + // indicate last copy trigger has failed + completion(nil, nil) + return + } + + PasteboardManager.shared.copy(oldText) + completion(newText, nil) + } + } +} diff --git a/liltr/Utils/CryptoEncoder/Encoder.swift b/liltr/Utils/CryptoEncoder/Encoder.swift index bd33ada..6589093 100644 --- a/liltr/Utils/CryptoEncoder/Encoder.swift +++ b/liltr/Utils/CryptoEncoder/Encoder.swift @@ -5,35 +5,34 @@ class CryptoEncoder { static func str2data(_ string: String) -> Data { return Data(string.utf8) } - + static func data2str(_ data: Data) -> String { return data.map { String(format: "%02hhx", $0) }.joined() } - + static func md5(data: Data) -> Data { return Data(Insecure.MD5.hash(data: data)) } - + static func md5(string: String) -> String { let stringData = CryptoEncoder.str2data(string) let md5Data = md5(data: stringData) return data2str(md5Data) } - + static func base64(data: Data) -> String { return data.base64EncodedString() } - + static func base64(data: Data) -> Data { return data.base64EncodedData() } - + static func base64(string: String) -> String { let stringData = CryptoEncoder.str2data(string) return stringData.base64EncodedString() } - - + // // static func md5(_ string: String) -> Data { // let data = Insecure.MD5.hash(data: Data(string.utf8)) diff --git a/liltr/Utils/KeyboardShortcut/KeyboardShortcut.swift b/liltr/Utils/KeyboardShortcut/KeyboardShortcut.swift index 04f2afe..35a874c 100644 --- a/liltr/Utils/KeyboardShortcut/KeyboardShortcut.swift +++ b/liltr/Utils/KeyboardShortcut/KeyboardShortcut.swift @@ -2,31 +2,31 @@ import KeyboardShortcuts import SwiftUI func string2Shortcut(_ str: String) -> KeyboardShortcut? { - if (str.count == 0) { + if str.count == 0 { return nil } - + var modifiers: SwiftUI.EventModifiers = [] - + if str.contains("⌘") { modifiers.update(with: EventModifiers.command) } - + if str.contains("⌃") { modifiers.update(with: EventModifiers.control) } - + if str.contains("⌥") { modifiers.update(with: EventModifiers.option) } - + if str.contains("⇧") { modifiers.update(with: EventModifiers.shift) } - + if str.contains("⇪") { modifiers.update(with: EventModifiers.capsLock) } - + return KeyboardShortcut(KeyEquivalent(str.last!), modifiers: modifiers) } diff --git a/liltr/Utils/Language/Language.swift b/liltr/Utils/Language/Language.swift index 5939c09..f716919 100644 --- a/liltr/Utils/Language/Language.swift +++ b/liltr/Utils/Language/Language.swift @@ -2,15 +2,14 @@ struct Language { var code: String var flag: String var name: String - + var shortCode: String { return String(code.split(separator: "-")[0]) } - + init(code: String, flag: String, name: String) { self.code = code self.flag = flag self.name = name } } - diff --git a/liltr/Utils/Language/LanguageManager.swift b/liltr/Utils/Language/LanguageManager.swift index 8da8df3..4dc3296 100644 --- a/liltr/Utils/Language/LanguageManager.swift +++ b/liltr/Utils/Language/LanguageManager.swift @@ -20,60 +20,60 @@ let LANGUAGE_ARRAY = [ Language(code: "hi-IN", flag: "🇮🇳", name: "हिन्दी") ] -let LANGUAGE_DICT = Dictionary(uniqueKeysWithValues: LANGUAGE_ARRAY.map{ ($0.code, $0) }) +let LANGUAGE_DICT = Dictionary(uniqueKeysWithValues: LANGUAGE_ARRAY.map { ($0.code, $0) }) class LanguageManager { static var primaryLanguage: Language { return LanguageManager.getLanguageByCode(Defaults.shared.primaryLanguage)! } - + static var secondaryLanguage: Language { return LanguageManager.getLanguageByCode(Defaults.shared.secondaryLanguage)! } - + static func getShortCode(_ code: String) -> String { - if (code.contains("-")) { + if code.contains("-") { return String(code.split(separator: "-")[0]) - } else if (code.contains("_")) { + } else if code.contains("_") { return String(code.split(separator: "-")[0]) } return code } - + static func getLanguageByCode(_ code: String) -> Language? { - if (LANGUAGE_DICT[code] != nil) { + if LANGUAGE_DICT[code] != nil { return LANGUAGE_DICT[code] } - + let shortCode = getShortCode(code) for language in LANGUAGE_ARRAY { if shortCode == language.shortCode { return language } } - + return nil } - + static func getStandardCode(_ code: String) -> String? { return getLanguageByCode(code)?.code } - + static func getLanguageByContent(_ content: String) -> Language { let recognizer = NLLanguageRecognizer() recognizer.processString(content) - + if let language = recognizer.dominantLanguage { let code = language.rawValue.description debugPrint("[LanguageManager] getLanguageByContent", code) return getLanguageByCode(code) ?? secondaryLanguage } - + return secondaryLanguage } - + static func getFromTo(_ source: String, _ oldTargetLanguage: Language?) -> (Language, Language)? { - if (source.isEmpty) { + if source.isEmpty { return nil } @@ -86,7 +86,7 @@ class LanguageManager { targetLanguage = secondaryLanguage } } - + return (recognizedLanguage, targetLanguage ?? recognizedLanguage) } } diff --git a/liltr/Utils/Language/SpeechManager.swift b/liltr/Utils/Language/SpeechManager.swift index 5b3cfc9..8ed5fe9 100644 --- a/liltr/Utils/Language/SpeechManager.swift +++ b/liltr/Utils/Language/SpeechManager.swift @@ -3,31 +3,31 @@ import AVFoundation class SpeechManager { private static let speechSynthesizer = AVSpeechSynthesizer() - + private static func getVoiceScore(voice: AVSpeechSynthesisVoice, language: Language) -> Int { var score = 1 - if (voice.language == language.code) { + if voice.language == language.code { score += 10 } - if (voice.quality == .premium) { + if voice.quality == .premium { score += 8 - } else if (voice.quality == .enhanced) { + } else if voice.quality == .enhanced { score += 5 } - + return score } - + static func stop(at boundary: AVSpeechBoundary = .immediate) { speechSynthesizer.stopSpeaking(at: boundary) } - + static func start(_ string: String, _ language: Language) { - if (string.isEmpty) { + if string.isEmpty { return } - - if (speechSynthesizer.isSpeaking) { + + if speechSynthesizer.isSpeaking { stop() } else { let speechUtterance: AVSpeechUtterance = AVSpeechUtterance(string: string) @@ -36,14 +36,14 @@ class SpeechManager { speechSynthesizer.speak(speechUtterance) } } - + static func getVoiceByLanguage(_ language: Language) -> AVSpeechSynthesisVoice? { var resultScore: Int = 0 - var result: AVSpeechSynthesisVoice? = nil + var result: AVSpeechSynthesisVoice? for voice in AVSpeechSynthesisVoice.speechVoices() { - if (voice.language == language.code || LanguageManager.getShortCode(voice.language) == language.shortCode) { + if voice.language == language.code || LanguageManager.getShortCode(voice.language) == language.shortCode { let score = getVoiceScore(voice: voice, language: language) - if (score > resultScore) { + if score > resultScore { result = voice resultScore = score } @@ -51,5 +51,5 @@ class SpeechManager { } return result } - + } diff --git a/liltr/Utils/Notification/NotificationManager.swift b/liltr/Utils/Notification/NotificationManager.swift index 1d07934..b762639 100644 --- a/liltr/Utils/Notification/NotificationManager.swift +++ b/liltr/Utils/Notification/NotificationManager.swift @@ -9,13 +9,13 @@ func pushNotification(title: String, body: String) { content.title = title content.body = body content.categoryIdentifier = categoryIdentifier - + let copy = UNNotificationAction(identifier: "copy", title: "Copy") let speak = UNNotificationAction(identifier: "speak", title: "Speak") let expand = UNNotificationAction(identifier: "expand", title: "Expand", options: [.foreground]) let category = UNNotificationCategory(identifier: categoryIdentifier, actions: [copy, speak, expand], intentIdentifiers: []) center.setNotificationCategories([category]) - + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) center.add(request) } else { diff --git a/liltr/Utils/OCR/OCRManager.swift b/liltr/Utils/OCR/OCRManager.swift index f5f4e26..cc23fb5 100644 --- a/liltr/Utils/OCR/OCRManager.swift +++ b/liltr/Utils/OCR/OCRManager.swift @@ -6,19 +6,19 @@ import SwiftUI // https://developer.apple.com/documentation/vision/recognizing_text_in_images class OCRManager { public static let shared = OCRManager() - + private var _task: Process? private var _taskId: String? - + private func resetTask() { _task?.terminate() _taskId = nil _task = nil } - + func capture() -> NSImage? { resetTask() - + let taskId = String(Int(round(Date().timeIntervalSince1970))) let tempPath = NSURL.fileURL(withPathComponents: [NSTemporaryDirectory(), "capture_\(taskId).png"])!.path() _taskId = taskId @@ -33,31 +33,31 @@ class OCRManager { resetTask() return nil } - + _task?.waitUntilExit() - if (_taskId != taskId) { + if _taskId != taskId { return nil } resetTask() return NSImage(contentsOfFile: tempPath) } - + private func _recoginizeTextHandler(request: VNRequest, error: Error?, cb: @escaping (String) -> Void) { guard let observations = request.results as? [VNRecognizedTextObservation] else { return } - if (observations.isEmpty) { + if observations.isEmpty { debugPrint("[OCR Manager] observations result is empty") return } let recognizedStrings = observations.compactMap { observation in return observation.topCandidates(1).first?.string } - + cb(recognizedStrings.joined(separator: "\n")) } - + func ocr(image: CGImage, cb: @escaping (String) -> Void) { let requestHandler = VNImageRequestHandler(cgImage: image) let request = VNRecognizeTextRequest { request, error in @@ -70,10 +70,10 @@ class OCRManager { debugPrint("[OCR Manager] Unable to perform the requests: \(error)") } } - + func captureWithOCR(cb: @escaping (String) -> Void) { let image = self.capture() - if (image != nil) { + if image != nil { ocr(image: image!.cgImage(forProposedRect: nil, context: nil, hints: nil)!, cb: cb) } } diff --git a/liltr/Utils/Provider/Ali.swift b/liltr/Utils/Provider/Ali.swift index f965959..5b2a51e 100644 --- a/liltr/Utils/Provider/Ali.swift +++ b/liltr/Utils/Provider/Ali.swift @@ -1,5 +1,3 @@ - - import Alamofire import Foundation import CryptoKit @@ -15,16 +13,16 @@ struct AliResponse: BaseResponse { let Message: String? let RequestId: String? let Data: AliResult? - + var target: String? { - if (Data?.Translated?.isEmpty == false) { + if Data?.Translated?.isEmpty == false { return Data!.Translated! } return nil } - + var errorMessage: String? { - if (Message != nil) { + if Message != nil { return "\(Code!): \(Message!)" } return nil @@ -38,10 +36,10 @@ class AliProvider: BaseProvider { let delay: DispatchTimeInterval = .seconds(1) let name = AliProviderName let apiUrl = "https://mt.cn-hangzhou.aliyuncs.com/api/translate/web/general" - + var ak = Defaults.shared.AliAK.isEmpty ? Bundle.main.infoDictionary!["AliAK"] as! String : Defaults.shared.AliAK var sk = Defaults.shared.AliSK.isEmpty ? Bundle.main.infoDictionary!["AliSK"] as! String : Defaults.shared.AliSK - + private func _getDate() -> String { let date = Date() let dateFormatter = DateFormatter() @@ -50,7 +48,7 @@ class AliProvider: BaseProvider { dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) return dateFormatter.string(from: date) } - + private func _getHeaders() -> HTTPHeaders { let date = self._getDate() let url = URL(string: apiUrl)! @@ -62,52 +60,48 @@ class AliProvider: BaseProvider { "Date": date, "Host": "mt.cn-hangzhou.aliyuncs.com", "x-acs-signature-method": "HMAC-SHA1", - "x-acs-signature-nonce": uuid, + "x-acs-signature-nonce": uuid ] - + let stringToSignArr: [String] = ["POST", headers["Accept"]!+"\n", headers["Content-Type"]!, headers["Date"]!, "x-acs-signature-method:HMAC-SHA1", "x-acs-signature-nonce:\(uuid)", path] let stringToSign = stringToSignArr.joined(separator: "\n") let signature = hmac_sha1(sk, stringToSign) let authHeader = "acs \(ak):\(signature)" headers.updateValue(authHeader, forKey: "Authorization") - + return dict2headers(dict: headers) } - + private func hmac_sha1(_ key: String, _ content: String) -> String { let _key = key.data(using: .utf8)! let data = content.data(using: .utf8)! let hmac = HMAC.authenticationCode(for: data, using: SymmetricKey(data: _key)) return Data(hmac).base64EncodedString() } - - - func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ _sourceLanguage: Language? , _ _targetLanguage: Language?) -> Void) -> Void { + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ _sourceLanguage: Language?, _ _targetLanguage: Language?) -> Void) { let parameters: [String: String] = [ "FormatType": "text", "SourceText": source, "SourceLanguage": from.shortCode, "TargetLanguage": to.shortCode, - "Scene": "general", + "Scene": "general" ] let headers = _getHeaders() - + debugPrint("[AliProvider] parameters:", parameters) - + AF.request(apiUrl, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers) .cacheResponse(using: .cache) .responseDecodable(of: AliResponse.self) { response in - if (response.error != nil) { + if response.error != nil { cb(response.error!.errorDescription!, nil, nil) - } else if (response.value?.errorMessage != nil) { + } else if response.value?.errorMessage != nil { cb(response.value!.errorMessage!, nil, nil) } else { cb(response.value!.target!, from, to) } } } - -} - - +} diff --git a/liltr/Utils/Provider/AppleDictionary.swift b/liltr/Utils/Provider/AppleDictionary.swift index 4ca909a..f74e2d3 100644 --- a/liltr/Utils/Provider/AppleDictionary.swift +++ b/liltr/Utils/Provider/AppleDictionary.swift @@ -9,8 +9,8 @@ func replaceLinks(html: String) -> String { let curRange = Range(match.range, in: html)! let letterRange = Range(match.range(at: 1), in: html)! let captured = String(html[letterRange]) - - if (captured.contains("<")) { + + if captured.contains("<") { result.replaceSubrange(curRange, with: captured) } else { let url = SchemeURLManager.getUrlByAction(.translateInWindow, querys: ["src": captured]) @@ -26,10 +26,10 @@ func extractFromHTML(html: String, tag: String) -> String { let regex = try NSRegularExpression(pattern: pattern, options: .dotMatchesLineSeparators) let nsString = html as NSString let matches = regex.matches(in: html, options: [], range: NSRange(location: 0, length: nsString.length)) - + guard let match = matches.first else { return "" } let bodyContent = nsString.substring(with: match.range) - + if let data = bodyContent.data(using: .utf8), let decodedString = String(data: data, encoding: .utf8) { return decodedString } else { @@ -41,21 +41,20 @@ func extractFromHTML(html: String, tag: String) -> String { } } - let AppleDictionaryProviderName = "AppleDictionary" // https://github.com/tisfeng/Easydict/blob/main/docs/How-to-use-macOS-system-dictionary-in-Easydict-zh.md // https://dictionaries.io/ class AppleDictionaryProvider: BaseProvider { static let shared = AppleDictionaryProvider() - + var delay: DispatchTimeInterval = .milliseconds(250) let name = AppleDictionaryProviderName - + var dictionary: String { return Defaults.shared.dictionary } - + private func _lookupNative(_ word: String) -> String { let cfWord = word as CFString let range = DCSGetTermRangeInString(nil, cfWord, 0) @@ -66,19 +65,19 @@ class AppleDictionaryProvider: BaseProvider { return ("No definition found for \(word)") } } - + private func _lookUpByDictionary(term: String, dictionary: String) -> TTTDictionaryEntry? { let dictionary = TTTDictionary.init(named: dictionary) let entries = dictionary.entries(forSearchTerm: term) as? [TTTDictionaryEntry] return entries?.first } - + func getDictionaries() -> [String] { return TTTDictionary.availableDictionaries().map { item in return item.name } } - + private func _handleHTML(_ html: String) -> String { let head = extractFromHTML(html: html, tag: "head") let body = extractFromHTML(html: html, tag: "body") @@ -94,11 +93,11 @@ class AppleDictionaryProvider: BaseProvider { """ return result } - - func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { - if (!dictionary.isEmpty) { + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) { + if !dictionary.isEmpty { let result = _lookUpByDictionary(term: source, dictionary: dictionary) - if (result != nil) { + if result != nil { cb(_handleHTML(result!.htmlWithPopoverCSS), nil, nil) return } @@ -109,7 +108,7 @@ class AppleDictionaryProvider: BaseProvider { continue } let result = _lookUpByDictionary(term: source, dictionary: DCSOxfordDictionaryOfEnglish) - if (result != nil) { + if result != nil { cb(_handleHTML(result!.htmlWithPopoverCSS), nil, nil) break } @@ -117,6 +116,3 @@ class AppleDictionaryProvider: BaseProvider { cb("No definition found for \(source)", nil, nil) } } - - - diff --git a/liltr/Utils/Provider/Baidu.swift b/liltr/Utils/Provider/Baidu.swift index f501f20..e8983f2 100644 --- a/liltr/Utils/Provider/Baidu.swift +++ b/liltr/Utils/Provider/Baidu.swift @@ -12,9 +12,9 @@ struct BaiduResponse: BaseResponse { let trans_result: [BaiduResult]? let from: String? let to: String? - + var target: String? { - if (trans_result?.isEmpty == false) { + if trans_result?.isEmpty == false { var result: [String] = [] for item in trans_result! { result.append(item.dst!) @@ -23,9 +23,9 @@ struct BaiduResponse: BaseResponse { } return nil } - + var errorMessage: String? { - if (error_msg != nil) { + if error_msg != nil { return "\(error_code!): \(error_msg!)" } return nil @@ -39,20 +39,19 @@ class BaiduProvider: BaseProvider { let name = BaiduProviderName let delay: DispatchTimeInterval = .seconds(1) let apiUrl = "https://fanyi-api.baidu.com/api/trans/vip/translate" - + var ak = Defaults.shared.BaiduAK.isEmpty ? Bundle.main.infoDictionary!["BaiduAK"] as! String : Defaults.shared.BaiduAK var sk = Defaults.shared.BaiduSK.isEmpty ? Bundle.main.infoDictionary!["BaiduSK"] as! String : Defaults.shared.BaiduSK - + @Published var isTranslating = false - private let matchManager = MatchManager() private func _sign(q: String, salt: String) -> String { let str1 = "\(ak)\(q)\(salt)\(sk)" let str2 = CryptoEncoder.md5(string: str1).lowercased() return str2 } - - func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) { let salt = String(Int(round(Date().timeIntervalSince1970))) let sign = _sign(q: source, salt: salt) let parameters: [String: String] = [ @@ -63,23 +62,20 @@ class BaiduProvider: BaseProvider { "salt": salt, "sign": sign ] - + debugPrint("[BaiduProvider] parameters:", parameters) - + AF.request(apiUrl, method: .post, parameters: parameters, encoding: URLEncoding.default) .cacheResponse(using: .cache) .responseDecodable(of: BaiduResponse.self) { response in - if (response.error != nil) { + if response.error != nil { cb(response.error!.errorDescription!, nil, nil) - } else if (response.value?.errorMessage != nil) { + } else if response.value?.errorMessage != nil { cb(response.value!.errorMessage!, nil, nil) } else { cb(response.value!.target!, from, to) } } } - -} - - +} diff --git a/liltr/Utils/Provider/BigHugeThesaurus.swift b/liltr/Utils/Provider/BigHugeThesaurus.swift index 169edc8..03ef065 100644 --- a/liltr/Utils/Provider/BigHugeThesaurus.swift +++ b/liltr/Utils/Provider/BigHugeThesaurus.swift @@ -14,63 +14,63 @@ struct BigHugeThesaurusResponse: BaseResponse { let verb: BigHugeThesaurusResult? let adjective: BigHugeThesaurusResult? let adverb: BigHugeThesaurusResult? - + func parseResult(result: BigHugeThesaurusResult?) -> String { if result == nil { return "" } - + var lines: [String] = [] - if (!(result?.syn?.isEmpty ?? true)) { + if !(result?.syn?.isEmpty ?? true) { lines.append("• synonyms:") lines.append("\t" + result!.syn!.joined(separator: " / ")) } - if (!(result?.ant?.isEmpty ?? true)) { + if !(result?.ant?.isEmpty ?? true) { lines.append("• antonyms:") lines.append("\t" + result!.ant!.joined(separator: " / ")) } - if (!(result?.rel?.isEmpty ?? true)) { + if !(result?.rel?.isEmpty ?? true) { lines.append("• related:") lines.append("\t" + result!.rel!.joined(separator: " / ")) } - if (!(result?.sim?.isEmpty ?? true)) { + if !(result?.sim?.isEmpty ?? true) { lines.append("• similar:") lines.append("\t" + result!.sim!.joined(separator: " / ")) } - if (!(result?.usr?.isEmpty ?? true)) { + if !(result?.usr?.isEmpty ?? true) { lines.append("• suggestions:") lines.append("\t" + result!.usr!.joined(separator: " / ")) } - + return lines.joined(separator: "\n") } - + var target: String? { var parts: [String] = [] - let nounStr = self.parseResult(result: self.noun); - if (!nounStr.isEmpty) { + let nounStr = self.parseResult(result: self.noun) + if !nounStr.isEmpty { parts.append("[noun]") parts.append(nounStr + "\n") } - let verbStr = self.parseResult(result: self.verb); - if (!verbStr.isEmpty) { + let verbStr = self.parseResult(result: self.verb) + if !verbStr.isEmpty { parts.append("[verb]") parts.append(verbStr + "\n") } - let adjStr = self.parseResult(result: self.adjective); - if (!adjStr.isEmpty) { + let adjStr = self.parseResult(result: self.adjective) + if !adjStr.isEmpty { parts.append("[adj]") parts.append(adjStr + "\n") } - let advStr = self.parseResult(result: self.adverb); - if (!advStr.isEmpty) { + let advStr = self.parseResult(result: self.adverb) + if !advStr.isEmpty { parts.append("[adv]") parts.append(advStr + "\n") } - + return parts.joined(separator: "\n") } - + var errorMessage: String? { return nil } @@ -80,26 +80,26 @@ let BigHugeThesaurusProviderName = "BigHugeThesaurus" class BigHugeThesaurusProvider: BaseProvider { static let shared = BigHugeThesaurusProvider() - + let name = BigHugeThesaurusProviderName let delay: DispatchTimeInterval = .microseconds(250) let apiUrl = "https://words.bighugelabs.com/api/2" - + var ak = Defaults.shared.BigHugeThesaurusAK.isEmpty ? "" : Defaults.shared.BigHugeThesaurusAK var sk = Defaults.shared.BigHugeThesaurusSK.isEmpty ? Bundle.main.infoDictionary!["BigHugeThesaurusSK"] as! String : Defaults.shared.BigHugeThesaurusSK - - func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) { let word = String(source.firstWord ?? "") - if (word.isEmpty) { + if word.isEmpty { return } - + AF.request("\(apiUrl)/\(sk)/\(word)/json", method: .get) .cacheResponse(using: .cache) .responseDecodable(of: BigHugeThesaurusResponse.self) { response in - if (response.error != nil) { + if response.error != nil { cb(response.error!.errorDescription!, nil, nil) - } else if (response.value?.errorMessage != nil) { + } else if response.value?.errorMessage != nil { cb(response.value!.errorMessage!, nil, nil) } else { cb(response.value!.target!, from, to) @@ -107,6 +107,3 @@ class BigHugeThesaurusProvider: BaseProvider { } } } - - - diff --git a/liltr/Utils/Provider/NiuTrans.swift b/liltr/Utils/Provider/NiuTrans.swift index 316b8c8..06910e3 100644 --- a/liltr/Utils/Provider/NiuTrans.swift +++ b/liltr/Utils/Provider/NiuTrans.swift @@ -8,13 +8,13 @@ struct NiuTransResponse: BaseResponse { let from: String let to: String let src_text: String? - + var target: String? { return tgt_text } - + var errorMessage: String? { - if (error_msg != nil && !error_msg!.isEmpty) { + if error_msg != nil && !error_msg!.isEmpty { return "\(error_code!): \(error_msg!)" } return nil @@ -25,30 +25,30 @@ let NiuTransProviderName = "NiuTrans" class NiuTransProvider: BaseProvider { static let shared = NiuTransProvider() - + let name = NiuTransProviderName let delay: DispatchTimeInterval = .microseconds(250) let apiUrl = "https://api.niutrans.com/NiuTransServer/translation" - + var ak = Defaults.shared.NiuTransAK.isEmpty ? "" : Defaults.shared.NiuTransAK var sk = Defaults.shared.NiuTransSK.isEmpty ? Bundle.main.infoDictionary!["NiuTransSK"] as! String : Defaults.shared.NiuTransSK - - func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) { let parameters: [String: String] = [ "apikey": sk, "src_text": source, "from": from.shortCode, - "to": to.shortCode, + "to": to.shortCode ] - + debugPrint("[NiuTransProvider] parameters:", parameters) - + AF.request(apiUrl, method: .post, parameters: parameters, encoding: JSONEncoding.default) .cacheResponse(using: .cache) .responseDecodable(of: NiuTransResponse.self) { response in - if (response.error != nil) { + if response.error != nil { cb(response.error!.errorDescription!, nil, nil) - } else if (response.value?.errorMessage != nil) { + } else if response.value?.errorMessage != nil { cb(response.value!.errorMessage!, nil, nil) } else { cb(response.value!.target!, from, to) @@ -56,6 +56,3 @@ class NiuTransProvider: BaseProvider { } } } - - - diff --git a/liltr/Utils/Provider/ProviderManager.swift b/liltr/Utils/Provider/ProviderManager.swift index 680ba3e..88b0466 100644 --- a/liltr/Utils/Provider/ProviderManager.swift +++ b/liltr/Utils/Provider/ProviderManager.swift @@ -3,8 +3,8 @@ import Foundation protocol BaseProvider: ObservableObject { var delay: DispatchTimeInterval { get } var name: String { get } - - func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) } protocol BaseResponse: Decodable { @@ -13,8 +13,7 @@ protocol BaseResponse: Decodable { } let PROVIDER_ARRAY: [any BaseProvider] = [NiuTransProvider.shared, BaiduProvider.shared, VolcengineProvider.shared, AliProvider.shared, AppleDictionaryProvider.shared, BigHugeThesaurusProvider.shared] -let PROVIDER_DICT: [String: any BaseProvider] = Dictionary(uniqueKeysWithValues: PROVIDER_ARRAY.map{ ($0.name, $0) }) - +let PROVIDER_DICT: [String: any BaseProvider] = Dictionary(uniqueKeysWithValues: PROVIDER_ARRAY.map { ($0.name, $0) }) struct ProviderCallbackData { let target: String @@ -22,11 +21,11 @@ struct ProviderCallbackData { let sourceLanguage: Language? let targetLanguage: Language? let providerName: String - + var isDictionary: Bool { return providerName == AppleDictionaryProviderName } - + init(_ result: String, _ source: String, sourceLanguage: Language? = nil, targetLanguage: Language? = nil, providerName: String) { self.target = result self.source = source @@ -40,35 +39,35 @@ class ProviderManager: ObservableObject { static let shared = ProviderManager() private var resultCache: [String: ProviderCallbackData] = [:] private var curQuery: String = "" - + @Published var name = PROVIDER_DICT[Defaults.shared.primaryProvider]!.name @Published var usePrimary = true @Published var isTranslating = false - + init() {} - + var provider: any BaseProvider { return usePrimary ? PROVIDER_DICT[Defaults.shared.primaryProvider]! : PROVIDER_DICT[Defaults.shared.secondaryProvider]! } - + func switchProvider() { usePrimary = !usePrimary name = provider.name } - + func translate(_ source: String, _ targetLanguage: Language?, _ cb: @escaping (_ data: ProviderCallbackData) -> Void) { var cur = targetLanguage == nil ? AppleDictionaryProvider.shared : provider let transformedSource = Defaults.shared.preProcessSource ? TextHandler.handle(source) : source - if (transformedSource.isEmpty) { + if transformedSource.isEmpty { return } // if (regexMatched(transformedSource, "^\\b\\w+\\b$")) { // cur = AppleDictionaryProvider.shared // } let query = "\(CryptoEncoder.base64(string: source))_\(targetLanguage?.code ?? "nil")_\(cur.name)_\(cur.name == AppleDictionaryProviderName ? Defaults.shared.dictionary : "nil")" - + func _callback(_ target: String, _ _sourceLanguage: Language? = nil, _ _targetLanguage: Language? = nil) { - if (query == curQuery) { + if query == curQuery { let data = ProviderCallbackData(target, source, sourceLanguage: _sourceLanguage, targetLanguage: _targetLanguage?.name == _sourceLanguage?.name ? nil : _targetLanguage, providerName: cur.name) cb(data) resultCache[query] = data @@ -76,29 +75,29 @@ class ProviderManager: ObservableObject { curQuery = "" } } - - if (resultCache[query] != nil) { + + if resultCache[query] != nil { cb(resultCache[query]!) return } - + isTranslating = true curQuery = query - + let fromTo = LanguageManager.getFromTo(transformedSource, targetLanguage) - if (fromTo == nil) { + if fromTo == nil { _callback("Source text can not be recognized") return } let (from, to) = fromTo! - + debugPrint("[ProviderManager#translate]", [ "name": cur.name, "from": from.code, "to": to.code, "source": transformedSource ]) - + return cur.translate(source: transformedSource, from: from, to: to, cb: _callback) } } diff --git a/liltr/Utils/Provider/Volcengine.swift b/liltr/Utils/Provider/Volcengine.swift index f83f52f..f102d5b 100644 --- a/liltr/Utils/Provider/Volcengine.swift +++ b/liltr/Utils/Provider/Volcengine.swift @@ -24,21 +24,21 @@ struct VolcengineTranslation: Decodable { struct VolcengineResponse: BaseResponse { let ResponseMetadata: VolcengineResponseMetadata let TranslationList: [VolcengineTranslation]? - + var target: String? { - if (TranslationList?.isEmpty == false) { + if TranslationList?.isEmpty == false { var result: [String] = [] for item in TranslationList! { result.append(item.Translation) } return result.joined(separator: "\n") } - + return nil } - + var errorMessage: String? { - if (ResponseMetadata.Error?.Message != nil) { + if ResponseMetadata.Error?.Message != nil { return "\(ResponseMetadata.Error!.Code!): \(ResponseMetadata.Error!.Message!)" } return nil @@ -49,27 +49,27 @@ let VolcengineProviderName = "Volcengine" class VolcengineProvider: BaseProvider { static let shared = VolcengineProvider() - + let name = VolcengineProviderName let delay: DispatchTimeInterval = .microseconds(300) var apiUrl: String { return "https://\(self.host)\(self.uri)?\(self.queryString)" } - + var ak: String { return Defaults.shared.VolcengineAK.isEmpty ? Bundle.main.infoDictionary!["VolcengineAK"] as! String : Defaults.shared.VolcengineAK } - + var sk: String { return Defaults.shared.VolcengineSK.isEmpty ? Bundle.main.infoDictionary!["VolcengineSK"] as! String : Defaults.shared.VolcengineSK } - + private let host = "translate.volcengineapi.com" private let uri = "/" private let queryString = "Action=TranslateText&Version=2020-06-01" private let region = "cn-north-1" private let service = "translate" - + private func _getSignedHeaders(headers: [String: String]) -> String { var result: [String] = [] for (key, _) in headers { @@ -78,7 +78,7 @@ class VolcengineProvider: BaseProvider { result = result.sorted { $0 < $1 } return result.joined(separator: ";") } - + private func _getCanonicalHeaders(headers: [String: String]) -> String { var result: [String] = [] for (key, value) in headers { @@ -87,7 +87,7 @@ class VolcengineProvider: BaseProvider { result = result.sorted { $0 < $1 } return result.joined(separator: "\n") + "\n" } - + private func _getXDate() -> String { let date = Date() let dateFormatter = DateFormatter() @@ -96,14 +96,14 @@ class VolcengineProvider: BaseProvider { let xDate = dateFormatter.string(from: date) return xDate } - + private func _sign(contentHashed: String, headers: [String: String]) -> String { // step 1 let method = "POST" let canonicalHeaders = _getCanonicalHeaders(headers: headers) let signedHeaders = _getSignedHeaders(headers: headers) let canoicalRequest = [method, uri, queryString, canonicalHeaders, signedHeaders, contentHashed].joined(separator: "\n") - + // step 2 let algorithm = "HMAC-SHA256" let xDate = headers["X-Date"]! @@ -111,7 +111,7 @@ class VolcengineProvider: BaseProvider { let credentialScope = [shortDate, region, service, "request"].joined(separator: "/") let canonicalRequestHashed = _hashSha256(content: canoicalRequest) let stringToSign = [algorithm, xDate, credentialScope, canonicalRequestHashed].joined(separator: "\n") - + // step 3 let kDate = _hmacSha256(sk.data(using: .utf8)!, shortDate) let kRegion = _hmacSha256(kDate, region) @@ -119,55 +119,52 @@ class VolcengineProvider: BaseProvider { let kSigning = _hmacSha256(kService, "request") let signature = CryptoEncoder.data2str(_hmacSha256(kSigning, stringToSign)) let authorization = "HMAC-SHA256 Credential=\(ak)/\(credentialScope), SignedHeaders=\(signedHeaders), Signature=\(signature)" - + return authorization } - + func _hmacSha256(_ key: Data, _ content: String) -> Data { let hmac = HMAC.authenticationCode(for: content.data(using: .utf8)!, using: SymmetricKey(data: key)) return Data(hmac) } - + func _hashSha256(content: String) -> String { let digest = SHA256.hash(data: content.data(using: .utf8)!) return digest.compactMap { String(format: "%02x", $0) }.joined() } - - func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) -> Void { + + func translate(source: String, from: Language, to: Language, cb: @escaping (_ target: String, _ sourceLanguage: Language?, _ targetLanguage: Language?) -> Void) { let parameters: [String: Any] = [ "SourceLanguage": from.shortCode, "TargetLanguage": to.shortCode, - "TextList": source.split(separator: "\n"), + "TextList": source.split(separator: "\n") ] - + let date = _getXDate() let contentHashed = _hashSha256(content: String(data: try! JSONSerialization.data(withJSONObject: parameters, options: []), encoding: .utf8)!) - + var headers = [ "Content-Type": "application/json", "Host": host, - "X-Date": date, + "X-Date": date ] - + let authorization = _sign(contentHashed: contentHashed, headers: headers) headers.updateValue(authorization, forKey: "Authorization") debugPrint("[VolcengineProvider] parameters:", parameters, headers) - + AF.request(apiUrl, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: dict2headers(dict: headers)) .cacheResponse(using: .cache) .responseDecodable(of: VolcengineResponse.self) { response in - if (response.error != nil) { + if response.error != nil { cb(response.error!.errorDescription!, nil, nil) - } else if (response.value?.errorMessage != nil) { + } else if response.value?.errorMessage != nil { cb(response.value!.errorMessage!, nil, nil) } else { cb(response.value!.target!, from, to) } } } - -} - - +} diff --git a/liltr/Utils/SchemeURL/SchemeURLManager.swift b/liltr/Utils/SchemeURL/SchemeURLManager.swift index a969887..9b83ca8 100644 --- a/liltr/Utils/SchemeURL/SchemeURLManager.swift +++ b/liltr/Utils/SchemeURL/SchemeURLManager.swift @@ -7,7 +7,7 @@ enum SchemeAction: String { } class SchemeURLManager { - static func getUrlByAction(_ action: SchemeAction, querys: Dictionary = [:]) -> URL { + static func getUrlByAction(_ action: SchemeAction, querys: [String: String] = [:]) -> URL { var components = URLComponents() components.scheme = APP_NAME components.host = action.rawValue @@ -16,7 +16,7 @@ class SchemeURLManager { value: $1.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ) } - + return components.url! } } diff --git a/liltr/Utils/TextHandler/TextHandler.swift b/liltr/Utils/TextHandler/TextHandler.swift index f15e003..becf4ba 100644 --- a/liltr/Utils/TextHandler/TextHandler.swift +++ b/liltr/Utils/TextHandler/TextHandler.swift @@ -4,36 +4,36 @@ class TextHandler { private static func _removeWhiteSpacesFromLine(_ line: String) -> String { return line.trimmingCharacters(in: .whitespaces) } - + private static func _removeCommentsFromLine(_ line: String) -> String { let trimedLine = _removeWhiteSpacesFromLine(line) - if (trimedLine.hasPrefix("/") || trimedLine.hasPrefix("*")) { + if trimedLine.hasPrefix("/") || trimedLine.hasPrefix("*") { return _removeCommentsFromLine(String(trimedLine.dropFirst())) } else { return trimedLine } } - + private static func _transformCamelCase(_ word: String) -> String { - if (word == word.uppercased()) { + if word == word.uppercased() { return word } - + let pattern = "(?<=.)([A-Z])" let regex = try? NSRegularExpression(pattern: pattern, options: []) let range = NSRange(location: 0, length: word.utf16.count) let result = regex?.stringByReplacingMatches(in: word, options: [], range: range, withTemplate: " $1") return result?.lowercased() ?? word } - + private static func _transformSnakeCase(_ word: String) -> String { return word.replacingOccurrences(of: "_", with: " ") } - + private static func _transformWord(_ word: String) -> String { return _transformSnakeCase(_transformCamelCase(word)) } - + private static func _transformLine(_ line: String) -> String { let trimedLine = _removeCommentsFromLine(line) let words = trimedLine.split(separator: " ").map { word in @@ -41,21 +41,21 @@ class TextHandler { } return words.joined(separator: " ") } - + static func handle(_ content: String) -> String { let originalLines = content.split(separator: "\n").filter { line in return !line.isEmpty } - + let lines = originalLines.map { line in return _transformLine(String(line)) } - + var separator = " " - if (originalLines.first != nil && lines.first != nil && originalLines.first! == lines.first!) { + if originalLines.first != nil && lines.first != nil && originalLines.first! == lines.first! { separator = "\n" } - + return lines.filter { !$0.isEmpty }.joined(separator: separator) } } diff --git a/liltr/Utils/Window/WindowManager.swift b/liltr/Utils/Window/WindowManager.swift index 0557c90..3a8516a 100644 --- a/liltr/Utils/Window/WindowManager.swift +++ b/liltr/Utils/Window/WindowManager.swift @@ -4,41 +4,41 @@ import SwiftUI enum WindowID: String, Identifiable { case translate case settings - + var id: String { self.rawValue } } class WindowManager { static func getById(_ id: WindowID) -> NSWindow? { let window = NSApp.windows.first(where: { $0.identifier?.rawValue == id.rawValue}) - if (window == nil) { + if window == nil { debugPrint("[WindowManager] get window \(id) failed") } return window } - + static func float(id: WindowID, enable: Bool) { let window = getById(id) - if (window != nil) { + if window != nil { window!.level = enable ? .floating : .normal } } - + static func open(openWindow: OpenWindowAction, id: WindowID) { NSApplication.shared.activate(ignoringOtherApps: true) openWindow(id: id.rawValue) let window = getById(id) - if (window != nil) { + if window != nil { window!.makeKeyAndOrderFront(nil) window!.orderFrontRegardless() window!.setIsVisible(true) NSApplication.shared.activate(ignoringOtherApps: true) } } - + static func setSize(id: WindowID, width: CGFloat, height: CGFloat) { let window = getById(id) - if (window != nil) { + if window != nil { let origin = window!.frame.origin window!.setFrame(NSRect(x: origin.x, y: origin.y + window!.frame.height - height, width: width, height: height), display: true, animate: false) } diff --git a/liltr/Utils/extensions.swift b/liltr/Utils/extensions.swift index 91f074a..38a2418 100644 --- a/liltr/Utils/extensions.swift +++ b/liltr/Utils/extensions.swift @@ -25,7 +25,6 @@ extension Color { } } - extension String: Error {} extension StringProtocol { @@ -33,7 +32,7 @@ extension StringProtocol { var byLines: [SubSequence] { components(separated: .byLines) } var byWords: [SubSequence] { components(separated: .byWords) } - func components(separated options: String.EnumerationOptions)-> [SubSequence] { + func components(separated options: String.EnumerationOptions) -> [SubSequence] { var components: [SubSequence] = [] enumerateSubstrings(in: startIndex..., options: options) { _, range, _, _ in components.append(self[range]) } return components @@ -58,12 +57,12 @@ extension StringProtocol { } extension View { - func bottomFade(fadeLength:CGFloat = 20) -> some View { + func bottomFade(fadeLength: CGFloat = 20) -> some View { return mask( VStack(spacing: 0) { - + Rectangle().fill(Color.backgroundColor) - + LinearGradient(gradient: Gradient( colors: [Color.backgroundColor.opacity(0), Color.backgroundColor]), startPoint: .bottom, endPoint: .top @@ -79,7 +78,7 @@ extension View { if #available(macOS 13.0, *) { return self.onContinuousHover { phase in switch phase { - case .active(_): + case .active: cursor.push() case .ended: NSCursor.pop() @@ -97,7 +96,7 @@ extension View { } } -//extension WindowGroup { +// extension WindowGroup { // init(_ titleKey: LocalizedStringKey, uniqueWindow: W, @ViewBuilder content: @escaping () -> C) // where W.ID == String, Content == PresentedWindowContent { // self.init(titleKey, id: uniqueWindow.id, for: String.self) { _ in @@ -106,10 +105,10 @@ extension View { // uniqueWindow.id // } // } -//} +// } // -//extension OpenWindowAction { +// extension OpenWindowAction { // func callAsFunction(_ window: W) where W.ID == String { // self.callAsFunction(id: window.id, value: window.id) // } -//} +// } diff --git a/liltr/Utils/views.swift b/liltr/Utils/views.swift index 3351a96..639c8fb 100644 --- a/liltr/Utils/views.swift +++ b/liltr/Utils/views.swift @@ -4,42 +4,42 @@ struct BlurWindow: NSViewRepresentable { func updateNSView(_ nsView: NSVisualEffectView, context: Context) { // } - + func makeNSView(context: Context) -> NSVisualEffectView { let view = NSVisualEffectView() view.blendingMode = .behindWindow - + return view } } class SizeHolder { private var base: Float - + var fontSize: Float { return base * 6 } - + var iconSize: Float { return fontSize * 1.5 } - + var radiusSize: Float { return fontSize / 2 } - + var innerGapSize: Float { return base } - + var gapSize: Float { return base * 2 } - + var outerGapSize: Float { return base * 4 } - + init(base: Float? = nil) { self.base = base ?? 2 } diff --git a/liltr/appDelegate.swift b/liltr/appDelegate.swift index 8066711..595beb3 100644 --- a/liltr/appDelegate.swift +++ b/liltr/appDelegate.swift @@ -2,26 +2,24 @@ import AppKit import UserNotifications import SwiftUI -class AppDelegate: NSObject, NSApplicationDelegate { +class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { UNUserNotificationCenter.current().delegate = self } } - extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.banner, .badge, .sound]) } - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let content = response.notification.request.content if content.categoryIdentifier == "translate" { switch response.actionIdentifier { case "copy": - copyToPasteboard(content.body) + PasteboardManager.shared.copy(content.body) break case "speak": let language = LanguageManager.getLanguageByContent(content.body) diff --git a/liltr/liltrApp.swift b/liltr/liltrApp.swift index 295142a..11093be 100644 --- a/liltr/liltrApp.swift +++ b/liltr/liltrApp.swift @@ -11,13 +11,13 @@ struct liltrApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @Default(\.menuIconSymbol) var menuIconSymbol private let updaterController: SPUStandardUpdaterController - + init() { updaterController = SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil) } - + var body: some Scene { - MenuBarExtra() { + MenuBarExtra { AppMenu() } label: { let imageDefinedByUser = NSImage(systemSymbolName: menuIconSymbol, accessibilityDescription: APP_NAME) @@ -29,7 +29,7 @@ struct liltrApp: App { }(NSImage(named: "monochrome.fill")!) Image(nsImage: imageDefinedByUser ?? imageDefault) } - + Window("Translate", id: WindowID.translate.id) { TranslateView() .frame(minWidth: 220, minHeight: 300) @@ -37,8 +37,7 @@ struct liltrApp: App { .defaultSize(width: 300, height: 500) .windowStyle(.hiddenTitleBar) .handlesExternalEvents(matching: Set(arrayLiteral: SchemeAction.translateInWindow.rawValue)) - - + Window("Settings", id: WindowID.settings.id) { SettingsView(updater: updaterController.updater) .ignoresSafeArea(edges: .top) @@ -58,18 +57,17 @@ struct AppMenu: View { @Default(\.primaryLanguage) var primaryLanguage @Default(\.hotKeyTriggerInNotification) var hotKeyTriggerInNotification @Default(\.preProcessSource) var preProcessSource - - + private func _translateInNotification(text: String) { ProviderManager.shared.translate(text, LanguageManager.getLanguageByCode(primaryLanguage)!) { data in pushNotification(title: data.source, body: data.target) } } - + private func _gotoTranslate(text: String? = nil) { openURL(SchemeURLManager.getUrlByAction(SchemeAction.translateInWindow, querys: ["src": text ?? ""])) } - + private func _gotoSettings() { openURL(SchemeURLManager.getUrlByAction(SchemeAction.settings)) } @@ -77,55 +75,55 @@ struct AppMenu: View { private func _quit() { NSApplication.shared.terminate(nil) } - + // MARK: hotkey private func _onHotKeyTranslate() { - let text = getSelectedText() ?? "" - - if (!text.isEmpty && Defaults.shared.hotKeyTriggerInNotification) { - _translateInNotification(text: text) - } else { - _gotoTranslate(text: text) + SelectedTextManager.shared.getText { text, _ in + if text != nil && !text!.isEmpty && Defaults.shared.hotKeyTriggerInNotification { + _translateInNotification(text: text!) + } else { + _gotoTranslate(text: text ?? "") + } } } - + private func _onHotKeyOCR() { OCRManager.shared.captureWithOCR { text in - if (Defaults.shared.hotKeyTriggerInNotification) { + if Defaults.shared.hotKeyTriggerInNotification { _translateInNotification(text: text) } else { _gotoTranslate(text: text) } } } - + init() { KeyboardShortcuts.onKeyUp(for: .translate, action: _onHotKeyTranslate) KeyboardShortcuts.onKeyUp(for: .ocr, action: _onHotKeyOCR) } - + var body: some View { VStack { Button(action: _onHotKeyTranslate, label: { Text("Translate") }) .keyboardShortcut(string2Shortcut(hotKey)) - + Button(action: _onHotKeyOCR, label: { Text("OCR Translate") }) .keyboardShortcut(string2Shortcut(ocrHotKey)) - + Button(action: _gotoSettings, label: { Text("Settings...") }) - + Divider() - + Toggle(isOn: $hotKeyTriggerInNotification, label: { Text("In-Notification Mode") }).toggleStyle(.checkbox) - + Toggle(isOn: $preProcessSource, label: { Text("Preprocess Source Text") }).toggleStyle(.checkbox) - + Divider() - + Button(action: _quit, label: { Text("Quit") }) .keyboardShortcut("q") } diff --git a/liltr/userDefaults.swift b/liltr/userDefaults.swift index 12ea4bd..f16093e 100644 --- a/liltr/userDefaults.swift +++ b/liltr/userDefaults.swift @@ -5,16 +5,16 @@ public class Defaults: ObservableObject { @AppStorage("launchAtLogin") public var launchAtLogin = false @AppStorage("menuIconSymbol") public var menuIconSymbol = "" @AppStorage("floatOnTop") public var floatOnTop = false - + // MARK: HotKey @AppStorage("hotKey") public var hotKey = "" @AppStorage("ocrHotKey") public var ocrHotKey = "" @AppStorage("hotKeyTriggerInNotification") public var hotKeyTriggerInNotification = true - + // MARK: Language @AppStorage("primaryLanguage") public var primaryLanguage = LANGUAGE_ARRAY[0].code @AppStorage("secondaryLanguage") public var secondaryLanguage = LANGUAGE_ARRAY[1].code - + // MARK: Provider @AppStorage("primaryProvider") public var primaryProvider = NiuTransProviderName @AppStorage("secondaryProvider") public var secondaryProvider = VolcengineProviderName @@ -33,10 +33,10 @@ public class Defaults: ObservableObject { // big huge @AppStorage("\(BigHugeThesaurusProviderName)AK") public var BigHugeThesaurusAK = "" @AppStorage("\(BigHugeThesaurusProviderName)SK") public var BigHugeThesaurusSK = "" - + // MARK: Dictionary @AppStorage("dictionary") public var dictionary = DCSOxfordDictionaryOfEnglish - + // MARK: Advanced @AppStorage("preProcessSource") public var preProcessSource = true