diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index dd7aefbb4..41e4ce22e 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -229,6 +229,7 @@ 03FD68BB2B1DC59600FD388E /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03FD68BA2B1DC59600FD388E /* CryptoSwift */; }; 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */; }; 0A057D6D2B499A000025C51D /* ServiceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A057D6C2B499A000025C51D /* ServiceTab.swift */; }; + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */; }; 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */; }; 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */; }; 0A8685C82B552A590022534F /* DisabledTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8685C72B552A590022534F /* DisabledTab.swift */; }; @@ -705,6 +706,7 @@ 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+EncryptAES.swift"; sourceTree = ""; }; 06E15747A7BD34D510ADC6A8 /* Pods-Easydict.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.debug.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.debug.xcconfig"; sourceTree = ""; }; 0A057D6C2B499A000025C51D /* ServiceTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTab.swift; sourceTree = ""; }; + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppInfo.swift"; sourceTree = ""; }; 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+DidSet.swift"; sourceTree = ""; }; 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name.swift"; sourceTree = ""; }; 0A8685C72B552A590022534F /* DisabledTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledTab.swift; sourceTree = ""; }; @@ -1823,6 +1825,7 @@ 03CF88602B137ECB0030C199 /* Swift */ = { isa = PBXGroup; children = ( + 0A2A05A42B59755F00EEA142 /* Bundle */, 0A2BA9622B4A3CBB002872A4 /* Notification */, 0A2BA95E2B49A967002872A4 /* Binding */, 03FD68BC2B1E14B500FD388E /* String */, @@ -2003,6 +2006,14 @@ path = String; sourceTree = ""; }; + 0A2A05A42B59755F00EEA142 /* Bundle */ = { + isa = PBXGroup; + children = ( + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */, + ); + path = Bundle; + sourceTree = ""; + }; 0A2BA95E2B49A967002872A4 /* Binding */ = { isa = PBXGroup; children = ( @@ -2656,6 +2667,7 @@ 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */, 03BDA7BF2A26DA280079D04F /* NSScanner+EscapedScanning.m in Sources */, 03542A4C2937B5F100C34C33 /* EZYoudaoTranslate.m in Sources */, + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */, 037852B329583F5200D0E2CF /* EZServiceCell.m in Sources */, 03247E362968158B00AFCD67 /* EZScriptExecutor.m in Sources */, 03882F8E29D95044005B5A52 /* ToastWindowController.m in Sources */, diff --git a/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift new file mode 100644 index 000000000..8e99cb95a --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift @@ -0,0 +1,23 @@ +// +// Bundle+AppInfo.swift +// Easydict +// +// Created by phlpsong on 2024/1/18. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Bundle { + var applicationName: String { + if let displayName: String = infoDictionary?["CFBundleDisplayName"] as? String { + return displayName + } else if let name: String = infoDictionary?["CFBundleName"] as? String { + return name + } + if let executableURL { + return executableURL.deletingLastPathComponent().lastPathComponent + } + return "" + } +} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index 7667688d1..cebd339d0 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -31,9 +31,9 @@ struct SettingView: View { .tabItem { Label("service", systemImage: "briefcase") } .tag(SettingTab.service) - DisabledTab() + DisabledAppTab() .tabItem { Label("disabled_app_list", systemImage: "nosign") } - .tag(SettingTab.disabled.rawValue) + .tag(SettingTab.disabled) PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } @@ -59,7 +59,7 @@ struct SettingView: View { let originalFrame = window.frame let newSize = switch selection { - case .general, .privacy, .about: + case .general, .privacy, .about, .disabled: CGSize(width: 500, height: 520) case .service: CGSize(width: 800, height: 520) diff --git a/Easydict/NewApp/View/SettingView/Tabs/DisabledTab.swift b/Easydict/NewApp/View/SettingView/Tabs/DisabledTab.swift index f0c42b8a9..12e5403e8 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/DisabledTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/DisabledTab.swift @@ -9,10 +9,10 @@ import Combine import SwiftUI -class DisableViewModel: ObservableObject { +class DisabledAppViewModel: ObservableObject { @Published var appModelList: [EZAppModel] = [] - @Published var selectedAppModel: EZAppModel? = nil + @Published var selectedAppModels: Set = [] @Published var isImporting = false @@ -20,19 +20,24 @@ class DisableViewModel: ObservableObject { appModelList = EZLocalStorage.shared().selectTextTypeAppModelList } - func removeDisabledApp() { - appModelList = appModelList.filter { $0.appBundleID != selectedAppModel?.appBundleID } + func saveDisabledApps() { EZLocalStorage.shared().selectTextTypeAppModelList = appModelList } - func newAppSelected(for url: URL) { - guard let newSelectApp = newBlockApps(url: url) else { return } + func removeDisabledApp() { + appModelList = appModelList.filter { !selectedAppModels.contains($0) } + saveDisabledApps() + selectedAppModels = [] + } + func newAppSelected(for url: URL) { + guard let newSelectApp = newDisabledApp(from: url) else { return } + guard !appModelList.contains(newSelectApp) else { return } appModelList.append(newSelectApp) - EZLocalStorage.shared().selectTextTypeAppModelList = appModelList + saveDisabledApps() } - func newBlockApps(url: URL) -> EZAppModel? { + func newDisabledApp(from url: URL) -> EZAppModel? { let appModel = EZAppModel() guard let bundle = Bundle(url: url) else { return nil } appModel.appBundleID = bundle.bundleIdentifier ?? "" @@ -42,45 +47,56 @@ class DisableViewModel: ObservableObject { } @available(macOS 13.0, *) -struct DisabledTab: View { - - @ObservedObject var viewModel = DisableViewModel() +struct DisabledAppTab: View { + @Environment(\.colorScheme) private var colorScheme - var appListView: some View { - VStack(spacing: 0) { - List { - ForEach(viewModel.appModelList, id: \.self) { app in - BlockAppItemView(with: app) - .tag(app) - .environmentObject(viewModel) - } - .listRowSeparator(.hidden) - } - .listStyle(.plain) - - ListToolbar() - .environmentObject(viewModel) - .fileImporter( - isPresented: $viewModel.isImporting, - allowedContentTypes: [.application], - allowsMultipleSelection: true - ) { result in - switch result { - case let .success(urls): - urls.forEach { url in - let gotAccess = url.startAccessingSecurityScopedResource() - if !gotAccess { return } - viewModel.newAppSelected(for: url) - url.stopAccessingSecurityScopedResource() - } - case let .failure(error): - print("error: \(error)") + @ObservedObject var disabledAppViewModel = DisabledAppViewModel() + + var listToolbar: some View { + ListToolbar() + .fileImporter( + isPresented: $disabledAppViewModel.isImporting, + allowedContentTypes: [.application], + allowsMultipleSelection: true + ) { result in + switch result { + case let .success(urls): + urls.forEach { url in + let gotAccess = url.startAccessingSecurityScopedResource() + if !gotAccess { return } + disabledAppViewModel.newAppSelected(for: url) + url.stopAccessingSecurityScopedResource() } + case let .failure(error): + print("fileImporter error: \(error)") } + } + } + + var appListView: some View { + List(selection: $disabledAppViewModel.selectedAppModels) { + ForEach(disabledAppViewModel.appModelList, id: \.self) { app in + BlockAppItemView(with: app) + .tag(app) + } + .listRowSeparator(.hidden) + } + .listStyle(.plain) + .scrollIndicators(.never) + } + + var appListViewWithToolbar: some View { + VStack(spacing: 0) { + appListView + + listToolbar } - .frame(maxWidth: 420) .clipShape(RoundedRectangle(cornerRadius: 8)) - .padding([.bottom]) + .padding(.bottom) + .padding(.horizontal, 40) + .onTapGesture { + disabledAppViewModel.selectedAppModels = [] + } } var body: some View { @@ -88,41 +104,37 @@ struct DisabledTab: View { Text("disabled_title") .padding() - appListView + appListViewWithToolbar } + .environmentObject(disabledAppViewModel) .task { - viewModel.fetchDisabledApps() + disabledAppViewModel.fetchDisabledApps() } } } @available(macOS 13.0, *) struct ListToolbar: View { - @Environment(\.colorScheme) private var colorScheme - - @EnvironmentObject var viewModel: DisableViewModel - - var bgColor: Color { - Color(nsColor: colorScheme == .light ? NSColor.controlBackgroundColor : NSColor.controlBackgroundColor) - } + @EnvironmentObject private var disabledAppViewModel: DisabledAppViewModel var body: some View { VStack(spacing: 0) { Divider() HStack(spacing: 0) { ListButton(imageName: "plus") { - viewModel.isImporting.toggle() + disabledAppViewModel.isImporting.toggle() } Divider() ListButton(imageName: "minus") { - viewModel.removeDisabledApp() + disabledAppViewModel.removeDisabledApp() } + .disabled(disabledAppViewModel.selectedAppModels.isEmpty) Spacer() } .padding(2) } .frame(height: 28) - .background(bgColor) + .background(Color(.controlBackgroundColor)) } } @@ -136,36 +148,37 @@ struct ListButton: View { action() }) { Image(systemName: imageName) + .resizable() + .scaledToFit() + .frame(width: 12, height: 12) + .padding(6) + .contentShape(Rectangle()) } .buttonStyle(BorderlessButtonStyle()) - .contentShape(Rectangle()) - .frame(width: 24, height: 24) } } @available(macOS 13.0, *) struct BlockAppItemView: View { - var app: EZAppModel - - @ObservedObject var appFetcher: AppFetcher + @StateObject private var appFetcher: AppFetcher - @Environment(\.colorScheme) private var colorScheme + @EnvironmentObject var disabledAppViewModel: DisabledAppViewModel - @EnvironmentObject var viewModel: DisableViewModel - - private var tableColor: Color { - Color(nsColor: colorScheme == .light ? .ez_tableRowViewBgLight() : .ez_tableRowViewBgDark()) + private var listRowBgColor: Color { + disabledAppViewModel.selectedAppModels.contains { + $0.appBundleID == appFetcher.appModel.appBundleID + } ? Color("service_cell_highlight") : .clear } init(with appModel: EZAppModel) { - app = appModel - appFetcher = AppFetcher(appBundleId: app.appBundleID) + _appFetcher = StateObject(wrappedValue: AppFetcher(appModel: appModel)) } var body: some View { HStack(alignment: .center) { Image(nsImage: appFetcher.appIcon ?? NSImage()) .resizable() + .scaledToFit() .frame(width: 24, height: 24) Text(appFetcher.appName) @@ -175,67 +188,41 @@ struct BlockAppItemView: View { .frame(maxWidth: .infinity) .contentShape(Rectangle()) .padding(.vertical, 4) - .overlay { - TapHandler { - let selectedAppModel = viewModel.selectedAppModel - if selectedAppModel == nil || selectedAppModel != app { - viewModel.selectedAppModel = app - } else { - viewModel.selectedAppModel = nil - } - } + .padding(.leading, 6) + .task { + appFetcher.getAppBundleInfo() } - .listRowBackground(viewModel.selectedAppModel == app ? Color("service_cell_highlight") : .clear) } } @available(macOS 13.0, *) class AppFetcher: ObservableObject { @Published var appIcon: NSImage? = nil + @Published var appName = "" - init(appBundleId: String) { - let workspace = NSWorkspace.shared - guard let appURL = workspace.urlForApplication(withBundleIdentifier: appBundleId) else { - return - } - print("path: \(appURL.path())") - appIcon = getApplicationIcon(forAppBundleIdentifier: appBundleId) + var appModel: EZAppModel - guard let appBundle = Bundle(url: appURL) else { - return - } - appName = appBundle.applicationName + init(appModel: EZAppModel) { + self.appModel = appModel } - func getApplicationIcon(forAppBundleIdentifier bundleIdentifier: String) -> NSImage? { + func getAppBundleInfo() { + let appBundleId = appModel.appBundleID let workspace = NSWorkspace.shared - - // If the app is not running, try to get the icon from the app bundle - if let appPath = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) { - let icon = workspace.icon(forFile: appPath.path(percentEncoded: false)) - return icon - } - - return nil - } -} - -extension Bundle { - var applicationName: String { - if let displayName: String = infoDictionary?["CFBundleDisplayName"] as? String { - return displayName - } else if let name: String = infoDictionary?["CFBundleName"] as? String { - return name - } - if let executableURL { - return executableURL.deletingLastPathComponent().lastPathComponent - } - return "" + let appURL = workspace.urlForApplication(withBundleIdentifier: appBundleId) + guard let appURL else { return } + + let appPath = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appBundleId) + guard let appPath else { return } + appIcon = workspace.icon(forFile: appPath.path(percentEncoded: false)) + + guard let appBundle = Bundle(url: appURL) else { return } + appName = appBundle.applicationName } } @available(macOS 13.0, *) #Preview { - DisabledTab() + DisabledAppTab() }