[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