install: support tipa packages and show success alerts (#142)

This commit is contained in:
TastyHeadphones
2026-03-08 12:54:14 +08:00
committed by GitHub
parent 32b73cd50b
commit 4e14197e93
3 changed files with 58 additions and 19 deletions
@@ -0,0 +1,29 @@
import Foundation
import UniformTypeIdentifiers
enum VPhoneInstallPackage {
static let allowedContentTypes: [UTType] = [
UTType(filenameExtension: "ipa"),
UTType(filenameExtension: "tipa"),
].compactMap { $0 }
static func isSupportedFile(_ url: URL) -> Bool {
switch url.pathExtension.lowercased() {
case "ipa", "tipa":
true
default:
false
}
}
static func successMessage(for fileName: String, detail: String) -> String {
let trimmedDetail = detail.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedDetail.isEmpty else {
return "Installed \(fileName)."
}
if trimmedDetail.localizedCaseInsensitiveContains(fileName) {
return trimmedDetail
}
return "Installed \(fileName).\n\n\(trimmedDetail)"
}
}
+13 -8
View File
@@ -1,6 +1,5 @@
import AppKit
import Foundation
import UniformTypeIdentifiers
// MARK: - Install Menu
@@ -8,14 +7,14 @@ extension VPhoneMenuController {
func buildInstallMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Install")
menu.addItem(makeItem("Install IPA...", action: #selector(installIPAFromDisk)))
menu.addItem(makeItem("Install IPA/TIPA...", action: #selector(installIPAFromDisk)))
item.submenu = menu
return item
}
@objc func installIPAFromDisk() {
guard control.isConnected else {
showAlert(title: "Install IPA", message: "Guest is not connected.", style: .warning)
showAlert(title: "Install App Package", message: "Guest is not connected.", style: .warning)
return
}
@@ -23,11 +22,9 @@ extension VPhoneMenuController {
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = false
panel.allowedContentTypes = [
UTType(filenameExtension: "ipa") ?? .data,
]
panel.allowedContentTypes = VPhoneInstallPackage.allowedContentTypes
panel.prompt = "Install"
panel.message = "Choose an IPA to install in the guest."
panel.message = "Choose an IPA or TIPA package to install in the guest."
let response = panel.runModal()
guard response == .OK, let url = panel.url else { return }
@@ -36,8 +33,16 @@ extension VPhoneMenuController {
do {
let result = try await control.installIPA(localURL: url)
print("[install] \(result)")
showAlert(
title: "Install App Package",
message: VPhoneInstallPackage.successMessage(
for: url.lastPathComponent,
detail: result
),
style: .informational
)
} catch {
showAlert(title: "Install IPA", message: "\(error)", style: .warning)
showAlert(title: "Install App Package", message: "\(error)", style: .warning)
}
}
}
@@ -88,7 +88,7 @@ class VPhoneVirtualMachineView: VZVirtualMachineView {
// MARK: - Drag and Drop Install
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
guard droppedIPAURL(from: sender) != nil else { return [] }
guard droppedInstallPackageURL(from: sender) != nil else { return [] }
updateDragHighlight(true)
return .copy
}
@@ -99,44 +99,49 @@ class VPhoneVirtualMachineView: VZVirtualMachineView {
}
override func prepareForDragOperation(_ sender: any NSDraggingInfo) -> Bool {
droppedIPAURL(from: sender) != nil
droppedInstallPackageURL(from: sender) != nil
}
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
updateDragHighlight(false)
guard let url = droppedIPAURL(from: sender) else { return false }
guard let url = droppedInstallPackageURL(from: sender) else { return false }
Task { @MainActor in
guard let control else {
showAlert(title: "Install IPA", message: "Guest is not connected.", style: .warning)
showAlert(title: "Install App Package", message: "Guest is not connected.", style: .warning)
return
}
guard control.isConnected else {
showAlert(title: "Install IPA", message: "Guest is not connected.", style: .warning)
showAlert(title: "Install App Package", message: "Guest is not connected.", style: .warning)
return
}
do {
let result = try await control.installIPA(localURL: url)
print("[install] \(result)")
showAlert(
title: "Install App Package",
message: VPhoneInstallPackage.successMessage(
for: url.lastPathComponent,
detail: result
),
style: .informational
)
} catch {
showAlert(title: "Install IPA", message: "\(error)", style: .warning)
showAlert(title: "Install App Package", message: "\(error)", style: .warning)
}
}
return true
}
private func droppedIPAURL(from sender: any NSDraggingInfo) -> URL? {
private func droppedInstallPackageURL(from sender: any NSDraggingInfo) -> URL? {
let options: [NSPasteboard.ReadingOptionKey: Any] = [
.urlReadingFileURLsOnly: true,
]
guard let urls = sender.draggingPasteboard.readObjects(forClasses: [NSURL.self], options: options) as? [URL] else {
return nil
}
return urls.first {
let ext = $0.pathExtension.lowercased()
return ext == "ipa" || ext == "tipa"
}
return urls.first(where: VPhoneInstallPackage.isSupportedFile)
}
private func updateDragHighlight(_ visible: Bool) {