[pve-devel] [PATCH pve_flutter_frontend v2] feat: ios: enable opening of virt-viewer (.vv) file with spice app
Shan Shaji
s.shaji at proxmox.com
Mon Jul 21 15:46:43 CEST 2025
The feature to open the spice connection file was disabled in iOS and
was only available in Android. To support iOS, a new native channel
implementation for iOS has been added.
For the iOS implementation, use the `UIActivityViewController` [0] to
show the share sheet with the suggested apps that can open the file.
Alternatively, users can also save the file to the device storage.
The `getExternalChacheDirectories` function has been replaced with
`getTemporaryDirectory` as the external cache directories function is
not supported [1] in iOS.
[0] - https://developer.apple.com/documentation/uikit/uiactivityviewcontroller
[1] - https://developer.apple.com/documentation/bundleresources/information-property-list/utimportedtypedeclarations
References:
- https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures#Trailing-Closures
- https://medium.com/@dinesh.kachhot/different-ways-to-share-data-between-apps-de75a0a46d4a
- https://docs.flutter.dev/platform-integration/platform-channels#step-4-add-an-ios-platform-specific-implementation
- https://stackoverflow.com/questions/25644054/uiactivityviewcontroller-crashing-on-ios-8-ipads
Signed-off-by: Shan Shaji <s.shaji at proxmox.com>
---
changes since v1:
- Fixed commit message
Tested:
- The above changes are tested on iPad simulator and a real iPhone in
debug mode.
ios/Runner/AppDelegate.swift | 64 +++++++++++++++++++++---
lib/widgets/pve_console_menu_widget.dart | 10 ++--
2 files changed, 60 insertions(+), 14 deletions(-)
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 6266644..115e1fd 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -3,11 +3,61 @@ import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
- override func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
- ) -> Bool {
- GeneratedPluginRegistrant.register(with: self)
- return super.application(application, didFinishLaunchingWithOptions: launchOptions)
- }
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
+ let channel: FlutterMethodChannel = FlutterMethodChannel(
+ name: "com.proxmox.app.pve_flutter_frontend/filesharing",
+ binaryMessenger: controller.binaryMessenger)
+
+ channel.setMethodCallHandler({
+ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
+
+ guard call.method == "shareFile" else {
+ result(FlutterMethodNotImplemented)
+ return
+ }
+
+ let arguments = call.arguments as? [String: Any]
+ let path = arguments?["path"] as? String
+ let type = arguments?["type"] as? String
+
+ if let filePath = path, let _ = type {
+ self?.shareFile(atPath: filePath, from: controller, result: result)
+ } else {
+ result(FlutterError(code: "FileNotFoundException", message: "File not found", details: nil))
+ }
+ })
+
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ private func shareFile(atPath path: String, from controller: UIViewController, result: @escaping FlutterResult) {
+ let fileURL = URL(fileURLWithPath: path)
+ let activityVC = UIActivityViewController(
+ activityItems: [fileURL],
+ applicationActivities: nil,
+ )
+
+ // To avoid crashing in iPad
+ if let popover = activityVC.popoverPresentationController {
+ popover.sourceView = controller.view
+ popover.sourceRect = CGRect(
+ x: controller.view.bounds.midX,
+ y: controller.view.bounds.midY,
+ width: 0,
+ height: 0,
+ )
+ }
+
+
+ controller.present(activityVC, animated: true) {
+ result(nil)
+ }
+ }
+
+
}
diff --git a/lib/widgets/pve_console_menu_widget.dart b/lib/widgets/pve_console_menu_widget.dart
index cd8c314..8fa5538 100644
--- a/lib/widgets/pve_console_menu_widget.dart
+++ b/lib/widgets/pve_console_menu_widget.dart
@@ -38,7 +38,7 @@ class PveConsoleMenu extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- if (Platform.isAndroid && (allowSpice ?? true))
+ if ((Platform.isAndroid || Platform.isIOS) && (allowSpice ?? true))
ListTile(
title: const Text(
"SPICE",
@@ -47,8 +47,7 @@ class PveConsoleMenu extends StatelessWidget {
subtitle:
const Text("Open SPICE connection file with external App"),
onTap: () async {
- if (Platform.isAndroid) {
- final tempDir = await getExternalCacheDirectories();
+ final tempDir = await getTemporaryDirectory();
String apiPath;
if (['qemu', 'lxc'].contains(type)) {
@@ -67,7 +66,7 @@ class PveConsoleMenu extends StatelessWidget {
}
return;
}
- var filePath = await writeSpiceFile(data, tempDir![0].path);
+ var filePath = await writeSpiceFile(data, tempDir.path);
try {
await platform.invokeMethod('shareFile', {
@@ -98,9 +97,6 @@ class PveConsoleMenu extends StatelessWidget {
}
}
}
- } else {
- print('not implemented for current platform');
- }
},
),
if (Platform.isAndroid) // web_view is only available for mobile :(
--
2.39.5 (Apple Git-154)
More information about the pve-devel
mailing list