From a36d99cd720149e42b519b06b04a887833b9fa23 Mon Sep 17 00:00:00 2001 From: wibus-wee <1596355173@qq.com> Date: Sun, 21 Jul 2024 16:57:24 +0800 Subject: [PATCH] feat: remote applist sidebar view --- InjectGUI.code-workspace | 3 + InjectGUI/Backend/SoftwareManager.swift | 98 +++++++--- InjectGUI/View/AppDetailView.swift | 13 +- InjectGUI/View/SidebarView.swift | 236 +++++++++++++++++++----- 4 files changed, 269 insertions(+), 81 deletions(-) diff --git a/InjectGUI.code-workspace b/InjectGUI.code-workspace index 3512b01..fee1827 100644 --- a/InjectGUI.code-workspace +++ b/InjectGUI.code-workspace @@ -6,6 +6,9 @@ { "path": "./InjectGUI/Extension" }, + { + "path": "./InjectGUI/Backend" + }, { "path": "./InjectGUI/Util" }, diff --git a/InjectGUI/Backend/SoftwareManager.swift b/InjectGUI/Backend/SoftwareManager.swift index baccc36..b19f79d 100644 --- a/InjectGUI/Backend/SoftwareManager.swift +++ b/InjectGUI/Backend/SoftwareManager.swift @@ -29,60 +29,102 @@ class SoftwareManager: ObservableObject { } } - private func loadAppInfo(from plistPath: String) -> AppDetail? { - let url = URL(fileURLWithPath: plistPath) - guard let data = try? Data(contentsOf: url), - let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any], + private func loadAppInfo( + from plistPath: String + ) -> AppDetail? { + let url = URL( + fileURLWithPath: plistPath + ) + guard let data = try? Data( + contentsOf: url + ), + let plist = try? PropertyListSerialization.propertyList( + from: data, + format: nil + ) as? [String: Any], let bundleName = plist["CFBundleName"] as? String, let bundleIdentifier = plist["CFBundleIdentifier"] as? String, let bundleVersion = plist["CFBundleVersion"] as? String, let bundleShortVersion = plist["CFBundleShortVersionString"] as? String else { return nil } - + // 获取图标文件名 let iconFileRaw = plist["CFBundleIconFile"] as? String ?? plist["CFBundleIconName"] as? String - + // 检查文件名并添加扩展名(如果需要) let iconFile: String? if let iconFileRaw = iconFileRaw { - iconFile = iconFileRaw.hasSuffix(".icns") ? iconFileRaw : iconFileRaw.appending(".icns") + iconFile = iconFileRaw.hasSuffix( + ".icns" + ) ? iconFileRaw : iconFileRaw.appending( + ".icns" + ) } else { iconFile = nil } - + // 检查 iconFile 是否为 nil guard let finalIconFile = iconFile else { return nil } - - + + let path = url.deletingLastPathComponent().path - let iconPath = URL(fileURLWithPath: plistPath).deletingLastPathComponent().appendingPathComponent("Resources").appendingPathComponent(finalIconFile).path - let icon = NSImage(contentsOfFile: iconPath) + let iconPath = URL( + fileURLWithPath: plistPath + ).deletingLastPathComponent().appendingPathComponent( + "Resources" + ).appendingPathComponent( + finalIconFile + ).path + let icon = NSImage( + contentsOfFile: iconPath + ) if icon == nil { - print("[W] Failed to load icon from path: \(iconPath)") + print( + "[W] Failed to load icon from path: \(iconPath)" + ) } - return AppDetail(name: bundleName, identifier: bundleIdentifier, version: bundleVersion, path: path, icon: icon ?? NSImage()) - } - - func getList() { - print("[*] Getting app list...") - let applicationDirectory = "/Applications" - let fileManager = FileManager.default - - guard let appPaths = try? fileManager.contentsOfDirectory(atPath: applicationDirectory) else { - return + return AppDetail( + name: bundleName, + identifier: bundleIdentifier, + version: bundleVersion, + path: path, + icon: icon ?? NSImage() + ) } - - for appPath in appPaths { - let fullPath = "\(applicationDirectory)/\(appPath)" - let infoPlistPath = "\(fullPath)/Contents/Info.plist" - if let appInfo = loadAppInfo(from: infoPlistPath) { + + func getList() { + print( + "[*] Getting app list..." + ) + let applicationDirectory = "/Applications" + let fileManager = FileManager.default + + guard let appPaths = try? fileManager.contentsOfDirectory( + atPath: applicationDirectory + ) else { + return + } + + for appPath in appPaths { + let fullPath = "\(applicationDirectory)/\(appPath)" + let infoPlistPath = "\(fullPath)/Contents/Info.plist" + if let appInfo = loadAppInfo( + from: infoPlistPath + ) { appListCache[appInfo.identifier] = appInfo } } // print("[*] App list: \(appListCache.keys)") } + + /// 检查某个软件是否存在于系统中 + func checkSoftwareIsInstalled(package: String) -> Bool { + print("[*] Checking if \(package) is installed...") + return appListCache[package] != nil + } + } diff --git a/InjectGUI/View/AppDetailView.swift b/InjectGUI/View/AppDetailView.swift index b4faae2..ed9e164 100644 --- a/InjectGUI/View/AppDetailView.swift +++ b/InjectGUI/View/AppDetailView.swift @@ -32,7 +32,15 @@ struct AppDetailView: View { init(appId: String) { self.appId = appId - self._appDetail = State(wrappedValue: SoftwareManager.shared.appListCache[appId] ?? AppDetail(name: "", identifier: "", version: "", path: "", icon: NSImage())) + let appInjectConfDetail = injectConfiguration.injectDetail(package: appId) + self.appInjectConfDetail = appInjectConfDetail + let getAppDetailFromSoftwareManager = softwareManager.appListCache[appId] + if getAppDetailFromSoftwareManager != nil { + self.appDetail = SoftwareManager.shared.appListCache[appId]! + } else { + self.appDetail = AppDetail(name: appInjectConfDetail?.packageName.allStrings.first ?? "", identifier: appInjectConfDetail?.packageName.allStrings.first ?? "", version: "", path: "", icon: NSImage()) + } + // self._appDetail = State(wrappedValue: SoftwareManager.shared.appListCache[appId] ?? AppDetail(name: "", identifier: "", version: "", path: "", icon: NSImage())) self._compatibility = State(wrappedValue: Compatibility(id: appId, inInjectLibList: false)) self._appInjectConfDetail = State(wrappedValue: nil) } @@ -56,7 +64,8 @@ struct AppDetailView: View { id: appId, inInjectLibList: inInjectLibList ) - self.appInjectConfDetail = injectConfiguration.injectDetail(package: appId) + let appInjectConfDetail = injectConfiguration.injectDetail(package: appId) + self.appInjectConfDetail = appInjectConfDetail } } diff --git a/InjectGUI/View/SidebarView.swift b/InjectGUI/View/SidebarView.swift index 55fcb3f..fcf3c71 100644 --- a/InjectGUI/View/SidebarView.swift +++ b/InjectGUI/View/SidebarView.swift @@ -17,87 +17,221 @@ enum DisplayMode { case remote } +extension SidebarView { + func handleApps( + apps: [AppEntry] + ) { + if searchText.isEmpty { + filteredApps = apps + } else { + filteredApps = apps.filter { + $0.detail.name.lowercased().contains( + searchText.lowercased() + ) || + $0.detail.identifier.lowercased().contains( + searchText.lowercased() + ) + } + } + } +} + struct SidebarView: View { @State var displayMode: DisplayMode = .local @State var searchText: String = "" @StateObject var softwareManager = SoftwareManager.shared - var filteredApps: [AppEntry] { - let apps = softwareManager.appListCache.map { AppEntry(id: $0.key, detail: $0.value) } - if searchText.isEmpty { - return apps.sorted { $0.detail.name < $1.detail.name } + @State var filteredApps: [AppEntry] = [] + + private func onDisplayModeChanged() { + if displayMode == .local { + let apps = softwareManager.appListCache.map { + AppEntry( + id: $0.key, + detail: $0.value + ) + } + handleApps( + apps: apps + ) } else { - return apps.filter { - $0.detail.name.lowercased().contains(searchText.lowercased()) || - $0.detail.identifier.lowercased().contains(searchText.lowercased()) - }.sorted { $0.detail.name < $1.detail.name } + let packages = injectConfiguration.getSupportedPackages() + let apps = packages.map { + AppEntry( + id: $0.id, + detail: AppDetail( + name: $0.name, + identifier: $0.id, + version: "", + path: "", + icon: NSImage() + ) + ) + } + handleApps( + apps: apps + ) } } - + var body: some View { VStack { - Picker("", selection: $displayMode) { - Text("Local").tag(DisplayMode.local) - Text("Remote").tag(DisplayMode.remote) + Picker( + "", + selection: $displayMode + ) { + Text( + "Local" + ).tag( + DisplayMode.local + ) + Text( + "Remote" + ).tag( + DisplayMode.remote + ) } - .pickerStyle(.segmented) - .padding(.horizontal) + .pickerStyle( + .segmented + ) + .padding( + .horizontal + ) HStack { - Image(systemName: "magnifyingglass") // 添加放大镜图标 - .foregroundColor(.secondary) - TextField("Search", text: $searchText) // 添加搜索栏 - .textFieldStyle(PlainTextFieldStyle()) + Image( + systemName: "magnifyingglass" + ) // 添加放大镜图标 + .foregroundColor( + .secondary + ) + TextField( + "Search", + text: $searchText + ) // 添加搜索栏 + .textFieldStyle( + PlainTextFieldStyle() + ) } - .padding(8) - .cornerRadius(8) - .padding(.horizontal) + .padding( + 8 + ) + .cornerRadius( + 8 + ) + .padding( + .horizontal + ) Divider() // 添加分隔线 - + Group { - List(filteredApps, id: \.id) { app in + List( + filteredApps, + id: \.id + ) { app in NavigationLink { - AppDetailView(appId: app.detail.identifier) + AppDetailView( + appId: app.detail.identifier + ) } label: { HStack { - Image(nsImage: app.detail.icon) - .resizable() - .frame(width: 32, height: 32) - .cornerRadius(4) - VStack (alignment: .leading) { - Text(app.detail.name) - .font(.headline) - VStack (alignment: .leading) { - Text(app.detail.identifier) - .font(.subheadline) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) + Image( + nsImage: app.detail.icon + ) + .resizable() + .frame( + width: 32, + height: 32 + ) + .cornerRadius( + 4 + ) + VStack ( + alignment: .leading + ) { + Text( + app.detail.name + ) + .font( + .headline + ) + VStack ( + alignment: .leading + ) { + Text( + app.detail.identifier + ) + .font( + .subheadline + ) + .foregroundColor( + .secondary + ) + .frame( + maxWidth: .infinity, + alignment: .leading + ) - Text("Version: \(app.detail.version)") - .font(.caption) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) + Text( + "Version: \(app.detail.version)" + ) + .font( + .caption + ) + .foregroundColor( + .secondary + ) + .frame( + maxWidth: .infinity, + alignment: .leading + ) } } } - .padding(.horizontal, 8) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .contentShape(Rectangle()) + .padding( + .horizontal, + 8 + ) + .frame( + maxWidth: .infinity, + maxHeight: .infinity + ) + .contentShape( + Rectangle() + ) } } - .frame(maxWidth: .infinity, maxHeight: .infinity) + .frame( + maxWidth: .infinity, + maxHeight: .infinity + ) } - .listStyle(SidebarListStyle()) + .listStyle( + SidebarListStyle() + ) + } + .onAppear() { + onDisplayModeChanged() + } + .onChange( + of: displayMode + ) { _ in + onDisplayModeChanged() + } + .onChange( + of: searchText + ) { _ in + onDisplayModeChanged() } } } - -struct SidebarView_Previews: PreviewProvider { - static var previews: some View { - SidebarView() - } -} +// +//struct SidebarView_Previews: PreviewProvider { +// static var previews: some View { +// SidebarView() +// } +//}