diff --git a/gui/packages/ubuntupro/lib/app.dart b/gui/packages/ubuntupro/lib/app.dart index adaf848a2..749656acf 100644 --- a/gui/packages/ubuntupro/lib/app.dart +++ b/gui/packages/ubuntupro/lib/app.dart @@ -11,6 +11,7 @@ import 'core/agent_connection.dart'; import 'core/agent_monitor.dart'; import 'core/settings.dart'; import 'pages/landscape/landscape_page.dart'; +import 'pages/landscape_skip/landscape_skip_page.dart'; import 'pages/startup/startup_page.dart'; import 'pages/subscribe_now/subscribe_now_page.dart'; import 'pages/subscription_status/subscription_status_page.dart'; @@ -76,6 +77,8 @@ class Pro4WSLApp extends StatelessWidget { }, ), if (settings.isLandscapeConfigurationEnabled) ...{ + Routes.skipLandscape: + WizardRoute(builder: (_) => const LandscapeSkipPage()), Routes.configureLandscape: const WizardRoute(builder: LandscapePage.create), Routes.subscriptionStatus: WizardRoute( diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index 1cbdf00a9..af2b8692b 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -57,7 +57,12 @@ "purchaseStatusServer":"Something went wrong with Microsoft Store. Please try again later.", "purchaseStatusUnknown": "Unknown error when trying to purchase the subscription. Consider restarting this app.", - "landscapeHeading": "Configure the connection to {landscapeLink} to manage your Ubuntu WSL instances remotely.", + "landscapeSkip": "Skip for now", + "landscapeSkipDescription": "You can always configure Landscape later", + "landscapeSkipRegister": "Register with Landscape", + + "landscapeTitle": "Landscape", + "landscapeHeading": "Configure the connection to Landscape to manage your Ubuntu WSL instances remotely. {landscapeLink}", "@landscapeHeading": { "placeholders": { "landscapeLink": { diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart index a3f6426d5..0e3cc6145 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart @@ -19,7 +19,7 @@ class LandscapeModel extends ChangeNotifier { LandscapeModel(this.client); /// The URL to be shown in the UI. - final landscapeURI = Uri.https('ubuntu.com', '/landscape'); + static final landscapeURI = Uri.https('ubuntu.com', '/landscape'); /// Whether the current form is complete (ready to be submitted). bool get isComplete => _active.isComplete; diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart index c2df76073..84bf06b30 100644 --- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart +++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart @@ -25,7 +25,6 @@ class LandscapePage extends StatelessWidget { super.key, required this.onApplyConfig, required this.onBack, - required this.onSkip, }); /// Callable invoked when this page successfully applies the configuration. @@ -34,9 +33,6 @@ class LandscapePage extends StatelessWidget { /// Callable invoked when the user navigates back. final void Function() onBack; - /// Callable invoked when the user skips this page. - final void Function() onSkip; - @override Widget build(BuildContext context) { final lang = AppLocalizations.of(context); @@ -49,21 +45,17 @@ class LandscapePage extends StatelessWidget { ), ), ), - ).copyWith( - a: const TextStyle( - decoration: TextDecoration.underline, - ), ); return LandingPage( svgAsset: 'assets/Landscape-tag.svg', - title: 'Landscape', + title: lang.landscapeTitle, children: [ // Only rebuilds if the value of model.landscapeURI changes (never in production) Selector( - selector: (_, model) => model.landscapeURI, + selector: (_, model) => LandscapeModel.landscapeURI, builder: (context, uri, _) => MarkdownBody( - data: lang.landscapeHeading('[Landscape]($uri)'), + data: lang.landscapeHeading('[${lang.learnMore}]($uri)'), onTapLink: (_, href, __) => launchUrl(uri), styleSheet: linkStyle, ), @@ -79,7 +71,6 @@ class LandscapePage extends StatelessWidget { selector: (_, model) => model.isComplete, builder: (context, isComplete, _) => _NavigationButtonRow( onBack: onBack, - onSkip: onSkip, onNext: isComplete ? () => _tryApplyConfig(context) : null, ), ), @@ -114,13 +105,11 @@ class LandscapePage extends StatelessWidget { landscapePage = LandscapePage( onApplyConfig: Wizard.of(context).back, onBack: Wizard.of(context).back, - onSkip: Wizard.of(context).back, ); } else { landscapePage = LandscapePage( onApplyConfig: Wizard.of(context).next, onBack: Wizard.of(context).back, - onSkip: Wizard.of(context).next, ); } @@ -242,12 +231,10 @@ class LandscapeConfigForm extends StatelessWidget { class _NavigationButtonRow extends StatelessWidget { const _NavigationButtonRow({ this.onBack, - this.onSkip, this.onNext, }); final void Function()? onBack; - final void Function()? onSkip; final void Function()? onNext; @override @@ -261,13 +248,6 @@ class _NavigationButtonRow extends StatelessWidget { child: Text(lang.buttonBack), ), const Spacer(), - FilledButton( - onPressed: onSkip, - child: Text(lang.buttonSkip), - ), - const SizedBox( - width: 16.0, - ), ElevatedButton( onPressed: onNext, child: Text(lang.buttonNext), diff --git a/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart new file mode 100644 index 000000000..42811dfc1 --- /dev/null +++ b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:wizard_router/wizard_router.dart'; + +import '../../routes.dart'; +import '../landscape/landscape_model.dart'; +import '../widgets/navigation_row.dart'; +import '../widgets/page_widgets.dart'; +import '../widgets/radio_tile.dart'; + +enum _SkipEnum { + skip, + register, +} + +class LandscapeSkipPage extends StatefulWidget { + const LandscapeSkipPage({super.key}); + + @override + State createState() => _LandscapeSkipPageState(); +} + +class _LandscapeSkipPageState extends State { + _SkipEnum groupValue = _SkipEnum.skip; + + @override + Widget build(BuildContext context) { + final lang = AppLocalizations.of(context); + final wizard = Wizard.of(context); + + return ColumnPage( + svgAsset: 'assets/Landscape-tag.svg', + title: lang.landscapeTitle, + left: [ + MarkdownBody( + data: lang.landscapeHeading( + '[${lang.learnMore}](${LandscapeModel.landscapeURI})', + ), + onTapLink: (_, href, __) => launchUrl(LandscapeModel.landscapeURI), + ), + ], + right: [ + RadioTile( + value: _SkipEnum.skip, + title: lang.landscapeSkip, + subtitle: lang.landscapeSkipDescription, + groupValue: groupValue, + onChanged: (v) => setState(() { + groupValue = v!; + }), + ), + RadioTile( + value: _SkipEnum.register, + title: lang.landscapeSkipRegister, + groupValue: groupValue, + onChanged: (v) => setState(() { + groupValue = v!; + }), + ), + ], + navigationRow: NavigationRow( + onBack: wizard.back, + onNext: () { + switch (groupValue) { + case _SkipEnum.skip: + wizard.jump(Routes.subscriptionStatus); + case _SkipEnum.register: + wizard.next(); + } + }, + ), + ); + } +} diff --git a/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart new file mode 100644 index 000000000..f9269c875 --- /dev/null +++ b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class NavigationRow extends StatelessWidget { + const NavigationRow({ + required this.onBack, + required this.onNext, + this.backText, + this.nextText, + super.key, + }); + + final void Function()? onBack; + final String? backText; + final void Function()? onNext; + final String? nextText; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + OutlinedButton(onPressed: onBack, child: Text(backText ?? 'Back')), + FilledButton(onPressed: onNext, child: Text(nextText ?? 'Next')), + ], + ); + } +} diff --git a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart index a27003310..432313c5e 100644 --- a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart @@ -27,6 +27,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:yaru/yaru.dart'; +import 'navigation_row.dart'; import 'status_bar.dart'; /// The simplest material page that covers most of the use cases in this app, @@ -168,3 +169,91 @@ class _PageContent extends StatelessWidget { ); } } + +/// Two-column, vertically centered page. The left column always contains the +/// svg image and title, with the left children below it. Both columns are equal +/// in width. Optionally, a [NavigationRow] may be provided that will span the +/// width below both columns. +class ColumnPage extends StatelessWidget { + const ColumnPage({ + required this.left, + required this.right, + this.svgAsset = 'assets/Ubuntu-tag.svg', + this.title = 'Ubuntu Pro', + this.navigationRow, + super.key, + }); + + final List left; + final List right; + final String svgAsset; + final String title; + final NavigationRow? navigationRow; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Pro4WSLPage( + body: Padding( + padding: const EdgeInsets.fromLTRB(32.0, 32.0, 32.0, 32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Left column + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: SvgPicture.asset( + svgAsset, + height: 70, + ), + ), + const WidgetSpan( + child: SizedBox( + width: 8, + ), + ), + TextSpan( + text: title, + style: theme.textTheme.displaySmall + ?.copyWith(fontWeight: FontWeight.w100), + ), + ], + ), + ), + const SizedBox(height: 24), + ...left, + ], + ), + ), + // Spacer + const SizedBox(width: 32), + // Right column + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: right, + ), + ), + ], + ), + ), + if (navigationRow != null) navigationRow!, + ], + ), + ), + ); + } +} diff --git a/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart b/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart new file mode 100644 index 000000000..6220ca6a3 --- /dev/null +++ b/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:yaru/yaru.dart'; + +class RadioTile extends StatelessWidget { + const RadioTile({ + required this.value, + required this.title, + required this.groupValue, + required this.onChanged, + this.subtitle, + super.key, + }); + + final String title; + final String? subtitle; + final T value, groupValue; + final Function(T?)? onChanged; + + @override + Widget build(BuildContext context) { + // Adds a nice visual clue that the tile is selected. + return YaruSelectableContainer( + selected: groupValue == value, + selectionColor: Theme.of(context).colorScheme.tertiaryContainer, + child: YaruRadioListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.comfortable, + dense: true, + title: Text(title), + subtitle: subtitle != null ? Text(subtitle!) : null, + value: value, + groupValue: groupValue, + onChanged: onChanged, + ), + ); + } +} diff --git a/gui/packages/ubuntupro/lib/routes.dart b/gui/packages/ubuntupro/lib/routes.dart index 28a9c61cd..248b69226 100644 --- a/gui/packages/ubuntupro/lib/routes.dart +++ b/gui/packages/ubuntupro/lib/routes.dart @@ -1,6 +1,7 @@ abstract class Routes { static const startup = '/startup'; static const subscribeNow = '/subscribe-now'; + static const skipLandscape = '/skip-landscape'; static const configureLandscape = '/configure-landscape'; static const subscriptionStatus = '/subscription-status'; static const configureLandscapeLate = '/configure-landscape-late'; diff --git a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart index 65e34df52..087d4a208 100644 --- a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart @@ -403,7 +403,6 @@ Widget buildApp( return buildSingleRouteMultiProviderApp( child: LandscapePage( onApplyConfig: () {}, - onSkip: () {}, onBack: () {}, ), providers: [ChangeNotifierProvider.value(value: model)],