feat: remote applist sidebar view

This commit is contained in:
wibus-wee
2024-07-21 16:57:24 +08:00
parent f1872da330
commit a36d99cd72
4 changed files with 269 additions and 81 deletions

View File

@@ -6,6 +6,9 @@
{
"path": "./InjectGUI/Extension"
},
{
"path": "./InjectGUI/Backend"
},
{
"path": "./InjectGUI/Util"
},

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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()
// }
//}