[pve-devel] [PATCH pve_flutter_frontend 3/3] Add first welcome screen

Tim Marx t.marx at proxmox.com
Wed Sep 30 14:04:22 CEST 2020


comments inline
> Aaron Lauterer <a.lauterer at proxmox.com> hat am 28.09.2020 15:41 geschrieben:
> 
>  
> Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
> ---
> 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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
>  
> -  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<int> 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<Widget>.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<TextSpan> 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>[
> +                  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<PveWelcome> with TickerProviderStateMixin {
> +  PageController _controller;
> +  SharedPreferences _sharedPreferences;
> +
> +  final List<Widget> _pages = [
> +    PveWelcomePageLogo(),
> +    PveWelcomePageSSLValidation(),
> +    PveWelcomePageFAQ(),
> +  ];
> +
> +  // Duration for page change
> +  static const Duration _pageChangeDuration = Duration(milliseconds: 150);
> +  static const Curve _pageChangeCurve = Curves.easeInOut;
> +
> +  AnimationController _animController;
> +  Animation<Color> _animation;
> +
> +  final colors = <TweenSequenceItem<Color>>[
> +    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<void> those are async functions and should therefore be marked as Futures
> +  void _setWelcomeSeen() async {
> +    _sharedPreferences.setBool('showWelcomeScreen', false);
> +  }
> +
Future<void> 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<Color>(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 at lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel




More information about the pve-devel mailing list