From 1f6fdc27ce25060057d6b0aca71eb7719e521f24 Mon Sep 17 00:00:00 2001 From: phlpsong <103433299+phlpsong@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:32:43 +0800 Subject: [PATCH] add disabled app list tab (#340) * feat: add disabled app tab * fix: refactor app list issues * fix: refactor disabled app tab * fix: remove list row bg color * fix: bundle application name issue * fix: update code for review comments * fix: update app item view model * fix: remove item view model * fix: refactor disable app item view * perf: adjust add & minus bg color * perf: add a border for list * perf: adjust custom color name * perf: show minus image color according to enable state * perf: adjust disabled tab UI * fix: add disabled title horizontal padding * fix: code refactor * fix: disabled environment * fix: disable issue --------- Co-authored-by: tisfeng --- Easydict.xcodeproj/project.pbxproj | 23 +- .../App/Assets.xcassets/Colors/Contents.json | 6 + .../add_minus_bg_color.colorset/Contents.json | 38 +++ .../list_border_color.colorset/Contents.json | 38 +++ .../Contents.json | 0 Easydict/App/Easydict-Bridging-Header.h | 1 + Easydict/App/Localizable.xcstrings | 20 ++ .../Utility/Swift/Bundle/Bundle+AppInfo.swift | 23 ++ .../NewApp/View/SettingView/SettingView.swift | 9 +- .../SettingView/Tabs/DisabledAppTab.swift | 231 ++++++++++++++++++ .../View/SettingView/Tabs/ServiceTab.swift | 2 +- 11 files changed, 385 insertions(+), 6 deletions(-) create mode 100644 Easydict/App/Assets.xcassets/Colors/Contents.json create mode 100644 Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json create mode 100644 Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json rename Easydict/App/Assets.xcassets/{service_cell_highlight.colorset => Colors/service_cell_highlight_color.colorset}/Contents.json (100%) create mode 100644 Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift create mode 100644 Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift diff --git a/Easydict.xcodeproj/project.pbxproj b/Easydict.xcodeproj/project.pbxproj index 9bb89217d..c1c23e80f 100644 --- a/Easydict.xcodeproj/project.pbxproj +++ b/Easydict.xcodeproj/project.pbxproj @@ -229,8 +229,10 @@ 03FD68BB2B1DC59600FD388E /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 03FD68BA2B1DC59600FD388E /* CryptoSwift */; }; 03FD68BE2B1E151A00FD388E /* String+EncryptAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */; }; 0A057D6D2B499A000025C51D /* ServiceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A057D6C2B499A000025C51D /* ServiceTab.swift */; }; + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */; }; 0A2BA9602B49A989002872A4 /* Binding+DidSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */; }; 0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */; }; + 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A8685C72B552A590022534F /* DisabledAppTab.swift */; }; 0AC11B222B4D16A500F07198 /* WindowAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */; }; 0AC11B242B4E46B300F07198 /* TapHandlerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */; }; 17BCAEF72B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 17BCAEF52B0DFF9000A7D372 /* EZNiuTransTranslateResponse.m */; }; @@ -704,8 +706,10 @@ 03FD68BD2B1E151A00FD388E /* String+EncryptAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+EncryptAES.swift"; sourceTree = ""; }; 06E15747A7BD34D510ADC6A8 /* Pods-Easydict.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Easydict.debug.xcconfig"; path = "Target Support Files/Pods-Easydict/Pods-Easydict.debug.xcconfig"; sourceTree = ""; }; 0A057D6C2B499A000025C51D /* ServiceTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceTab.swift; sourceTree = ""; }; + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+AppInfo.swift"; sourceTree = ""; }; 0A2BA95F2B49A989002872A4 /* Binding+DidSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+DidSet.swift"; sourceTree = ""; }; 0A2BA9632B4A3CCD002872A4 /* Notification+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Name.swift"; sourceTree = ""; }; + 0A8685C72B552A590022534F /* DisabledAppTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppTab.swift; sourceTree = ""; }; 0AC11B212B4D16A500F07198 /* WindowAccessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAccessor.swift; sourceTree = ""; }; 0AC11B232B4E46B300F07198 /* TapHandlerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapHandlerView.swift; sourceTree = ""; }; 17BCAEF32B0DFF9000A7D372 /* EZNiuTransTranslateResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EZNiuTransTranslateResponse.h; sourceTree = ""; }; @@ -1821,6 +1825,7 @@ 03CF88602B137ECB0030C199 /* Swift */ = { isa = PBXGroup; children = ( + 0A2A05A42B59755F00EEA142 /* Bundle */, 0A2BA9622B4A3CBB002872A4 /* Notification */, 0A2BA95E2B49A967002872A4 /* Binding */, 03FD68BC2B1E14B500FD388E /* String */, @@ -2001,6 +2006,14 @@ path = String; sourceTree = ""; }; + 0A2A05A42B59755F00EEA142 /* Bundle */ = { + isa = PBXGroup; + children = ( + 0A2A05A52B59757100EEA142 /* Bundle+AppInfo.swift */, + ); + path = Bundle; + sourceTree = ""; + }; 0A2BA95E2B49A967002872A4 /* Binding */ = { isa = PBXGroup; children = ( @@ -2077,6 +2090,7 @@ EAED41EA2B54A4900005FE0A /* ServiceConfiguration */, 278540332B3DE04F004E9488 /* GeneralTab.swift */, 0A057D6C2B499A000025C51D /* ServiceTab.swift */, + 0A8685C72B552A590022534F /* DisabledAppTab.swift */, 276742042B3DC230002A2C75 /* PrivacyTab.swift */, 276742052B3DC230002A2C75 /* AboutTab.swift */, ); @@ -2320,7 +2334,7 @@ buildConfigurationList = C99EEB2C2385796900FEE666 /* Build configuration list for PBXNativeTarget "Easydict" */; buildPhases = ( 21D768ECC6D11E109E6EB73A /* [CP] Check Pods Manifest.lock */, - 03B04B582B2D4B8E00E30823 /* ShellScript */, + 03B04B582B2D4B8E00E30823 /* Run Script */, C99EEB142385796700FEE666 /* Sources */, C99EEB152385796700FEE666 /* Frameworks */, C99EEB162385796700FEE666 /* Resources */, @@ -2479,16 +2493,17 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 03B04B582B2D4B8E00E30823 /* ShellScript */ = { + 03B04B582B2D4B8E00E30823 /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 2147483647; + buildActionMask = 12; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); + name = "Run Script"; outputFileListPaths = ( ); outputPaths = ( @@ -2652,6 +2667,7 @@ 9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */, 03BDA7BF2A26DA280079D04F /* NSScanner+EscapedScanning.m in Sources */, 03542A4C2937B5F100C34C33 /* EZYoudaoTranslate.m in Sources */, + 0A2A05A62B59757100EEA142 /* Bundle+AppInfo.swift in Sources */, 037852B329583F5200D0E2CF /* EZServiceCell.m in Sources */, 03247E362968158B00AFCD67 /* EZScriptExecutor.m in Sources */, 03882F8E29D95044005B5A52 /* ToastWindowController.m in Sources */, @@ -2691,6 +2707,7 @@ 17BCAEF82B0DFF9000A7D372 /* EZNiuTransTranslate.m in Sources */, 039F5506294B6E29004AB940 /* EZSettingViewController.m in Sources */, 03BD281E29481C0400F5891A /* EZAudioPlayer.m in Sources */, + 0A8685C82B552A590022534F /* DisabledAppTab.swift in Sources */, 03E02A2629250D1D00A10260 /* EZEventMonitor.m in Sources */, 03B0233429231FA6001C7E63 /* MMConsoleLogFormatter.m in Sources */, 037852B9295D49F900D0E2CF /* EZTableRowView.m in Sources */, diff --git a/Easydict/App/Assets.xcassets/Colors/Contents.json b/Easydict/App/Assets.xcassets/Colors/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json new file mode 100644 index 000000000..a83dbf8e7 --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/add_minus_bg_color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE9", + "green" : "0xE9", + "red" : "0xEA" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3A", + "green" : "0x39", + "red" : "0x3B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json new file mode 100644 index 000000000..d60e3bb57 --- /dev/null +++ b/Easydict/App/Assets.xcassets/Colors/list_border_color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDE", + "green" : "0xDD", + "red" : "0xDE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0x50", + "red" : "0x50" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json b/Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json similarity index 100% rename from Easydict/App/Assets.xcassets/service_cell_highlight.colorset/Contents.json rename to Easydict/App/Assets.xcassets/Colors/service_cell_highlight_color.colorset/Contents.json diff --git a/Easydict/App/Easydict-Bridging-Header.h b/Easydict/App/Easydict-Bridging-Header.h index f98fcafec..06670f99e 100644 --- a/Easydict/App/Easydict-Bridging-Header.h +++ b/Easydict/App/Easydict-Bridging-Header.h @@ -21,6 +21,7 @@ #import "entry.h" #import "AppDelegate.h" #import "EZConfiguration.h" +#import "EZAppModel.h" #import "EZLocalStorage.h" #import "NSString+EZConvenience.h" diff --git a/Easydict/App/Localizable.xcstrings b/Easydict/App/Localizable.xcstrings index 2114e3a63..aacf0329e 100644 --- a/Easydict/App/Localizable.xcstrings +++ b/Easydict/App/Localizable.xcstrings @@ -1,6 +1,9 @@ { "sourceLanguage" : "en", "strings" : { + "" : { + + }, "about" : { "comment" : "about", "localizations" : { @@ -2415,6 +2418,23 @@ } } }, + "setting.disabled.import_app_error.message" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unable to add Application" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法添加应用" + } + } + } + }, "setting.general.advance.default_tts_service" : { "localizations" : { "en" : { diff --git a/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift new file mode 100644 index 000000000..c095084cb --- /dev/null +++ b/Easydict/Feature/Utility/Swift/Bundle/Bundle+AppInfo.swift @@ -0,0 +1,23 @@ +// +// Bundle+AppInfo.swift +// Easydict +// +// Created by phlpsong on 2024/1/18. +// Copyright © 2024 izual. All rights reserved. +// + +import Foundation + +extension Bundle { + var applicationName: String { + if let displayName: String = object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { + return displayName + } else if let name: String = object(forInfoDictionaryKey: "CFBundleName") as? String { + return name + } + if let executableURL { + return executableURL.deletingLastPathComponent().lastPathComponent + } + return "" + } +} diff --git a/Easydict/NewApp/View/SettingView/SettingView.swift b/Easydict/NewApp/View/SettingView/SettingView.swift index c6446538c..f64f70efc 100644 --- a/Easydict/NewApp/View/SettingView/SettingView.swift +++ b/Easydict/NewApp/View/SettingView/SettingView.swift @@ -11,6 +11,7 @@ import SwiftUI enum SettingTab: Int { case general case service + case disabled case privacy case about } @@ -30,6 +31,10 @@ struct SettingView: View { .tabItem { Label("service", systemImage: "briefcase") } .tag(SettingTab.service) + DisabledAppTab() + .tabItem { Label("disabled_app_list", systemImage: "nosign") } + .tag(SettingTab.disabled) + PrivacyTab() .tabItem { Label("privacy", systemImage: "hand.raised.square") } .tag(SettingTab.privacy) @@ -51,7 +56,7 @@ struct SettingView: View { func resizeWindowFrame() { guard let window else { return } - + // Disable zoom button, ref: https://stackoverflow.com/a/66039864/8378840 window.standardWindowButton(.zoomButton)?.isEnabled = false @@ -60,7 +65,7 @@ struct SettingView: View { let height = switch selection { case .general: maxWidth - case .service: + case .service, .disabled: 500 case .privacy: 320 diff --git a/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift new file mode 100644 index 000000000..a121652c7 --- /dev/null +++ b/Easydict/NewApp/View/SettingView/Tabs/DisabledAppTab.swift @@ -0,0 +1,231 @@ +// +// DisabledAppTab.swift +// Easydict +// +// Created by phlpsong on 2024/1/15. +// Copyright © 2024 izual. All rights reserved. +// + +import Combine +import SwiftUI + +private class DisabledAppViewModel: ObservableObject { + @Published var appModelList: [EZAppModel] = [] + @Published var selectedAppModels: Set = [] + @Published var isImporting = false + @Published var isShowImportErrorAlert = false + + init() { + fetchDisabledApps() + } + + func fetchDisabledApps() { + appModelList = EZLocalStorage.shared().selectTextTypeAppModelList + } + + func saveDisabledApps() { + EZLocalStorage.shared().selectTextTypeAppModelList = appModelList + } + + func removeDisabledApp() { + appModelList = appModelList.filter { !selectedAppModels.contains($0) } + saveDisabledApps() + selectedAppModels = [] + } + + func newAppURLsSelected(from urls: [URL]) { + urls.forEach { url in + let gotAccess = url.startAccessingSecurityScopedResource() + if !gotAccess { return } + appendNewDisabledApp(for: url) + url.stopAccessingSecurityScopedResource() + } + } + + func appendNewDisabledApp(for url: URL) { + guard let selectAppModel = disabledAppModel(from: url) else { return } + guard !appModelList.contains(selectAppModel) else { return } + appModelList.append(selectAppModel) + saveDisabledApps() + } + + func disabledAppModel(from url: URL) -> EZAppModel? { + let appModel = EZAppModel() + guard let bundle = Bundle(url: url) else { return nil } + appModel.appBundleID = bundle.bundleIdentifier ?? "" + appModel.triggerType = [] + return appModel + } +} + +@available(macOS 13.0, *) +struct DisabledAppTab: View { + @StateObject private var disabledAppViewModel = DisabledAppViewModel() + + var listToolbar: some View { + ListToolbar() + .fileImporter( + isPresented: $disabledAppViewModel.isImporting, + allowedContentTypes: [.application], + allowsMultipleSelection: true + ) { result in + switch result { + case let .success(urls): + disabledAppViewModel.newAppURLsSelected(from: urls) + case let .failure(error): + print("fileImporter error: \(error)") + disabledAppViewModel.isShowImportErrorAlert.toggle() + } + } + .alert(isPresented: $disabledAppViewModel.isShowImportErrorAlert) { + Alert(title: Text(""), message: Text("setting.disabled.import_app_error.message"), dismissButton: .default(Text("ok"))) + } + } + + var appListView: some View { + List(selection: $disabledAppViewModel.selectedAppModels) { + ForEach(disabledAppViewModel.appModelList, id: \.self) { app in + BlockAppItemView(with: app) + .tag(app) + } + .listRowSeparator(.hidden) + } + .listStyle(.plain) + .scrollIndicators(.never) + } + + var appListViewWithToolbar: some View { + VStack(spacing: 0) { + appListView + + listToolbar + } + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay(content: { + RoundedRectangle(cornerRadius: 8) + .stroke(Color("list_border_color"), lineWidth: 0.5) + }) + .padding(.horizontal, 25) + .padding(.bottom, 25) + .onTapGesture { + disabledAppViewModel.selectedAppModels = [] + } + } + + var body: some View { + VStack { + Text("disabled_title") + .padding(.horizontal) + .padding(.top, 18) + .padding(.bottom, 8) + + appListViewWithToolbar + } + .environmentObject(disabledAppViewModel) + } +} + +@available(macOS 13.0, *) +private struct ListToolbar: View { + @EnvironmentObject private var disabledAppViewModel: DisabledAppViewModel + + var body: some View { + VStack(spacing: 0) { + Divider() + HStack(spacing: 0) { + ListButton(systemName: "plus") { + disabledAppViewModel.isImporting.toggle() + } + .disabled(false) + Divider() + .padding(.vertical, 1) + ListButton(systemName: "minus") { + disabledAppViewModel.removeDisabledApp() + } + .disabled(disabledAppViewModel.selectedAppModels.isEmpty) + Spacer() + } + .padding(2) + } + .frame(height: 28) + .background(Color("add_minus_bg_color")) + } +} + +@available(macOS 13.0, *) +private struct ListButton: View { + @Environment(\.isEnabled) private var isEnabled: Bool + var systemName: String + var action: () -> Void + + var body: some View { + Button(action: { + action() + }) { + Image(systemName: systemName) + .resizable() + .scaledToFit() + .frame(width: 10, height: 10) + .padding(.horizontal, 8) + .contentShape(Rectangle()) + .foregroundStyle(isEnabled ? Color(.secondaryLabelColor) : Color(.tertiaryLabelColor)) + .font(.system(size: 14, weight: .semibold)) + } + .buttonStyle(BorderlessButtonStyle()) + } +} + +@available(macOS 13.0, *) +private struct BlockAppItemView: View { + @EnvironmentObject var disabledAppViewModel: DisabledAppViewModel + + let appIcon: NSImage + let appName: String + + init(with appModel: EZAppModel) { + let appBundleId = appModel.appBundleID + let workspace = NSWorkspace.shared + let appURL = workspace.urlForApplication(withBundleIdentifier: appBundleId) + guard let appURL else { + appIcon = .init() + appName = "" + return + } + + let appPath = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appBundleId) + guard let appPath else { + appIcon = .init() + appName = "" + return + } + appIcon = workspace.icon(forFile: appPath.path(percentEncoded: false)) + + guard let appBundle = Bundle(url: appURL) else { + appName = "" + return + } + appName = appBundle.applicationName + } + + var body: some View { + HStack(alignment: .center) { + Image(nsImage: appIcon) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + + Text(appName) + + Spacer() + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .padding(.vertical, 4) + .padding(.leading, 6) + } +} + +@available(macOS 13.0, *) +#Preview { + DisabledAppTab() +} diff --git a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift index 5399f9af7..83a5f6398 100644 --- a/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift +++ b/Easydict/NewApp/View/SettingView/Tabs/ServiceTab.swift @@ -172,7 +172,7 @@ private struct ServiceItemView: View { .listRowSeparator(.hidden) .listRowInsets(.init()) .padding(10) - .listRowBackground(viewModel.selectedService == service ? Color("service_cell_highlight") : tableColor) + .listRowBackground(viewModel.selectedService == service ? Color("service_cell_highlight_color") : tableColor) .overlay { TapHandler { viewModel.selectedService = service