From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 8444F62712 for ; Wed, 30 Sep 2020 14:05:06 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5996318AD8 for ; Wed, 30 Sep 2020 14:04:36 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id D33AD18AC9 for ; Wed, 30 Sep 2020 14:04:33 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 99A564491E for ; Wed, 30 Sep 2020 14:04:33 +0200 (CEST) Date: Wed, 30 Sep 2020 14:04:22 +0200 (CEST) From: Tim Marx To: Proxmox VE development discussion , Aaron Lauterer Message-ID: <386034124.798.1601467462478@webmail.proxmox.com> In-Reply-To: <20200928134125.7266-1-a.lauterer@proxmox.com> References: <20200928122825.21547-4-a.lauterer@proxmox.com> <20200928134125.7266-1-a.lauterer@proxmox.com> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit X-Priority: 3 Importance: Normal X-Mailer: Open-Xchange Mailer v7.10.3-Rev22 X-Originating-Client: open-xchange-appsuite X-SPAM-LEVEL: Spam detection results: 0 AWL 0.759 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [flutter.dev, proxmox.com, controller.page, context.name] URIBL_SBL_A 0.1 Contains URL's A record listed in the Spamhaus SBL blocklist [flutter.dev] Subject: Re: [pve-devel] [PATCH pve_flutter_frontend 3/3] Add first welcome screen X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 30 Sep 2020 12:05:06 -0000 comments inline > Aaron Lauterer hat am 28.09.2020 15:41 geschrieben: > > > Signed-off-by: Aaron Lauterer > --- > Same patch without temp png files. > > Thanks @Dominik for noticing > > .../ssl_validate/login_manager_screen.png | Bin 0 -> 20389 bytes > .../login_manager_screen_settings.png | Bin 0 -> 37362 bytes > lib/main.dart | 14 +- > lib/utils/dot_indicators.dart | 67 ++++++ > .../pve_welcome_common.dart | 48 +++++ > .../firstWelcomeScreen/pve_welcome_faq.dart | 56 +++++ > .../firstWelcomeScreen/pve_welcome_last.dart | 66 ++++++ > .../firstWelcomeScreen/pve_welcome_logo.dart | 33 +++ > .../pve_welcome_ssl_hint.dart | 51 +++++ > lib/widgets/pve_first_welcome_screen.dart | 193 ++++++++++++++++++ > pubspec.yaml | 1 + > 11 files changed, 528 insertions(+), 1 deletion(-) > create mode 100644 assets/images/ssl_validate/login_manager_screen.png > create mode 100644 assets/images/ssl_validate/login_manager_screen_settings.png > create mode 100644 lib/utils/dot_indicators.dart > create mode 100644 lib/widgets/firstWelcomeScreen/pve_welcome_common.dart > create mode 100644 lib/widgets/firstWelcomeScreen/pve_welcome_faq.dart > create mode 100644 lib/widgets/firstWelcomeScreen/pve_welcome_last.dart > create mode 100644 lib/widgets/firstWelcomeScreen/pve_welcome_logo.dart > create mode 100644 lib/widgets/firstWelcomeScreen/pve_welcome_ssl_hint.dart > create mode 100644 lib/widgets/pve_first_welcome_screen.dart > > diff --git a/lib/main.dart b/lib/main.dart > index 8cd6c36..57ad39c 100644 > --- a/lib/main.dart > +++ b/lib/main.dart > @@ -1,6 +1,8 @@ > import 'package:flutter/foundation.dart'; > import 'package:flutter/material.dart'; > import 'package:provider/provider.dart'; > +import 'package:pve_flutter_frontend/widgets/pve_first_welcome_screen.dart'; > +import 'package:shared_preferences/shared_preferences.dart'; > import 'package:proxmox_login_manager/proxmox_login_manager.dart'; > import 'package:pve_flutter_frontend/bloc/pve_authentication_bloc.dart'; > import 'package:pve_flutter_frontend/bloc/pve_cluster_status_bloc.dart'; > @@ -47,6 +49,7 @@ void main() async { > FlutterError.dumpErrorToConsole(details); > if (kReleaseMode) ProxmoxGlobalErrorBloc().addError(details.exception); > }; > + SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); > Provider.debugCheckInvalidValueType = null; > > runApp( > @@ -60,6 +63,7 @@ void main() async { > ], > child: MyApp( > authbloc: authBloc, > + sharedPreferences: sharedPreferences, > ), > ), > ); > @@ -67,9 +71,12 @@ void main() async { > > class MyApp extends StatelessWidget { > final PveAuthenticationBloc authbloc; > + final SharedPreferences sharedPreferences; > final GlobalKey navigatorKey = GlobalKey(); > > - MyApp({Key key, this.authbloc}) : super(key: key); > + MyApp({Key key, this.authbloc, this.sharedPreferences}) > + : assert(sharedPreferences != null), > + super(key: key); > > @override > Widget build(BuildContext context) { > @@ -142,6 +149,11 @@ class MyApp extends StatelessWidget { > builder: (context) => PveSplashScreen(), > ); > } > + if (sharedPreferences.getBool('showWelcomeScreen') ?? true) { > + return MaterialPageRoute( > + builder: (context) => PveWelcome(), > + ); > + } > > if (authbloc.state.value is Unauthenticated || > context.name == '/login') { > diff --git a/lib/utils/dot_indicators.dart b/lib/utils/dot_indicators.dart Why is the file called ..._indicators? The class is called DotIndicator. > new file mode 100644 > index 0000000..7b5034f > --- /dev/null > +++ b/lib/utils/dot_indicators.dart > @@ -0,0 +1,67 @@ > +import 'package:flutter/cupertino.dart'; > +import 'package:flutter/material.dart'; > +import 'dart:math'; > + > +class DotIndicator extends AnimatedWidget { > + DotIndicator({ > + this.controller, > + this.itemCount, > + this.onPageSelected, > + this.color: Colors.white, > + }) : super(listenable: controller); > + > + final PageController controller; > + it seems wrong to add a PageController dependency to a widget called DotIndicator, just pass the current page index. > + final int itemCount; > + > + final ValueChanged onPageSelected; > + final Color color; > + > + static const double _dotSize = 8.0; > + static const double _maxZoom = 1.2; > + static const double _dotSpacing = 25.0; > + > + Widget _buildDot(int index) { > + double selectedness = Curves.easeOut.transform( > + max( > + 0.0, > + 1.0 - ((controller.page ?? controller.initialPage) - index).abs(), > + ), > + ); > + double zoom = 1.0 + (_maxZoom - 1.0) * selectedness; > + double shadowBlurRadius = 4.0 * selectedness; > + double shadowSpreadRadius = 1.0 * selectedness; > + return new Container( > + width: _dotSpacing, > + child: Center( > + child: Container( > + width: _dotSize * zoom, > + height: _dotSize * zoom, > + child: InkWell( > + onTap: () => onPageSelected(index), > + ), An InkWell is only used when you want that specific animation, but in your case the animation can't be seen. Would be great to see that animation or just use a GestureDetector. > + decoration: BoxDecoration( > + color: color, > + shape: BoxShape.circle, > + boxShadow: [ > + BoxShadow( > + color: Colors.white.withOpacity(0.72), > + blurRadius: shadowBlurRadius, > + spreadRadius: shadowSpreadRadius, > + offset: Offset(0.0, 0.0)) > + ], > + ), > + ), > + ), > + ); > + } > + ^^^^ That screams for, extract widget :D > + Widget build(BuildContext contect) { > + return Row( > + mainAxisAlignment: MainAxisAlignment.center, > + children: List.generate( > + itemCount, > + _buildDot, > + )); > + } > +} > diff --git a/lib/widgets/firstWelcomeScreen/pve_welcome_common.dart b/lib/widgets/firstWelcomeScreen/pve_welcome_common.dart > new file mode 100644 > index 0000000..52055f9 > --- /dev/null > +++ b/lib/widgets/firstWelcomeScreen/pve_welcome_common.dart > @@ -0,0 +1,48 @@ > +import 'package:flutter/material.dart'; > + > +class PveQuestion extends StatelessWidget { > + const PveQuestion({ > + Key key, > + this.text, > + }) : super(key: key); > + > + final String text; > + > + @override > + Widget build(BuildContext context) { > + return Padding( > + padding: EdgeInsets.fromLTRB(10.0, 10.0, 5.0, 0.0), > + child: Text( > + text, > + style: TextStyle( > + fontWeight: FontWeight.bold, > + ), > + ), > + ); > + } > +} > + > +class PveAnswer extends StatelessWidget { > + const PveAnswer({ > + Key key, > + this.text, > + this.spans, > + }) : super(key: key); > + > + final String text; > + final List spans; > + > + @override > + Widget build(BuildContext context) { > + return Padding( > + padding: EdgeInsets.fromLTRB(20.0, 5.0, 5.0, 5.0), > + child: RichText( > + text: TextSpan( > + text: text, > + style: DefaultTextStyle.of(context).style, > + children: spans, > + ), > + ), > + ); > + } > +} > diff --git a/lib/widgets/firstWelcomeScreen/pve_welcome_faq.dart b/lib/widgets/firstWelcomeScreen/pve_welcome_faq.dart > new file mode 100644 > index 0000000..65f931c > --- /dev/null > +++ b/lib/widgets/firstWelcomeScreen/pve_welcome_faq.dart > @@ -0,0 +1,56 @@ > +import 'package:flutter/material.dart'; > +import 'package:url_launcher/url_launcher.dart'; > +import 'package:flutter/gestures.dart'; > +import 'pve_welcome_common.dart'; > + > +// FAQ > +class PveWelcomePageFAQ extends StatelessWidget { > + const PveWelcomePageFAQ({ > + Key key, > + }) : super(key: key); > + > + @override > + Widget build(BuildContext context) { > + return Column( > + mainAxisAlignment: MainAxisAlignment.center, > + crossAxisAlignment: CrossAxisAlignment.start, > + children: [ > + PveQuestion( > + text: "How do I connect if I am not using the default port 8006?"), > + PveAnswer( > + text: > + "Add the port at the end, separated by a colon. For the default https port add 443."), > + PveAnswer( > + text: "For example: 192.168.1.10", > + spans: [ > + TextSpan( > + text: ":443", > + style: TextStyle( > + fontWeight: FontWeight.bold, fontStyle: FontStyle.italic)) > + ], > + ), > + PveQuestion( > + text: "What about remote consoles?", > + ), > + PveAnswer( > + text: > + "Spice is currently supported. We plan to integrate VNC in the future."), > + PveQuestion(text: "Which Spice client works?"), > + PveAnswer( > + text: "The ", > + spans: [ > + TextSpan( > + text: "Opague Spice client", > + style: TextStyle(decoration: TextDecoration.underline), > + recognizer: TapGestureRecognizer() > + ..onTap = () => { > + launch( > + 'https://play.google.com/store/apps/details?id=com.undatech.opaque') > + }), > + TextSpan(text: " works. We will support more in the future.") > + ], > + ) > + ], > + ); > + } > +} > diff --git a/lib/widgets/firstWelcomeScreen/pve_welcome_last.dart b/lib/widgets/firstWelcomeScreen/pve_welcome_last.dart > new file mode 100644 > index 0000000..cf34224 > --- /dev/null > +++ b/lib/widgets/firstWelcomeScreen/pve_welcome_last.dart > @@ -0,0 +1,66 @@ > +import 'package:flutter/material.dart'; > +import 'package:url_launcher/url_launcher.dart'; > +import 'package:flutter/gestures.dart'; > +import '../../utils/promox_colors.dart'; > + > +// goodbye > +class PveWelcomePageLast extends StatelessWidget { > + const PveWelcomePageLast({Key key, this.onDone}) : super(key: key); > + > + final VoidCallback onDone; > + > + @override > + Widget build(BuildContext context) { > + return Padding( > + padding: EdgeInsets.all(15.0), > + child: Column( > + mainAxisAlignment: MainAxisAlignment.center, > + children: [ > + Text("Enjoy the app"), > + Padding( > + padding: const EdgeInsets.all(8.0), > + child: Icon( > + Icons.emoji_people_rounded, > + color: Colors.white, > + size: 70, > + ), > + ), > + Padding( > + padding: const EdgeInsets.all(8.0), > + child: RaisedButton( > + onPressed: () => {onDone()}, > + color: ProxmoxColors.orange, > + textColor: Colors.white, > + child: Text("Start"), > + ), > + ), > + RichText( > + textAlign: TextAlign.center, > + text: TextSpan( > + text: "Please use our ", > + style: DefaultTextStyle.of(context).style, > + children: [ > + TextSpan( > + text: 'community forum', > + style: TextStyle(decoration: TextDecoration.underline), > + recognizer: TapGestureRecognizer() > + ..onTap = () => {launch('https://forum.proxmox.com')}), > + TextSpan(text: ' or the '), > + TextSpan( > + text: 'user mailing list', > + style: TextStyle(decoration: TextDecoration.underline), > + recognizer: TapGestureRecognizer() > + ..onTap = () => { > + launch( > + 'https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-user') > + }), > + TextSpan( > + text: > + ' if you have suggestions or experience any problems.'), > + ], > + ), > + ), > + ], > + )); > + } > +} > diff --git a/lib/widgets/firstWelcomeScreen/pve_welcome_logo.dart b/lib/widgets/firstWelcomeScreen/pve_welcome_logo.dart > new file mode 100644 > index 0000000..3aee19a > --- /dev/null > +++ b/lib/widgets/firstWelcomeScreen/pve_welcome_logo.dart > @@ -0,0 +1,33 @@ > +import 'package:flutter/material.dart'; > + > +// Big Logo > +class PveWelcomePageLogo extends StatelessWidget { > + const PveWelcomePageLogo({ > + Key key, > + }) : super(key: key); > + > + @override > + Widget build(BuildContext context) { > + return Column( > + mainAxisAlignment: MainAxisAlignment.center, > + crossAxisAlignment: CrossAxisAlignment.center, > + children: [ > + Padding( > + padding: const EdgeInsets.fromLTRB(120.0, 0.0, 120.0, 30.0), > + child: Image.asset( > + 'assets/images/Proxmox-logo-symbol-white-orange.png', > + alignment: Alignment.center, > + ), > + ), > + FittedBox( > + child: Padding( > + padding: const EdgeInsets.all(8.0), > + child: Text( > + 'Proxmox Virtual Environment', > + style: TextStyle(fontSize: 26), > + ), > + ), > + ), > + ]); > + } > +} > diff --git a/lib/widgets/firstWelcomeScreen/pve_welcome_ssl_hint.dart b/lib/widgets/firstWelcomeScreen/pve_welcome_ssl_hint.dart > new file mode 100644 > index 0000000..61d02f4 > --- /dev/null > +++ b/lib/widgets/firstWelcomeScreen/pve_welcome_ssl_hint.dart > @@ -0,0 +1,51 @@ > +import 'package:flutter/material.dart'; > +import 'pve_welcome_common.dart'; > + > +// disable ssl validation hint > +class PveWelcomePageSSLValidation extends StatelessWidget { > + const PveWelcomePageSSLValidation({ > + Key key, > + }) : super(key: key); > + > + @override > + Widget build(BuildContext context) { > + return Padding( > + padding: EdgeInsets.all(15.0), > + child: Column( > + mainAxisAlignment: MainAxisAlignment.center, > + crossAxisAlignment: CrossAxisAlignment.start, > + children: [ > + PveQuestion( > + text: "Are you using a self signed certificate?", > + ), > + PveAnswer( > + text: "Disable SSL Validation in the Login Manager settings."), > + Column( > + children: [ > + Padding( > + padding: const EdgeInsets.all(8.0), > + child: Container( > + decoration: BoxDecoration( > + border: Border.all(color: Colors.white, width: 0.5)), > + child: Image.asset( > + 'assets/images/ssl_validate/login_manager_screen.png', > + ), > + ), > + ), > + Padding( > + padding: const EdgeInsets.all(8.0), > + child: Container( > + decoration: BoxDecoration( > + border: Border.all(color: Colors.white, width: 0.5)), > + child: Image.asset( > + 'assets/images/ssl_validate/login_manager_screen_settings.png', > + ), > + ), > + ), > + ], > + ) > + ], > + ), > + ); > + } > +} All those screens aren't scrollable, if someone uses a tiny device the content will be cut. -> SingleChildScrollview Use package imports not local ones. I don't like the link clicking web style, this is an App link clicking is for web or when you have a mouse please remove that and use buttons/icon buttons etc. > diff --git a/lib/widgets/pve_first_welcome_screen.dart b/lib/widgets/pve_first_welcome_screen.dart > new file mode 100644 > index 0000000..83fe399 > --- /dev/null > +++ b/lib/widgets/pve_first_welcome_screen.dart > @@ -0,0 +1,193 @@ > +import 'dart:ui'; > + > +import 'package:flutter/material.dart'; > +import 'package:flutter/rendering.dart'; > +import 'package:shared_preferences/shared_preferences.dart'; > +import '../utils/dot_indicators.dart'; > +import '../utils/promox_colors.dart'; > +import 'firstWelcomeScreen/pve_welcome_logo.dart'; > +import 'firstWelcomeScreen/pve_welcome_faq.dart'; > +import 'firstWelcomeScreen/pve_welcome_ssl_hint.dart'; > +import 'firstWelcomeScreen/pve_welcome_last.dart'; > + use package imports not local ones. > +class PveWelcome extends StatefulWidget { > + @override > + _PveWelcomeState createState() => _PveWelcomeState(); > +} > + > +class _PveWelcomeState extends State with TickerProviderStateMixin { > + PageController _controller; > + SharedPreferences _sharedPreferences; > + > + final List _pages = [ > + PveWelcomePageLogo(), > + PveWelcomePageSSLValidation(), > + PveWelcomePageFAQ(), > + ]; > + > + // Duration for page change > + static const Duration _pageChangeDuration = Duration(milliseconds: 150); > + static const Curve _pageChangeCurve = Curves.easeInOut; > + > + AnimationController _animController; > + Animation _animation; > + > + final colors = >[ > + TweenSequenceItem( > + weight: 1.0, > + tween: ColorTween( > + begin: ProxmoxColors.supportBlue, end: ProxmoxColors.supportDarkGrey), > + ), > + TweenSequenceItem( > + weight: 1.0, > + tween: ColorTween( > + begin: ProxmoxColors.supportDarkGrey, end: ProxmoxColors.orange), > + ), > + TweenSequenceItem( > + weight: 1.0, > + tween: ColorTween( > + begin: ProxmoxColors.orange, end: ProxmoxColors.supportBlue), > + ), > + ]; > + I personally don't like the color changes between pages. > + bool _isLast = false; > + bool _isFirst = true; > + > + final _buttonTextColor = Colors.white; > + final _buttonDisabledTextColor = Colors.white30; > + > + void _getPrefs() async { > + _sharedPreferences = await SharedPreferences.getInstance(); > + } > + Future those are async functions and should therefore be marked as Futures > + void _setWelcomeSeen() async { > + _sharedPreferences.setBool('showWelcomeScreen', false); > + } > + Future those are async functions and should therefore be marked as Futures Typo Seen/Screen > + @override > + void initState() { > + super.initState(); > + > + _getPrefs(); This will actually result in a race condition, because this is an unawaited Future and is used in the skipDone function. You can't know that this will be completed when the user taps skip and this would result in a crash. > + _controller = PageController(); > + > + // add last page here so we can define the callback for the start button > + _pages.add(PveWelcomePageLast(onDone: () { > + skipDone(); > + })); > + > + _animController = AnimationController( > + duration: _pageChangeDuration, > + vsync: this, > + ); > + _animation = TweenSequence(colors).animate(_animController) > + ..addListener(() { > + setState(() {}); > + }); > + > + _controller.addListener(() { > + setState(() { > + _isLast = _controller.page.floor() == _pages.length - 1; > + _isFirst = _controller.page.floor() == 0; > + }); > + }); > + } > + > + void skipDone() { > + _setWelcomeSeen(); > + Navigator.pushReplacementNamed(context, '/'); > + } > + > + Widget nextDoneButton() { > + if (_isLast) { > + return FlatButton( > + textColor: _buttonTextColor, > + disabledTextColor: _buttonDisabledTextColor, > + child: Text( > + "Done", > + ), > + onPressed: () { > + skipDone(); > + }, > + ); > + } else { > + return FlatButton( > + child: Text("Next"), > + textColor: _buttonTextColor, > + disabledTextColor: _buttonDisabledTextColor, > + onPressed: () { > + _controller.nextPage( > + duration: _pageChangeDuration, curve: _pageChangeCurve); > + }, > + ); > + } > + } > + > + Widget skipPrevButton() { > + if (_isFirst) { > + return FlatButton( > + textColor: _buttonTextColor, > + disabledTextColor: _buttonDisabledTextColor, > + onPressed: () { > + skipDone(); > + }, > + child: Text( > + 'Skip', > + ), > + ); > + } else { > + return FlatButton( > + textColor: _buttonTextColor, > + disabledTextColor: _buttonDisabledTextColor, > + child: Text( > + "Prev", > + ), > + onPressed: () { > + _controller.previousPage( > + duration: _pageChangeDuration, curve: _pageChangeCurve); > + }, > + ); > + } > + } > + > + @override > + Widget build(BuildContext context) { > + return Scaffold( > + backgroundColor: _animation.value, > + body: DefaultTextStyle( > + style: TextStyle(color: Colors.white, fontSize: 18), > + child: Column( > + children: [ > + Expanded( > + child: PageView.builder( > + controller: _controller, > + itemCount: _pages.length, > + onPageChanged: ((int index) { > + _animController.animateTo(index / colors.length); > + }), > + itemBuilder: (context, index) { > + return _pages[index]; > + }, > + ), > + ), > + Row( > + mainAxisAlignment: MainAxisAlignment.spaceBetween, > + children: [ > + skipPrevButton(), > + DotIndicator( > + controller: _controller, > + itemCount: _pages.length, > + onPageSelected: (int page) { > + _controller.animateToPage(page, > + duration: _pageChangeDuration, curve: _pageChangeCurve); > + }, > + ), > + nextDoneButton(), > + ], > + ), > + ], > + ), > + ), > + ); > + } > +} > diff --git a/pubspec.yaml b/pubspec.yaml > index d31eb74..09fa250 100644 > --- a/pubspec.yaml > +++ b/pubspec.yaml > @@ -61,6 +61,7 @@ flutter: > - assets/images/Proxmox_logo_white_orange_800.png > - assets/images/Proxmox-logo-symbol-white-orange.png > - assets/images/proxmox_logo_icon_white.png > + - assets/images/ssl_validate/ > > # An image asset can refer to one or more resolution-specific "variants", see > # https://flutter.dev/assets-and-images/#resolution-aware. > -- > 2.20.1 > > > > _______________________________________________ > pve-devel mailing list > pve-devel@lists.proxmox.com > https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel