mirror of
https://github.com/wibus-wee/InjectGUI.git
synced 2025-11-25 03:14:58 +08:00
feat: remote applist sidebar view
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
{
|
||||
"path": "./InjectGUI/Extension"
|
||||
},
|
||||
{
|
||||
"path": "./InjectGUI/Backend"
|
||||
},
|
||||
{
|
||||
"path": "./InjectGUI/Util"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
//}
|
||||
|
||||
Reference in New Issue
Block a user