[pve-devel] [PATCH pve_flutter_frontend v3] ui: enable noVNC console on iOS
Dominik Csapak
d.csapak at proxmox.com
Wed Aug 13 15:36:28 CEST 2025
On 8/13/25 10:29, Shan Shaji wrote:
> The noVNC console was disabled in iOS and was only available in
> android.
>
> To fix the issue enabled the noVNC console view on iOS devices. The
> changes also includes a refactor of the function responsible for
> displaying the webview. Additionally, an `AppBar` has been added
> to the console view on to allow users to easily close the view. To
> avoid the body of the Scaffold to resize when the keyboard pops up set
> `resizeToAvoidBottomInset` [0] to `false`
>
> The `dart format` command was also ran in the file to fix the file
> formatting.
is it possible to send dart format patches upfront and as a seperate patch?
that way one can see the actual changes much more easily.
also which version of dart/flutter do you use?
when i do `dart format` on that file here on current master,
it does not change the formatting at all
(but i'm still on flutter 3.29 with dart sdk on 3.7.2)
>
> - [0] https://api.flutter.dev/flutter/material/Scaffold/resizeToAvoidBottomInset.html
>
> Signed-off-by: Shan Shaji <s.shaji at proxmox.com>
> ---
>
> changes since v2:
> - Set `resizeToAvoidBottomInset` top false to avoid the Scaffold body
> to resize automatically when the keyboard pops up.
> - Update commit message.
>
> changes since v1:
> - Rebased with master.
> - Update commit message.
>
> lib/widgets/pve_console_menu_widget.dart | 176 +++++++++++------------
> 1 file changed, 84 insertions(+), 92 deletions(-)
>
> diff --git a/lib/widgets/pve_console_menu_widget.dart b/lib/widgets/pve_console_menu_widget.dart
> index 473595d..8f1b889 100644
> --- a/lib/widgets/pve_console_menu_widget.dart
> +++ b/lib/widgets/pve_console_menu_widget.dart
> @@ -97,45 +97,19 @@ class PveConsoleMenu extends StatelessWidget {
> }
> },
> ),
> - if (Platform.isAndroid) // web_view is only available for mobile :(
> +
> + // web_view is only available for mobile :(
> + // xterm.js doesn't work that well on mobile
> + if (Platform.isAndroid || Platform.isIOS)
> ListTile(
> title: const Text(
> - //type == "qemu" ? "noVNC Console" : "xterm.js Console",
> - "noVNC Console", // xterm.js doesn't work that well on mobile
> + "noVNC Console",
> style: TextStyle(fontWeight: FontWeight.bold),
> ),
> subtitle: const Text("Open console view"),
> - onTap: () async {
> - if (Platform.isAndroid) {
> - if (['qemu', 'lxc'].contains(type)) {
> - SystemChrome.setEnabledSystemUIMode(
> - SystemUiMode.immersive);
> - Navigator.of(context)
> - .push(_createHTMLConsoleRoute())
> - .then((completion) {
> - SystemChrome.setEnabledSystemUIMode(
> - SystemUiMode.edgeToEdge,
> - overlays: [
> - SystemUiOverlay.top,
> - SystemUiOverlay.bottom
> - ]);
> - });
> - } else if (type == 'node') {
> - SystemChrome.setEnabledSystemUIMode(
> - SystemUiMode.immersive);
> - Navigator.of(context)
> - .push(_createHTMLConsoleRoute())
> - .then((completion) {
> - SystemChrome.setEnabledSystemUIMode(
> - SystemUiMode.edgeToEdge,
> - overlays: [
> - SystemUiOverlay.top,
> - SystemUiOverlay.bottom
> - ]);
> - });
> - }
> - } else {
> - print('not implemented for current platform');
> + onTap: () {
> + if (['qemu', 'lxc'].contains(type) || type == 'node') {
> + _openNoVncConsole(context);
> }
> },
> ),
> @@ -145,6 +119,18 @@ class PveConsoleMenu extends StatelessWidget {
> );
> }
>
> + Future<void> _openNoVncConsole(BuildContext context) async {
> + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
> + await Navigator.push(context, _createHTMLConsoleRoute());
> + SystemChrome.setEnabledSystemUIMode(
> + SystemUiMode.edgeToEdge,
> + overlays: [
> + SystemUiOverlay.top,
> + SystemUiOverlay.bottom,
> + ],
> + );
> + }
> +
> void showTextDialog(BuildContext context, String title, String content) {
> showDialog(
> context: context,
> @@ -226,74 +212,80 @@ class PVEWebConsoleState extends State<PVEWebConsole> {
> WidgetsFlutterBinding.ensureInitialized();
>
> return FutureBuilder(
> - future: CookieManager.instance().setCookie(
> - url: WebUri(consoleUrl),
> - name: 'PVEAuthCookie',
> - value: ticket,
> - ),
> - builder: (context, snapshot) {
> - return SafeArea(
> - child: InAppWebView(
> - onReceivedServerTrustAuthRequest: (controller, challenge) async {
> - final cert = challenge.protectionSpace.sslCertificate;
> - final certBytes = cert?.x509Certificate?.encoded;
> - final sslError = challenge.protectionSpace.sslError?.message;
> + future: CookieManager.instance().setCookie(
> + url: WebUri(consoleUrl),
> + name: 'PVEAuthCookie',
> + value: ticket,
> + ),
> + builder: (context, snapshot) {
> + return Scaffold(
> + resizeToAvoidBottomInset: false,
> + appBar: AppBar(),
> + body: InAppWebView(
> + onReceivedServerTrustAuthRequest: (controller, challenge) async {
> + final cert = challenge.protectionSpace.sslCertificate;
> + final certBytes = cert?.x509Certificate?.encoded;
> + final sslError = challenge.protectionSpace.sslError?.message;
>
> - String? issuedTo = cert?.issuedTo?.CName.toString();
> - String? hash = certBytes != null
> - ? sha256.convert(certBytes).toString()
> - : null;
> + String? issuedTo = cert?.issuedTo?.CName.toString();
> + String? hash = certBytes != null
> + ? sha256.convert(certBytes).toString()
> + : null;
>
> - final settings =
> - await ProxmoxGeneralSettingsModel.fromLocalStorage();
> + final settings =
> + await ProxmoxGeneralSettingsModel.fromLocalStorage();
>
> - bool trust = false;
> - if (hash != null && settings.trustedFingerprints != null) {
> - trust = settings.trustedFingerprints!.contains(hash);
> - }
> + bool trust = false;
> + if (hash != null && settings.trustedFingerprints != null) {
> + trust = settings.trustedFingerprints!.contains(hash);
> + }
>
> - if (!trust) {
> - // format hash to '01:23:...' format
> - String? formattedHash = hash?.toUpperCase().replaceAllMapped(
> - RegExp(r"[a-zA-Z0-9]{2}"),
> - (match) => "${match.group(0)}:");
> - formattedHash = formattedHash?.substring(
> - 0, formattedHash.length - 1); // remove last ':'
> + if (!trust) {
> + // format hash to '01:23:...' format
> + String? formattedHash = hash?.toUpperCase().replaceAllMapped(
> + RegExp(r"[a-zA-Z0-9]{2}"), (match) => "${match.group(0)}:");
> + formattedHash = formattedHash?.substring(
> + 0, formattedHash.length - 1); // remove last ':'
>
> - if (context.mounted) {
> - trust = await showTLSWarning(
> - context,
> - sslError ?? 'An unknown TLS error has occurred',
> - issuedTo ?? 'unknown',
> - formattedHash ?? 'unknown');
> - }
> + if (context.mounted) {
> + trust = await showTLSWarning(
> + context,
> + sslError ?? 'An unknown TLS error has occurred',
> + issuedTo ?? 'unknown',
> + formattedHash ?? 'unknown');
> }
> + }
>
> - // save Fingerprint
> - if (trust && hash != null) {
> - await settings
> - .rebuild((b) => b..trustedFingerprints.add(hash))
> - .toLocalStorage();
> - print(settings.toJson());
> - }
> + // save Fingerprint
> + if (trust && hash != null) {
> + await settings
> + .rebuild((b) => b..trustedFingerprints.add(hash))
> + .toLocalStorage();
> + print(settings.toJson());
> + }
>
> - final action = trust
> - ? ServerTrustAuthResponseAction.PROCEED
> - : ServerTrustAuthResponseAction.CANCEL;
> - return ServerTrustAuthResponse(action: action);
> - },
> - onWebViewCreated: (controller) {
> - webViewController = controller;
> - controller.loadUrl(
> - urlRequest: URLRequest(url: WebUri(consoleUrl)));
> - },
> - ),
> - );
> - });
> + final action = trust
> + ? ServerTrustAuthResponseAction.PROCEED
> + : ServerTrustAuthResponseAction.CANCEL;
> + return ServerTrustAuthResponse(action: action);
> + },
> + onWebViewCreated: (controller) {
> + webViewController = controller;
> + controller.loadUrl(
> + urlRequest: URLRequest(url: WebUri(consoleUrl)));
> + },
> + ),
> + );
> + },
> + );
> }
>
> - Future<bool> showTLSWarning(BuildContext context, String sslError,
> - String issuedTo, String hash) async {
> + Future<bool> showTLSWarning(
> + BuildContext context,
> + String sslError,
> + String issuedTo,
> + String hash,
> + ) async {
> return await showDialog(
> context: context,
> barrierDismissible: false,
More information about the pve-devel
mailing list