From 59daadb507cdc52d43950597a8f3e3687bba4b93 Mon Sep 17 00:00:00 2001 From: Alexey Inkin Date: Mon, 1 Aug 2022 18:24:34 +0400 Subject: [PATCH 01/17] [Tour of Beam][Frontend][#22600] TourScreen layout * theme setup * Replaced ThemeProvider with ThemeSwitchNotifier * header with theme mode switcher and logo * page container with header & footer * theme mode tests * renamed the directory to tour-of-beam * compressed beam_logo.png * added missing license comments * rudimentary layout of the first screen * review comments fixes #1 * moved notifyListeners inside then * responsive todo * split into 2 simple functions * deleted redundant constants & replaced 2018 text theme with 2021 * styling refinement * home screen layout * clickable sign in text * font weights fix * removed _getBaseFontTheme function * fixed border and bg color * color fixes * difficulty component * _LastModuleBody * todo in test * footer border * fixed overflows * replaced Project prefix with Tob * replaced then with await * inferred type * started translation of the home screen * sorted translations * Complexity comments * comment fixes * home screen translations * sign in overlay * import fix * integration test does not fail * playground_components package with dismissible_overlay * missing license * removed _dots from build * widgets refinement * renamed home screen to welcome screen * deleted copyWith * _SdkButton * trailing comma & pubspec formatting * license and lints * license * removed license from .metadata * pubspec formatting * total lints update * changed from tour_of_beam to tour-of-beam in build.gradle.kts * license check * _SdkButton mimics Radio button * renamed MyApp to TourOfBeamApp * onChanged mimics Radio button Tour of Beam frontend blank project [Tour of Beam][Frontend][#22600] TourScreen layout TourScreen layout (#22600) common theme, constants, split view missing license flutter_gen, summary layout details content layout details no functional widgets in split view main screen todos & translation main screen todos & translation comment fixes #1 ExpansionTileWrapper SplitViewController lists in tour screen widgets comment fixes #1 (31.08) split view package in PGC fixed button overflow splitter theme color comment fixes #2 (31.08) gradlew check welcome screen overflow test (#22600) SDK dropdown (#22600) flexible complete unit OutlinedButton (#22600) renamed PageContainer to TobScaffold dropdown style refinement DropdownButton implicit type sdk instead of e licenses #22600 renamed _ShrinkedTour to _NarrowTour #22600 tour screen style refinement #22600 BeamDivider in PGC #22600 removed todo, added license #22600 built with text #22600 _WideWelcome with IntrinsicHeight (#22600) Co-Authored-By: darkhan.nausharipov --- .../frontend/assets/png/welcome-laptop.png | Bin 0 -> 121755 bytes .../assets/svg/summary-hide-arrow.svg | 21 + .../frontend/assets/svg/unit-progress-0.svg | 19 + .../frontend/assets/svg/unit-progress-100.svg | 19 + .../frontend/assets/translations/en.yaml | 4 + .../frontend/integration_test/app_test.dart | 2 +- .../components/expansion_tile_wrapper.dart | 41 ++ .../frontend/lib/components/filler_text.dart | 29 ++ .../frontend/lib/components/footer.dart | 36 +- .../frontend/lib/components/logo.dart | 13 +- .../{page_container.dart => scaffold.dart} | 11 +- .../frontend/lib/components/sdk_dropdown.dart | 66 ++++ .../components/sign_in/sign_in_button.dart | 7 +- .../sign_in/sign_in_overlay_content.dart | 18 +- .../lib/components/test_screen_wrapper.dart | 40 ++ .../frontend/lib/constants/sizes.dart | 30 +- learning/tour-of-beam/frontend/lib/main.dart | 7 +- .../frontend/lib/pages/tour/screen.dart | 358 ++++++++++++++++++ .../frontend/lib/pages/welcome/screen.dart | 68 ++-- learning/tour-of-beam/frontend/pubspec.lock | 273 +++++++++++++ learning/tour-of-beam/frontend/pubspec.yaml | 5 + .../frontend/test/overflow_test.dart | 34 ++ .../assets/png/beam-logo.png | Bin 0 -> 1752 bytes .../playground_components/assets/svg/drag.svg | 23 ++ .../assets/svg/theme-mode.svg | 20 + .../assets/translations/en.yaml | 20 + .../lib/playground_components.dart | 34 ++ .../lib/src}/constants/colors.dart | 8 +- .../lib/src}/constants/links.dart | 2 +- .../lib/src/constants/names.dart | 21 + .../lib/src/constants/sizes.dart | 51 +++ .../lib/src/enums/complexity.dart | 23 ++ .../lib/src/theme/color_provider.dart | 20 +- .../lib/src}/theme/switch_notifier.dart | 2 +- .../lib/src}/theme/theme.dart | 71 ++-- .../lib/src/widgets}/complexity.dart | 15 +- .../widgets}/dismissible_overlay.dart | 0 .../lib/src/widgets/divider.dart | 34 ++ .../lib/src/widgets/logo.dart | 26 +- .../lib/src/widgets/split_view/pan.dart | 31 ++ .../lib/src/widgets/split_view/widget.dart | 62 +++ .../lib/src/widgets}/toggle_theme_button.dart | 10 +- .../playground_components/pubspec.yaml | 20 +- .../test}/theme/switch_notifier_test.dart | 2 +- 44 files changed, 1410 insertions(+), 186 deletions(-) create mode 100644 learning/tour-of-beam/frontend/assets/png/welcome-laptop.png create mode 100644 learning/tour-of-beam/frontend/assets/svg/summary-hide-arrow.svg create mode 100644 learning/tour-of-beam/frontend/assets/svg/unit-progress-0.svg create mode 100644 learning/tour-of-beam/frontend/assets/svg/unit-progress-100.svg create mode 100644 learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart create mode 100644 learning/tour-of-beam/frontend/lib/components/filler_text.dart rename learning/tour-of-beam/frontend/lib/components/{page_container.dart => scaffold.dart} (85%) create mode 100644 learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart create mode 100644 learning/tour-of-beam/frontend/lib/components/test_screen_wrapper.dart create mode 100644 learning/tour-of-beam/frontend/lib/pages/tour/screen.dart create mode 100644 learning/tour-of-beam/frontend/test/overflow_test.dart create mode 100644 playground/frontend/playground_components/assets/png/beam-logo.png create mode 100644 playground/frontend/playground_components/assets/svg/drag.svg create mode 100644 playground/frontend/playground_components/assets/svg/theme-mode.svg create mode 100644 playground/frontend/playground_components/assets/translations/en.yaml create mode 100644 playground/frontend/playground_components/lib/playground_components.dart rename {learning/tour-of-beam/frontend/lib => playground/frontend/playground_components/lib/src}/constants/colors.dart (92%) rename {learning/tour-of-beam/frontend/lib => playground/frontend/playground_components/lib/src}/constants/links.dart (98%) create mode 100644 playground/frontend/playground_components/lib/src/constants/names.dart create mode 100644 playground/frontend/playground_components/lib/src/constants/sizes.dart create mode 100644 playground/frontend/playground_components/lib/src/enums/complexity.dart rename learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart => playground/frontend/playground_components/lib/src/theme/color_provider.dart (77%) rename {learning/tour-of-beam/frontend/lib/config => playground/frontend/playground_components/lib/src}/theme/switch_notifier.dart (98%) rename {learning/tour-of-beam/frontend/lib/config => playground/frontend/playground_components/lib/src}/theme/theme.dart (65%) rename {learning/tour-of-beam/frontend/lib/components => playground/frontend/playground_components/lib/src/widgets}/complexity.dart (84%) rename playground/frontend/playground_components/lib/{ => src/widgets}/dismissible_overlay.dart (100%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/divider.dart rename learning/tour-of-beam/frontend/lib/constants/assets.dart => playground/frontend/playground_components/lib/src/widgets/logo.dart (61%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart create mode 100644 playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart rename {learning/tour-of-beam/frontend/lib/components => playground/frontend/playground_components/lib/src/widgets}/toggle_theme_button.dart (88%) rename {learning/tour-of-beam/frontend/test/config => playground/frontend/playground_components/test}/theme/switch_notifier_test.dart (93%) diff --git a/learning/tour-of-beam/frontend/assets/png/welcome-laptop.png b/learning/tour-of-beam/frontend/assets/png/welcome-laptop.png new file mode 100644 index 0000000000000000000000000000000000000000..b356d7acae7b84773a291fd7a61a00612c18f9c1 GIT binary patch literal 121755 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 + + + + + + diff --git a/learning/tour-of-beam/frontend/assets/svg/unit-progress-0.svg b/learning/tour-of-beam/frontend/assets/svg/unit-progress-0.svg new file mode 100644 index 000000000000..37d5945870d5 --- /dev/null +++ b/learning/tour-of-beam/frontend/assets/svg/unit-progress-0.svg @@ -0,0 +1,19 @@ + + + + + diff --git a/learning/tour-of-beam/frontend/assets/svg/unit-progress-100.svg b/learning/tour-of-beam/frontend/assets/svg/unit-progress-100.svg new file mode 100644 index 000000000000..8eb0cf172d20 --- /dev/null +++ b/learning/tour-of-beam/frontend/assets/svg/unit-progress-100.svg @@ -0,0 +1,19 @@ + + + + + diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index b40cbfee09d6..6bc0e751e195 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -16,6 +16,7 @@ # under the License. ui: + builtWith: 'Built with Apache Beam' copyright: '© The Apache Software Foundation' darkMode: 'Dark Mode' lightMode: 'Light Mode' @@ -31,6 +32,9 @@ pages: signIn: ' sign in.' selectLanguage: 'Please select the default language (you may change the language at any time):' startLearning: 'Start learning' + tour: + summaryTitle: 'Table of Contents' + completeUnit: 'Complete Unit' dialogs: signInIf: If you would like to save your progress and track completed modules complexity: diff --git a/learning/tour-of-beam/frontend/integration_test/app_test.dart b/learning/tour-of-beam/frontend/integration_test/app_test.dart index 232234bb0565..e74b4d35624f 100644 --- a/learning/tour-of-beam/frontend/integration_test/app_test.dart +++ b/learning/tour-of-beam/frontend/integration_test/app_test.dart @@ -19,7 +19,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:tour_of_beam/components/toggle_theme_button.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:tour_of_beam/main.dart' as app; void main() { diff --git a/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart new file mode 100644 index 000000000000..e3811d53a7d2 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +class ExpansionTileWrapper extends StatelessWidget { + final ExpansionTile expansionTile; + const ExpansionTileWrapper({required this.expansionTile}); + + @override + Widget build(BuildContext context) { + return Theme( + data: Theme.of(context).copyWith( + hoverColor: Colors.transparent, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + dividerColor: Colors.transparent, + unselectedWidgetColor: Colors.grey, + colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.grey), + visualDensity: const VisualDensity(vertical: -4), + listTileTheme: const ListTileThemeData(dense: true), + ), + child: expansionTile, + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/filler_text.dart b/learning/tour-of-beam/frontend/lib/components/filler_text.dart new file mode 100644 index 000000000000..a40ed8788424 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/components/filler_text.dart @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +class FillerText extends StatelessWidget { + final int width; + const FillerText({required this.width}); + + @override + Widget build(BuildContext context) { + return Text(''.padRight(width, 'Just a filler text ')); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/footer.dart b/learning/tour-of-beam/frontend/lib/components/footer.dart index b42fbca162ca..930034dfeffe 100644 --- a/learning/tour-of-beam/frontend/lib/components/footer.dart +++ b/learning/tour-of-beam/frontend/lib/components/footer.dart @@ -18,10 +18,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../config/theme/colors_provider.dart'; -import '../constants/links.dart'; import '../constants/sizes.dart'; class Footer extends StatelessWidget { @@ -31,12 +30,25 @@ class Footer extends StatelessWidget { Widget build(BuildContext context) { return _Body( child: Wrap( - spacing: TobSizes.size16, + alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, children: [ - const _ReportIssueButton(), - const _PrivacyPolicyButton(), - const Text('ui.copyright').tr(), + Wrap( + spacing: BeamSizes.size16, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const _ReportIssueButton(), + const _PrivacyPolicyButton(), + const Text('ui.copyright').tr(), + ], + ), + // TODO(nausharipov): get version + Text( + '${'ui.builtWith'.tr()} 2.40.0', + style: const TextStyle( + color: BeamColors.grey3, + ), + ), ], ), ); @@ -50,9 +62,11 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { return Container( + width: double.infinity, + height: TobSizes.footerHeight, padding: const EdgeInsets.symmetric( - vertical: TobSizes.size4, - horizontal: TobSizes.size16, + vertical: BeamSizes.size4, + horizontal: BeamSizes.size16, ), decoration: BoxDecoration( color: ThemeColors.of(context).secondaryBackground, @@ -60,8 +74,6 @@ class _Body extends StatelessWidget { top: BorderSide(color: ThemeColors.of(context).divider), ), ), - height: TobSizes.footerHeight, - width: double.infinity, child: child, ); } @@ -75,7 +87,7 @@ class _ReportIssueButton extends StatelessWidget { return TextButton( style: _linkButtonStyle, onPressed: () { - launchUrl(Uri.parse(TobLinks.reportIssue)); + launchUrl(Uri.parse(BeamLinks.reportIssue)); }, child: const Text('ui.reportIssue').tr(), ); @@ -90,7 +102,7 @@ class _PrivacyPolicyButton extends StatelessWidget { return TextButton( style: _linkButtonStyle, onPressed: () { - launchUrl(Uri.parse(TobLinks.privacyPolicy)); + launchUrl(Uri.parse(BeamLinks.privacyPolicy)); }, child: const Text('ui.privacyPolicy').tr(), ); diff --git a/learning/tour-of-beam/frontend/lib/components/logo.dart b/learning/tour-of-beam/frontend/lib/components/logo.dart index e48b3985bf6b..913678c76bd5 100644 --- a/learning/tour-of-beam/frontend/lib/components/logo.dart +++ b/learning/tour-of-beam/frontend/lib/components/logo.dart @@ -17,9 +17,7 @@ */ import 'package:flutter/material.dart'; - -import '../constants/assets.dart'; -import '../constants/sizes.dart'; +import 'package:playground_components/playground_components.dart'; class Logo extends StatelessWidget { const Logo(); @@ -28,12 +26,9 @@ class Logo extends StatelessWidget { Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - TobAssets.beamLogo, - height: TobIconSizes.large, - ), - const _Text(), + children: const [ + BeamLogo(), + _Text(), ], ); } diff --git a/learning/tour-of-beam/frontend/lib/components/page_container.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart similarity index 85% rename from learning/tour-of-beam/frontend/lib/components/page_container.dart rename to learning/tour-of-beam/frontend/lib/components/scaffold.dart index 3500e1739273..921b6f4532ea 100644 --- a/learning/tour-of-beam/frontend/lib/components/page_container.dart +++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart @@ -17,17 +17,17 @@ */ import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; -import '../constants/sizes.dart'; import 'footer.dart'; import 'logo.dart'; +import 'sdk_dropdown.dart'; import 'sign_in/sign_in_button.dart'; -import 'toggle_theme_button.dart'; -class PageContainer extends StatelessWidget { +class TobScaffold extends StatelessWidget { final Widget child; - const PageContainer({ + const TobScaffold({ super.key, required this.child, }); @@ -38,9 +38,10 @@ class PageContainer extends StatelessWidget { appBar: AppBar( title: const Logo(), actions: const [ + SdkDropdown(), ToggleThemeButton(), SignInButton(), - SizedBox(width: TobSizes.size16), + SizedBox(width: BeamSizes.size16), ], ), body: Column( diff --git a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart new file mode 100644 index 000000000000..28ba218ff650 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; + +class SdkDropdown extends StatelessWidget { + const SdkDropdown(); + + @override + Widget build(BuildContext context) { + return _DropdownWrapper( + child: DropdownButton( + value: 'Java', + onChanged: (sdk) { + // TODO(nausharipov): change SDK + }, + items: const ['Java', 'Python', 'Go'] + .map( + (sdk) => DropdownMenuItem( + value: sdk, + child: Text(sdk), + ), + ) + .toList(), + isDense: true, + alignment: Alignment.center, + focusColor: Colors.transparent, + borderRadius: BorderRadius.circular(BeamSizes.size6), + ), + ); + } +} + +class _DropdownWrapper extends StatelessWidget { + final Widget child; + const _DropdownWrapper({required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(BeamSizes.size10), + padding: const EdgeInsets.all(BeamSizes.size2), + decoration: BoxDecoration( + color: Theme.of(context).hoverColor, + borderRadius: BorderRadius.circular(BeamSizes.size6), + ), + child: DropdownButtonHideUnderline(child: child), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart index b823ea71c1b1..4f4fbdf2fff5 100644 --- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart +++ b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart @@ -18,9 +18,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:playground_components/dismissible_overlay.dart'; +import 'package:playground_components/playground_components.dart'; -import '../../constants/sizes.dart'; import 'sign_in_overlay_content.dart'; class SignInButton extends StatefulWidget { @@ -47,8 +46,8 @@ class _SignInButtonState extends State { overlay?.remove(); }, child: const Positioned( - right: TobSizes.size10, - top: TobSizes.appBarHeight, + right: BeamSizes.size10, + top: BeamSizes.appBarHeight, child: SignInOverlayContent(), ), ), diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart index 80fb00805e25..bbbfa4e4a63d 100644 --- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart @@ -18,9 +18,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; -import '../../../constants/colors.dart'; -import '../../../constants/sizes.dart'; +import '../../constants/sizes.dart'; class SignInOverlayContent extends StatelessWidget { const SignInOverlayContent(); @@ -34,7 +34,7 @@ class SignInOverlayContent extends StatelessWidget { 'ui.signIn', style: Theme.of(context).textTheme.titleLarge, ).tr(), - const SizedBox(height: TobSizes.size10), + const SizedBox(height: BeamSizes.size10), const Text( 'dialogs.signInIf', textAlign: TextAlign.center, @@ -45,7 +45,7 @@ class SignInOverlayContent extends StatelessWidget { onPressed: () {}, child: const Text('ui.continueGitHub').tr(), ), - const SizedBox(height: TobSizes.size16), + const SizedBox(height: BeamSizes.size16), ElevatedButton( onPressed: () {}, child: const Text('ui.continueGoogle').tr(), @@ -63,11 +63,11 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - elevation: TobSizes.size10, + elevation: BeamSizes.size10, borderRadius: BorderRadius.circular(10), child: Container( width: TobSizes.authOverlayWidth, - padding: const EdgeInsets.all(TobSizes.size24), + padding: const EdgeInsets.all(BeamSizes.size24), child: child, ), ); @@ -81,9 +81,9 @@ class _Divider extends StatelessWidget { Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(vertical: 20), - width: TobSizes.size32, - height: TobSizes.size1, - color: TobColors.grey3, + width: BeamSizes.size32, + height: BeamSizes.size1, + color: BeamColors.grey3, ); } } diff --git a/learning/tour-of-beam/frontend/lib/components/test_screen_wrapper.dart b/learning/tour-of-beam/frontend/lib/components/test_screen_wrapper.dart new file mode 100644 index 000000000000..8146e336c3df --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/components/test_screen_wrapper.dart @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; +import 'package:provider/provider.dart'; + +class TestScreenWrapper extends StatelessWidget { + final Widget child; + const TestScreenWrapper({required this.child}); + + @override + Widget build(BuildContext context) { + return ThemeSwitchNotifierProvider( + child: Consumer( + builder: (context, themeSwitchNotifier, _) { + return MaterialApp( + theme: kLightTheme, + home: child, + ); + }, + ), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/constants/sizes.dart b/learning/tour-of-beam/frontend/lib/constants/sizes.dart index 187a0f60f952..f4677576a8a8 100644 --- a/learning/tour-of-beam/frontend/lib/constants/sizes.dart +++ b/learning/tour-of-beam/frontend/lib/constants/sizes.dart @@ -17,38 +17,10 @@ */ class TobSizes { - static const double size0 = 0; - static const double size1 = 1; - static const double size4 = 4; - static const double size6 = 6; - static const double size8 = 8; - static const double size10 = 10; - static const double size12 = 12; - static const double size16 = 16; - static const double size20 = 20; - static const double size24 = 24; - static const double size32 = 32; - static const double size36 = 36; - static const double size40 = 40; - static const double appBarHeight = 55; - static const double footerHeight = 30; + static const double footerHeight = 35; static const double authOverlayWidth = 300; } -class TobBorderRadius { - static const double small = 4; - static const double medium = 6; - static const double large = 8; - static const double xl = 28; -} - -class TobIconSizes { - static const double xs = 8; - static const double small = 16; - static const double medium = 24; - static const double large = 32; -} - class ScreenSizes { // TODO(nausharipov): name better static const medium = 1024; diff --git a/learning/tour-of-beam/frontend/lib/main.dart b/learning/tour-of-beam/frontend/lib/main.dart index 4c81d88a4f51..ffdb55c697d9 100644 --- a/learning/tour-of-beam/frontend/lib/main.dart +++ b/learning/tour-of-beam/frontend/lib/main.dart @@ -19,13 +19,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; import 'package:url_strategy/url_strategy.dart'; -import 'config/theme/switch_notifier.dart'; -import 'config/theme/theme.dart'; import 'locator.dart'; -import 'pages/welcome/screen.dart'; +import 'pages/tour/screen.dart'; void main() async { setPathUrlStrategy(); @@ -61,7 +60,7 @@ class TourOfBeamApp extends StatelessWidget { localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, - home: const WelcomeScreen(), + home: const TourScreen(), ); }, ), diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart new file mode 100644 index 000000000000..efe8fd39bffc --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -0,0 +1,358 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../../components/expansion_tile_wrapper.dart'; +import '../../components/filler_text.dart'; +import '../../components/scaffold.dart'; +import '../../constants/sizes.dart'; +import '../../generated/assets.gen.dart'; + +class TourScreen extends StatelessWidget { + const TourScreen(); + + @override + Widget build(BuildContext context) { + return TobScaffold( + child: MediaQuery.of(context).size.width > ScreenBreakpoints.twoColumns + ? const _WideTour() + : const _NarrowTour(), + ); + } +} + +class _WideTour extends StatelessWidget { + const _WideTour(); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + _ContentTree(), + Expanded( + child: BeamSplitView( + direction: Axis.horizontal, + pans: [ + Pan( + child: _Content(), + minWeight: 0.3, + ), + Pan( + child: _Playground(), + minWeight: 0.3, + ), + ], + ), + ), + ], + ); + } +} + +class _NarrowTour extends StatelessWidget { + const _NarrowTour(); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + _ContentTree(), + Expanded(child: _Content()), + ], + ), + DecoratedBox( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: ThemeColors.of(context).divider), + ), + ), + child: const _Playground(), + ), + ], + ), + ); + } +} + +class _ContentTree extends StatelessWidget { + const _ContentTree(); + + @override + Widget build(BuildContext context) { + return Container( + width: 250, + padding: const EdgeInsets.symmetric(horizontal: BeamSizes.size12), + child: SingleChildScrollView( + child: Column( + children: [ + const _ContentTreeTitle(), + ...[ + 'Core Transforms', + 'Common Transforms', + ].map((e) => _Module(module: e)).toList(), + const SizedBox(height: BeamSizes.size12), + ], + ), + ), + ); + } +} + +class _Module extends StatelessWidget { + final String module; + const _Module({required this.module}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _ModuleTitle(title: module), + ...['Map', 'Combine'].map((e) => _Groups(group: e)).toList(), + const BeamDivider( + margin: EdgeInsets.symmetric(vertical: BeamSizes.size10), + ), + ], + ); + } +} + +class _ContentTreeTitle extends StatelessWidget { + const _ContentTreeTitle(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: BeamSizes.size12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'pages.tour.summaryTitle', + style: Theme.of(context).textTheme.headlineLarge, + ).tr(), + ], + ), + ); + } +} + +class _ModuleTitle extends StatelessWidget { + final String title; + const _ModuleTitle({required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: BeamSizes.size6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.headlineMedium, + ), + const Padding( + padding: EdgeInsets.only(right: BeamSizes.size4), + child: ComplexityWidget(complexity: Complexity.basic), + ), + ], + ), + ); + } +} + +class _Groups extends StatelessWidget { + final String group; + const _Groups({required this.group}); + + @override + Widget build(BuildContext context) { + return ExpansionTileWrapper( + expansionTile: ExpansionTile( + tilePadding: EdgeInsets.zero, + title: _GroupTitle(title: group), + childrenPadding: const EdgeInsets.only( + left: BeamSizes.size24, + top: BeamSizes.size10, + ), + children: const [_Units()], + ), + ); + } +} + +class _Units extends StatelessWidget { + const _Units(); + + @override + Widget build(BuildContext context) { + return Column( + children: ['ParDo one-to-one', 'ParDo one-to-many'] + .map((e) => _Unit(title: e)) + .toList(), + ); + } +} + +class _Unit extends StatelessWidget { + final String title; + const _Unit({required this.title}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: BeamSizes.size18), + child: Row( + children: [ + _ProgressIndicator( + assetPath: Assets.svg.unitProgress100, + ), + Text(title), + ], + ), + ); + } +} + +class _GroupTitle extends StatelessWidget { + final String title; + const _GroupTitle({required this.title}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + _ProgressIndicator( + assetPath: Assets.svg.unitProgress100, + ), + Text( + title, + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ); + } +} + +class _ProgressIndicator extends StatelessWidget { + final String assetPath; + const _ProgressIndicator({required this.assetPath}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + left: BeamSizes.size4, + right: BeamSizes.size8, + ), + child: SvgPicture.asset(assetPath), + ); + } +} + +class _Content extends StatelessWidget { + const _Content(); + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height - + BeamSizes.appBarHeight - + TobSizes.footerHeight, + decoration: BoxDecoration( + color: ThemeColors.of(context).background, + border: Border( + left: BorderSide(color: ThemeColors.of(context).divider), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: SingleChildScrollView( + controller: ScrollController(), + child: const FillerText(width: 1000), + ), + ), + const _ContentFooter(), + ], + ), + ); + } +} + +class _ContentFooter extends StatelessWidget { + const _ContentFooter(); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: ThemeColors.of(context).divider), + ), + color: ThemeColors.of(context).secondaryBackground, + ), + width: double.infinity, + padding: const EdgeInsets.all(BeamSizes.size20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: OutlinedButton( + style: OutlinedButton.styleFrom( + primary: ThemeColors.of(context).primary, + side: BorderSide(color: ThemeColors.of(context).primary), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(BeamSizes.size4), + ), + ), + ), + child: const Text( + 'pages.tour.completeUnit', + overflow: TextOverflow.ellipsis, + ).tr(), + onPressed: () { + // TODO(nausharipov): complete unit + }, + ), + ), + ], + ), + ); + } +} + +class _Playground extends StatelessWidget { + const _Playground(); + + @override + Widget build(BuildContext context) { + return const Center(child: Text('Playground')); + } +} diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart index 2898e1584653..c8c212502c6a 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart @@ -20,20 +20,19 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:playground_components/playground_components.dart'; -import '../../components/complexity.dart'; -import '../../components/page_container.dart'; -import '../../config/theme/colors_provider.dart'; -import '../../constants/assets.dart'; -import '../../constants/colors.dart'; +import '../../components/filler_text.dart'; +import '../../components/scaffold.dart'; import '../../constants/sizes.dart'; +import '../../generated/assets.gen.dart'; class WelcomeScreen extends StatelessWidget { const WelcomeScreen(); @override Widget build(BuildContext context) { - return PageContainer( + return TobScaffold( child: SingleChildScrollView( child: MediaQuery.of(context).size.width > ScreenBreakpoints.twoColumns ? const _WideWelcome() @@ -48,16 +47,18 @@ class _WideWelcome extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Expanded( - child: _SdkSelection(), - ), - Expanded( - child: _TourSummary(), - ), - ], + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Expanded( + child: _SdkSelection(), + ), + Expanded( + child: _TourSummary(), + ), + ], + ), ); } } @@ -84,7 +85,7 @@ class _SdkSelection extends StatelessWidget { return Container( constraints: BoxConstraints( minHeight: MediaQuery.of(context).size.height - - TobSizes.appBarHeight - + BeamSizes.appBarHeight - TobSizes.footerHeight, ), color: ThemeColors.of(context).background, @@ -94,10 +95,9 @@ class _SdkSelection extends StatelessWidget { bottom: 0, left: 0, right: 0, - // TODO(nausharipov): use flutter_gen after merging child: Theme.of(context).brightness == Brightness.dark - ? Image.asset(TobAssets.laptopDark) - : Image.asset(TobAssets.laptopLight), + ? Image.asset(Assets.png.laptopDark.path) + : Image.asset(Assets.png.laptopLight.path), ), const SizedBox(height: 900), Padding( @@ -106,7 +106,7 @@ class _SdkSelection extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: const [ _IntroText(), - SizedBox(height: TobSizes.size32), + SizedBox(height: BeamSizes.size32), _Buttons(), ], ), @@ -124,7 +124,7 @@ class _TourSummary extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric( - vertical: TobSizes.size20, + vertical: BeamSizes.size20, horizontal: 27, ), child: Column( @@ -164,7 +164,7 @@ class _IntroText extends StatelessWidget { Container( margin: const EdgeInsets.symmetric(vertical: 32), height: 2, - color: TobColors.grey2, + color: BeamColors.grey2, constraints: const BoxConstraints(maxWidth: 150), ), RichText( @@ -198,7 +198,7 @@ class _Buttons extends StatelessWidget { const _Buttons(); void _onSdkChanged(String value) { - // TODO(nausharipov): select the language + // TODO(nausharipov): change sdk } @override @@ -249,7 +249,7 @@ class _SdkButton extends StatelessWidget { backgroundColor: ThemeColors.of(context).background, side: groupValue == value ? null - : const BorderSide(color: TobColors.grey1), + : const BorderSide(color: BeamColors.grey1), ), onPressed: () { onChanged(value); @@ -293,13 +293,13 @@ class _ModuleHeader extends StatelessWidget { child: Row( children: [ Padding( - padding: const EdgeInsets.all(TobSizes.size4), + padding: const EdgeInsets.all(BeamSizes.size4), child: SvgPicture.asset( - TobAssets.welcomeProgress0, + Assets.svg.welcomeProgress0, color: ThemeColors.of(context).progressBackgroundColor, ), ), - const SizedBox(width: TobSizes.size16), + const SizedBox(width: BeamSizes.size16), Expanded( child: Text( title, @@ -315,7 +315,7 @@ class _ModuleHeader extends StatelessWidget { 'complexity.medium', style: Theme.of(context).textTheme.headlineSmall, ).tr(), - const SizedBox(width: TobSizes.size6), + const SizedBox(width: BeamSizes.size6), const ComplexityWidget(complexity: Complexity.medium), ], ), @@ -344,10 +344,8 @@ class _ModuleBody extends StatelessWidget { padding: _modulePadding, child: Column( children: [ - const Text( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam velit purus, tincidunt id velit vitae, mattis dictum velit. Nunc sit amet nunc at turpis eleifend commodo ac ut libero. Aenean rutrum rutrum nulla ut efficitur. Vestibulum pulvinar eros dictum lectus volutpat dignissim vitae quis nisi. Maecenas sem erat, elementum in euismod ut, interdum ac massa.', - ), - const SizedBox(height: TobSizes.size16), + const FillerText(width: 20), + const SizedBox(height: BeamSizes.size16), Divider( color: ThemeColors.of(context).divider, ), @@ -365,9 +363,7 @@ class _LastModuleBody extends StatelessWidget { return Container( margin: _moduleLeftMargin, padding: _modulePadding, - child: const Text( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam velit purus, tincidunt id velit vitae, mattis dictum velit. Nunc sit amet nunc at turpis eleifend commodo ac ut libero. Aenean rutrum rutrum nulla ut efficitur. Vestibulum pulvinar eros dictum lectus volutpat dignissim vitae quis nisi. Maecenas sem erat, elementum in euismod ut, interdum ac massa.', - ), + child: const FillerText(width: 20), ); } } diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index b0a898316f4b..c8d14482c60a 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "46.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.0" archive: dependency: transitive description: @@ -29,6 +43,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.1" characters: dependency: transitive description: @@ -43,6 +113,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" clock: dependency: transitive description: @@ -50,6 +127,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" collection: dependency: transitive description: @@ -57,6 +141,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + color: + dependency: transitive + description: + name: color + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" crypto: dependency: transitive description: @@ -71,6 +169,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + dartx: + dependency: transitive + description: + name: dartx + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" easy_localization: dependency: "direct main" description: @@ -113,6 +225,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" flutter: dependency: "direct main" description: flutter @@ -123,6 +242,20 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_gen_core: + dependency: transitive + description: + name: flutter_gen_core + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" + flutter_gen_runner: + dependency: "direct dev" + description: + name: flutter_gen_runner + url: "https://pub.dartlang.org" + source: hosted + version: "4.3.0" flutter_localizations: dependency: transitive description: flutter @@ -145,6 +278,13 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -157,6 +297,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "7.2.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" google_fonts: dependency: "direct main" description: @@ -164,6 +311,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" http: dependency: transitive description: @@ -171,6 +325,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -190,6 +351,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" js: dependency: transitive description: @@ -197,6 +365,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" matcher: dependency: transitive description: @@ -218,6 +400,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" nested: dependency: transitive description: @@ -225,6 +414,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -323,6 +519,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" process: dependency: transitive description: @@ -337,6 +540,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "6.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" shared_preferences: dependency: "direct main" description: @@ -393,6 +610,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" sky_engine: dependency: transitive description: flutter @@ -405,6 +636,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + split_view: + dependency: transitive + description: + name: split_view + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" stack_trace: dependency: transitive description: @@ -419,6 +657,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -447,6 +692,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + time: + dependency: transitive + description: + name: time + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" total_lints: dependency: "direct dev" description: @@ -538,6 +797,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "8.2.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" webdriver: dependency: transitive description: diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index 220bfd5e5bce..e34b474d281e 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: url_strategy: ^0.2.0 dev_dependencies: + build_runner: ^2.2.0 + flutter_gen_runner: ^4.3.0 flutter_test: { sdk: flutter } integration_test: { sdk: flutter } total_lints: ^2.17.0 @@ -51,3 +53,6 @@ flutter: - assets/translations/en.yaml - assets/png/ - assets/svg/ + +flutter_gen: + output: lib/generated/ diff --git a/learning/tour-of-beam/frontend/test/overflow_test.dart b/learning/tour-of-beam/frontend/test/overflow_test.dart new file mode 100644 index 000000000000..312edb8bd777 --- /dev/null +++ b/learning/tour-of-beam/frontend/test/overflow_test.dart @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tour_of_beam/components/test_screen_wrapper.dart'; +import 'package:tour_of_beam/pages/tour/screen.dart'; + +void main() { + testWidgets('WelcomeScreen overflow', (tester) async { + tester.binding.window.physicalSizeTestValue = const Size(500, 296); + // TODO(nausharipov): fix the failure + await tester.pumpWidget( + const TestScreenWrapper( + child: TourScreen(), + ), + ); + }); +} diff --git a/playground/frontend/playground_components/assets/png/beam-logo.png b/playground/frontend/playground_components/assets/png/beam-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cb196949a8bc648a3620aed6b10d5c03f2a0b652 GIT binary patch literal 1752 zcmV;}1}FK6P)UmpB^ChlP$<4hU%cO>Ic8t7gg-%J_(i7EMgC)iII-dh{> zdMD&!8{1$O`qo1H;X(hq5cqo~>|r1KekbZ)ANhMG=w=?!OcnBGApe6V=S>>^>H++~ zEd0MI>{uNB>Ja?CCHZwD{<|9f>Inb%rT)|i{=6UkzbOB?7X7^<{M%Rm>lpvh2>#Xw z|M#N*_nrOUP5?8i`AO7kL|Ni#<-ckR}3I6i8|LiIK z!!het8~^r|@nIhP)K>j{CH&S{{nAqMiB0{;LHpnb`JQM0@KpWIP5HVk_HH2kSF||M6S> z+DQG>LiMC4`kN>IjUw@SBJzJA`II00j2`^u9qL{j^o$$(z#8;}8vV-^_?{I0#S;F| z4fxpv|Nij*{pbDeoBro~{^fD~;#>EQJ^bc4@OLx&+cN#d?!-wXCF+j&WE@7ZLIC@zBo7z_6KD15&ZV000B? zNkl%a_G)c(_(;uCID)S9U}6u=a}n-ska8@1*zaE(_M{&)xf6S1V=j zKr&%S(spr|4P)E<*LL9>kX)Fz>uapvo@4ARz*=K`bh9P5u;aFwI~vT!tp#uSrR_FYvM#B7oOsWi^+TKDd8BH>dDv z8o1VQNfHP1@@gWu>|trC#SZ)v!ywbZ%^+JGPYg0KjCUL#$8Fe>BZ3>hOu*l;?1pHs z!We@e;Zp;^xRNgb<3M)tLGtccej{E?oAglHx zVmKnGx?S|ythtL#UOvqzDBz$MKeSWe`i~eCWY=Yg3g&1YhH(r0rnU=QN;opo+<;+z zT%nbo;L^j9FU$=9!;RVGx{*~t^e{JcLtUG#yUP2JLM9AD*K(AI_Chk@PEB@YffT*X zT?QP~Jr<(TXjsOnlgaHLGuc-Xlv*hIDsHmve-%Xj< zt(HsxQaBi78W_8F z;6E4^J472r0l^e=-`UCDq*c+dFL`j1J`_jwK{4y zxTJ7sm17lJ?*T3$T>BB!X;S;L#Q!$8_)pvReK0ODyndP;s6BjvCGVhm@nnmprry4D ux91rp!vaeYVi`NJEQ^rBnKNh3W~qO|3tnxQ`O01Z0000 + + + + + diff --git a/playground/frontend/playground_components/assets/svg/theme-mode.svg b/playground/frontend/playground_components/assets/svg/theme-mode.svg new file mode 100644 index 000000000000..fc1438aecf32 --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/theme-mode.svg @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/playground/frontend/playground_components/assets/translations/en.yaml b/playground/frontend/playground_components/assets/translations/en.yaml new file mode 100644 index 000000000000..9fa82e84b45d --- /dev/null +++ b/playground/frontend/playground_components/assets/translations/en.yaml @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +ui: + darkMode: 'Dark Mode' + lightMode: 'Light Mode' diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart new file mode 100644 index 000000000000..b59617a6b8f5 --- /dev/null +++ b/playground/frontend/playground_components/lib/playground_components.dart @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +library playground_components; + +export 'src/constants/colors.dart'; +export 'src/constants/links.dart'; +export 'src/constants/sizes.dart'; +export 'src/enums/complexity.dart'; +export 'src/theme/color_provider.dart'; +export 'src/theme/switch_notifier.dart'; +export 'src/theme/theme.dart'; +export 'src/widgets/complexity.dart'; +export 'src/widgets/dismissible_overlay.dart'; +export 'src/widgets/divider.dart'; +export 'src/widgets/logo.dart'; +export 'src/widgets/split_view/pan.dart'; +export 'src/widgets/split_view/widget.dart'; +export 'src/widgets/toggle_theme_button.dart'; diff --git a/learning/tour-of-beam/frontend/lib/constants/colors.dart b/playground/frontend/playground_components/lib/src/constants/colors.dart similarity index 92% rename from learning/tour-of-beam/frontend/lib/constants/colors.dart rename to playground/frontend/playground_components/lib/src/constants/colors.dart index b595f8a46a69..e5dad6a705a5 100644 --- a/learning/tour-of-beam/frontend/lib/constants/colors.dart +++ b/playground/frontend/playground_components/lib/src/constants/colors.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; -class TobColors { +class BeamColors { static const white = Colors.white; static const black = Colors.black; static const grey1 = Color(0xffDFE1E3); @@ -31,15 +31,15 @@ class TobColors { static const red = Color(0xffE54545); } -class TobLightThemeColors { +class BeamLightThemeColors { static const primaryBackground = Colors.white; - static const secondaryBackground = Color(0xffFEFDFD); + static const secondaryBackground = Color(0xffFCFCFC); static const grey = Color(0xffE5E5E5); static const text = Color(0xff242639); static const primary = Color(0xffE74D1A); } -class TobDarkThemeColors { +class BeamDarkThemeColors { static const primaryBackground = Color(0xff18181B); static const secondaryBackground = Color(0xff2E2E34); static const grey = Color(0xff3F3F46); diff --git a/learning/tour-of-beam/frontend/lib/constants/links.dart b/playground/frontend/playground_components/lib/src/constants/links.dart similarity index 98% rename from learning/tour-of-beam/frontend/lib/constants/links.dart rename to playground/frontend/playground_components/lib/src/constants/links.dart index 1d15c5c3656f..d55de92c8028 100644 --- a/learning/tour-of-beam/frontend/lib/constants/links.dart +++ b/playground/frontend/playground_components/lib/src/constants/links.dart @@ -16,7 +16,7 @@ * limitations under the License. */ -class TobLinks { +class BeamLinks { static const reportIssue = 'https://github.com/apache/beam/issues'; static const privacyPolicy = 'https://beam.apache.org/privacy_policy/'; } diff --git a/playground/frontend/playground_components/lib/src/constants/names.dart b/playground/frontend/playground_components/lib/src/constants/names.dart new file mode 100644 index 000000000000..346d03dba5c3 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/constants/names.dart @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class BeamNames { + static const package = 'playground_components'; +} diff --git a/playground/frontend/playground_components/lib/src/constants/sizes.dart b/playground/frontend/playground_components/lib/src/constants/sizes.dart new file mode 100644 index 000000000000..dd4cd994fc9f --- /dev/null +++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class BeamSizes { + static const double size0 = 0; + static const double size1 = 1; + static const double size2 = 2; + static const double size4 = 4; + static const double size6 = 6; + static const double size8 = 8; + static const double size10 = 10; + static const double size12 = 12; + static const double size16 = 16; + static const double size18 = 18; + static const double size20 = 20; + static const double size24 = 24; + static const double size32 = 32; + static const double size36 = 36; + static const double size40 = 40; + static const double appBarHeight = 55; + static const double splitViewSeparator = BeamSizes.size8; +} + +class BeamBorderRadius { + static const double small = 4; + static const double medium = 6; + static const double large = 8; + static const double xl = 28; +} + +class BeamIconSizes { + static const double xs = 8; + static const double small = 16; + static const double medium = 24; + static const double large = 32; +} diff --git a/playground/frontend/playground_components/lib/src/enums/complexity.dart b/playground/frontend/playground_components/lib/src/enums/complexity.dart new file mode 100644 index 000000000000..79767efa9b9b --- /dev/null +++ b/playground/frontend/playground_components/lib/src/enums/complexity.dart @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum Complexity { + basic, + medium, + advanced, +} diff --git a/learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart b/playground/frontend/playground_components/lib/src/theme/color_provider.dart similarity index 77% rename from learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart rename to playground/frontend/playground_components/lib/src/theme/color_provider.dart index 3aab5a1703ca..e3001708ac48 100644 --- a/learning/tour-of-beam/frontend/lib/config/theme/colors_provider.dart +++ b/playground/frontend/playground_components/lib/src/theme/color_provider.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../constants/colors.dart'; +import '../constants/colors.dart'; class ThemeColorsProvider extends StatelessWidget { final ThemeColors data; @@ -58,27 +58,27 @@ class ThemeColors { }) : _background = null; Color get divider => - isDark ? TobDarkThemeColors.grey : TobLightThemeColors.grey; + isDark ? BeamDarkThemeColors.grey : BeamLightThemeColors.grey; Color get primary => - isDark ? TobDarkThemeColors.primary : TobLightThemeColors.primary; + isDark ? BeamDarkThemeColors.primary : BeamLightThemeColors.primary; - Color get primaryBackgroundTextColor => TobColors.white; + Color get primaryBackgroundTextColor => BeamColors.white; - Color get lightGreyBackgroundTextColor => TobColors.black; + Color get lightGreyBackgroundTextColor => BeamColors.black; Color get secondaryBackground => isDark - ? TobDarkThemeColors.secondaryBackground - : TobLightThemeColors.secondaryBackground; + ? BeamDarkThemeColors.secondaryBackground + : BeamLightThemeColors.secondaryBackground; Color get background => _background ?? (isDark - ? TobDarkThemeColors.primaryBackground - : TobLightThemeColors.primaryBackground); + ? BeamDarkThemeColors.primaryBackground + : BeamLightThemeColors.primaryBackground); Color get textColor => - isDark ? TobDarkThemeColors.text : TobLightThemeColors.text; + isDark ? BeamDarkThemeColors.text : BeamLightThemeColors.text; Color get progressBackgroundColor => // TODO(nausharipov): reuse these colors after discussion with Anna diff --git a/learning/tour-of-beam/frontend/lib/config/theme/switch_notifier.dart b/playground/frontend/playground_components/lib/src/theme/switch_notifier.dart similarity index 98% rename from learning/tour-of-beam/frontend/lib/config/theme/switch_notifier.dart rename to playground/frontend/playground_components/lib/src/theme/switch_notifier.dart index 09851c200a2d..06f7bcad5725 100644 --- a/learning/tour-of-beam/frontend/lib/config/theme/switch_notifier.dart +++ b/playground/frontend/playground_components/lib/src/theme/switch_notifier.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'colors_provider.dart'; +import 'color_provider.dart'; const kThemeMode = 'theme_mode'; diff --git a/learning/tour-of-beam/frontend/lib/config/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart similarity index 65% rename from learning/tour-of-beam/frontend/lib/config/theme/theme.dart rename to playground/frontend/playground_components/lib/src/theme/theme.dart index d8cf2c1c2086..9544fbae167f 100644 --- a/learning/tour-of-beam/frontend/lib/config/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -19,39 +19,39 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../../constants/colors.dart'; -import '../../constants/sizes.dart'; +import '../constants/colors.dart'; +import '../constants/sizes.dart'; final kLightTheme = ThemeData( brightness: Brightness.light, - primaryColor: TobLightThemeColors.primary, - canvasColor: TobLightThemeColors.primaryBackground, - scaffoldBackgroundColor: TobLightThemeColors.secondaryBackground, - backgroundColor: TobLightThemeColors.primaryBackground, - textTheme: _getTextTheme(TobLightThemeColors.text), - textButtonTheme: _getTextButtonTheme(TobLightThemeColors.text), + primaryColor: BeamLightThemeColors.primary, + canvasColor: BeamLightThemeColors.primaryBackground, + scaffoldBackgroundColor: BeamLightThemeColors.secondaryBackground, + backgroundColor: BeamLightThemeColors.primaryBackground, + textTheme: _getTextTheme(BeamLightThemeColors.text), + textButtonTheme: _getTextButtonTheme(BeamLightThemeColors.text), outlinedButtonTheme: _getOutlineButtonTheme( - TobLightThemeColors.text, - TobLightThemeColors.primary, + BeamLightThemeColors.text, + BeamLightThemeColors.primary, ), - elevatedButtonTheme: _getElevatedButtonTheme(TobLightThemeColors.primary), - appBarTheme: _getAppBarTheme(TobLightThemeColors.secondaryBackground), + elevatedButtonTheme: _getElevatedButtonTheme(BeamLightThemeColors.primary), + appBarTheme: _getAppBarTheme(BeamLightThemeColors.secondaryBackground), ); final kDarkTheme = ThemeData( brightness: Brightness.dark, - primaryColor: TobDarkThemeColors.primary, - canvasColor: TobDarkThemeColors.primaryBackground, - scaffoldBackgroundColor: TobDarkThemeColors.secondaryBackground, - backgroundColor: TobDarkThemeColors.primaryBackground, - textTheme: _getTextTheme(TobDarkThemeColors.text), - textButtonTheme: _getTextButtonTheme(TobDarkThemeColors.text), + primaryColor: BeamDarkThemeColors.primary, + canvasColor: BeamDarkThemeColors.primaryBackground, + scaffoldBackgroundColor: BeamDarkThemeColors.secondaryBackground, + backgroundColor: BeamDarkThemeColors.primaryBackground, + textTheme: _getTextTheme(BeamDarkThemeColors.text), + textButtonTheme: _getTextButtonTheme(BeamDarkThemeColors.text), outlinedButtonTheme: _getOutlineButtonTheme( - TobDarkThemeColors.text, - TobDarkThemeColors.primary, + BeamDarkThemeColors.text, + BeamDarkThemeColors.primary, ), - elevatedButtonTheme: _getElevatedButtonTheme(TobDarkThemeColors.primary), - appBarTheme: _getAppBarTheme(TobDarkThemeColors.secondaryBackground), + elevatedButtonTheme: _getElevatedButtonTheme(BeamDarkThemeColors.primary), + appBarTheme: _getAppBarTheme(BeamDarkThemeColors.secondaryBackground), ); TextTheme _getTextTheme(Color textColor) { @@ -67,8 +67,14 @@ TextTheme _getTextTheme(Color textColor) { fontSize: 18, fontWeight: FontWeight.w400, ), - headlineLarge: _emptyTextStyle, - headlineMedium: _emptyTextStyle, + headlineLarge: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + headlineMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), headlineSmall: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -105,7 +111,7 @@ TextButtonThemeData _getTextButtonTheme(Color textColor) { return TextButtonThemeData( style: TextButton.styleFrom( primary: textColor, - shape: _getButtonBorder(TobBorderRadius.large), + shape: _getButtonBorder(BeamBorderRadius.large), ), ); } @@ -119,7 +125,7 @@ OutlinedButtonThemeData _getOutlineButtonTheme( primary: textColor, side: BorderSide(color: outlineColor, width: 3), padding: _buttonPadding, - shape: _getButtonBorder(TobBorderRadius.small), + shape: _getButtonBorder(BeamBorderRadius.small), ), ); } @@ -127,10 +133,10 @@ OutlinedButtonThemeData _getOutlineButtonTheme( ElevatedButtonThemeData _getElevatedButtonTheme(Color color) { return ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - onPrimary: TobColors.white, + onPrimary: BeamColors.white, primary: color, padding: _buttonPadding, - elevation: TobSizes.size0, + elevation: BeamSizes.size0, ), ); } @@ -138,15 +144,15 @@ ElevatedButtonThemeData _getElevatedButtonTheme(Color color) { AppBarTheme _getAppBarTheme(Color backgroundColor) { return AppBarTheme( color: backgroundColor, - elevation: TobSizes.size1, + elevation: BeamSizes.size1, centerTitle: false, - toolbarHeight: TobSizes.appBarHeight, + toolbarHeight: BeamSizes.appBarHeight, ); } const EdgeInsets _buttonPadding = EdgeInsets.symmetric( - vertical: TobSizes.size20, - horizontal: TobSizes.size40, + vertical: BeamSizes.size20, + horizontal: BeamSizes.size40, ); RoundedRectangleBorder _getButtonBorder(double radius) { @@ -157,4 +163,5 @@ RoundedRectangleBorder _getButtonBorder(double radius) { ); } +/// This is used to easily distinguish unimplemented text styles. const TextStyle _emptyTextStyle = TextStyle(); diff --git a/learning/tour-of-beam/frontend/lib/components/complexity.dart b/playground/frontend/playground_components/lib/src/widgets/complexity.dart similarity index 84% rename from learning/tour-of-beam/frontend/lib/components/complexity.dart rename to playground/frontend/playground_components/lib/src/widgets/complexity.dart index f8c28c1c0340..813d905f95db 100644 --- a/learning/tour-of-beam/frontend/lib/components/complexity.dart +++ b/playground/frontend/playground_components/lib/src/widgets/complexity.dart @@ -20,8 +20,7 @@ import 'package:flutter/material.dart'; import '../constants/colors.dart'; import '../constants/sizes.dart'; - -enum Complexity { basic, medium, advanced } +import '../enums/complexity.dart'; class ComplexityWidget extends StatelessWidget { final Complexity complexity; @@ -49,8 +48,8 @@ class _Dot extends StatelessWidget { Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(left: 1), - width: TobSizes.size4, - height: TobSizes.size4, + width: BeamSizes.size4, + height: BeamSizes.size4, decoration: BoxDecoration( shape: BoxShape.circle, color: color, @@ -58,8 +57,8 @@ class _Dot extends StatelessWidget { ); } - static const grey = _Dot(color: TobColors.grey4); - static const green = _Dot(color: TobColors.green); - static const orange = _Dot(color: TobColors.orange); - static const red = _Dot(color: TobColors.red); + static const grey = _Dot(color: BeamColors.grey2); + static const green = _Dot(color: BeamColors.green); + static const orange = _Dot(color: BeamColors.orange); + static const red = _Dot(color: BeamColors.red); } diff --git a/playground/frontend/playground_components/lib/dismissible_overlay.dart b/playground/frontend/playground_components/lib/src/widgets/dismissible_overlay.dart similarity index 100% rename from playground/frontend/playground_components/lib/dismissible_overlay.dart rename to playground/frontend/playground_components/lib/src/widgets/dismissible_overlay.dart diff --git a/playground/frontend/playground_components/lib/src/widgets/divider.dart b/playground/frontend/playground_components/lib/src/widgets/divider.dart new file mode 100644 index 000000000000..e76b793e0909 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/divider.dart @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +class BeamDivider extends StatelessWidget { + final EdgeInsets? margin; + const BeamDivider({this.margin}); + + @override + Widget build(BuildContext context) { + return Container( + margin: margin, + width: double.infinity, + height: 1, + color: Theme.of(context).dividerColor, + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/constants/assets.dart b/playground/frontend/playground_components/lib/src/widgets/logo.dart similarity index 61% rename from learning/tour-of-beam/frontend/lib/constants/assets.dart rename to playground/frontend/playground_components/lib/src/widgets/logo.dart index 1af152cc402b..febc64786fc3 100644 --- a/learning/tour-of-beam/frontend/lib/constants/assets.dart +++ b/playground/frontend/playground_components/lib/src/widgets/logo.dart @@ -16,19 +16,19 @@ * limitations under the License. */ -String _getPngPath(String fileName) { - return 'png/$fileName.png'; -} +import 'package:flutter/material.dart'; -String _getSvgPath(String fileName) { - return 'svg/$fileName.svg'; -} +import '../constants/sizes.dart'; +import '../generated/assets.gen.dart'; + +class BeamLogo extends StatelessWidget { + const BeamLogo(); -class TobAssets { - static final beamLogo = _getPngPath('beam-logo'); - static final themeMode = _getSvgPath('theme-mode'); - static final welcomeLaptop = _getPngPath('welcome-laptop'); - static final laptopDark = _getPngPath('laptop-dark'); - static final laptopLight = _getPngPath('laptop-light'); - static final welcomeProgress0 = _getSvgPath('welcome-progress-0'); + @override + Widget build(BuildContext context) { + return Image.asset( + Assets.png.beamLogo.path, + height: BeamIconSizes.large, + ); + } } diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart b/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart new file mode 100644 index 000000000000..33be8bb42464 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +class Pan { + final Widget child; + final double? minWeight; + final double? maxWeight; + + const Pan({ + required this.child, + this.minWeight, + this.maxWeight, + }); +} diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart b/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart new file mode 100644 index 000000000000..a9131b039d24 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:split_view/split_view.dart'; + +import '../../../playground_components.dart'; +import '../../constants/names.dart'; +import '../../generated/assets.gen.dart'; + +class BeamSplitView extends StatelessWidget { + final Axis direction; + final List pans; + + const BeamSplitView({ + required this.direction, + required this.pans, + }); + + @override + Widget build(BuildContext context) { + return SplitView( + gripSize: BeamSizes.splitViewSeparator, + gripColor: ThemeColors.of(context).divider, + gripColorActive: ThemeColors.of(context).divider, + indicator: SvgPicture.asset( + Assets.svg.drag, + package: BeamNames.package, + ), + viewMode: direction == Axis.horizontal + ? SplitViewMode.Horizontal + : SplitViewMode.Vertical, + controller: SplitViewController( + limits: pans + .map( + (e) => WeightLimit( + min: e.minWeight, + max: e.maxWeight, + ), + ) + .toList(), + ), + children: pans.map((e) => e.child).toList(), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/toggle_theme_button.dart b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart similarity index 88% rename from learning/tour-of-beam/frontend/lib/components/toggle_theme_button.dart rename to playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart index 2e0c84b68bf0..51c16f369e9c 100644 --- a/learning/tour-of-beam/frontend/lib/components/toggle_theme_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart @@ -21,9 +21,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; -import '../config/theme/switch_notifier.dart'; -import '../constants/assets.dart'; import '../constants/sizes.dart'; +import '../generated/assets.gen.dart'; +import '../theme/switch_notifier.dart'; class ToggleThemeButton extends StatelessWidget { const ToggleThemeButton(); @@ -37,11 +37,11 @@ class ToggleThemeButton extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric( - vertical: TobSizes.size4, - horizontal: TobSizes.size8, + vertical: BeamSizes.size4, + horizontal: BeamSizes.size8, ), child: TextButton.icon( - icon: SvgPicture.asset(TobAssets.themeMode), + icon: SvgPicture.asset(Assets.svg.themeMode), label: Text(text), onPressed: () { notifier.toggleTheme(); diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index 56037dea7696..499b1c972237 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -24,11 +24,27 @@ environment: flutter: '>=1.17.0' dependencies: + easy_localization: ^3.0.1 + easy_localization_loader: ^1.0.0 flutter: { sdk: flutter } - total_lints: ^2.17.4 + flutter_svg: ^1.0.3 + google_fonts: ^3.0.1 + provider: ^6.0.3 + shared_preferences: ^2.0.15 + split_view: ^3.2.1 dev_dependencies: - flutter_lints: ^2.0.0 + build_runner: ^2.2.0 + flutter_gen_runner: ^4.3.0 flutter_test: { sdk: flutter } + total_lints: ^2.17.4 flutter: + uses-material-design: true + assets: + - assets/svg/ + - assets/png/ + - assets/translations/en.yaml + +flutter_gen: + output: lib/src/generated/ diff --git a/learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart b/playground/frontend/playground_components/test/theme/switch_notifier_test.dart similarity index 93% rename from learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart rename to playground/frontend/playground_components/test/theme/switch_notifier_test.dart index 2565c073d2a4..b94ee6958524 100644 --- a/learning/tour-of-beam/frontend/test/config/theme/switch_notifier_test.dart +++ b/playground/frontend/playground_components/test/theme/switch_notifier_test.dart @@ -17,7 +17,7 @@ */ import 'package:flutter_test/flutter_test.dart'; -import 'package:tour_of_beam/config/theme/switch_notifier.dart'; +import 'package:playground_components/src/theme/switch_notifier.dart'; void main() { group('theme mode', () { From e64d3c8707e5f5a966426b2c9e2f8ced7c0b5bf3 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Mon, 5 Sep 2022 18:05:23 +0600 Subject: [PATCH 02/17] addressing review comments #22600 replaced magic numbers #22600 comments (#22600) comments #22600 comments #22600 comments #22600 comments #22600 comments, flutter 3.3.0 upgrade #22600 renamed ActionPadding to ActionVerticalPadding #22600 actions formatting #22600 --- .../frontend/assets/png/beam-logo.png | Bin 1752 -> 0 bytes .../frontend/assets/png/welcome-laptop.png | Bin 121755 -> 0 bytes .../assets/svg/summary-hide-arrow.svg | 21 --------- .../assets/svg/welcome-progress-0.svg | 2 +- .../components/expansion_tile_wrapper.dart | 2 +- .../frontend/lib/components/filler_text.dart | 2 +- .../frontend/lib/components/footer.dart | 2 +- .../frontend/lib/components/scaffold.dart | 21 +++++++-- .../frontend/lib/components/sdk_dropdown.dart | 6 +-- .../frontend/lib/pages/tour/screen.dart | 16 ++++--- .../frontend/lib/pages/welcome/screen.dart | 16 ++++--- learning/tour-of-beam/frontend/pubspec.lock | 41 ++++++++---------- .../common}/test_screen_wrapper.dart | 0 .../frontend/test/overflow_test.dart | 2 +- .../lib/playground_components.dart | 2 - .../lib/src/constants/sizes.dart | 12 ++--- .../lib/src/theme/color_provider.dart | 4 -- .../lib/src/widgets/divider.dart | 2 + .../lib/src/widgets/logo.dart | 2 + .../lib/src/widgets/split_view/pan.dart | 4 ++ .../lib/src/widgets/split_view/widget.dart | 10 ++--- .../lib/src/widgets/toggle_theme_button.dart | 18 +++----- 22 files changed, 84 insertions(+), 101 deletions(-) delete mode 100644 learning/tour-of-beam/frontend/assets/png/beam-logo.png delete mode 100644 learning/tour-of-beam/frontend/assets/png/welcome-laptop.png delete mode 100644 learning/tour-of-beam/frontend/assets/svg/summary-hide-arrow.svg rename learning/tour-of-beam/frontend/{lib/components => test/common}/test_screen_wrapper.dart (100%) diff --git a/learning/tour-of-beam/frontend/assets/png/beam-logo.png b/learning/tour-of-beam/frontend/assets/png/beam-logo.png deleted file mode 100644 index cb196949a8bc648a3620aed6b10d5c03f2a0b652..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1752 zcmV;}1}FK6P)UmpB^ChlP$<4hU%cO>Ic8t7gg-%J_(i7EMgC)iII-dh{> zdMD&!8{1$O`qo1H;X(hq5cqo~>|r1KekbZ)ANhMG=w=?!OcnBGApe6V=S>>^>H++~ zEd0MI>{uNB>Ja?CCHZwD{<|9f>Inb%rT)|i{=6UkzbOB?7X7^<{M%Rm>lpvh2>#Xw z|M#N*_nrOUP5?8i`AO7kL|Ni#<-ckR}3I6i8|LiIK z!!het8~^r|@nIhP)K>j{CH&S{{nAqMiB0{;LHpnb`JQM0@KpWIP5HVk_HH2kSF||M6S> z+DQG>LiMC4`kN>IjUw@SBJzJA`II00j2`^u9qL{j^o$$(z#8;}8vV-^_?{I0#S;F| z4fxpv|Nij*{pbDeoBro~{^fD~;#>EQJ^bc4@OLx&+cN#d?!-wXCF+j&WE@7ZLIC@zBo7z_6KD15&ZV000B? zNkl%a_G)c(_(;uCID)S9U}6u=a}n-ska8@1*zaE(_M{&)xf6S1V=j zKr&%S(spr|4P)E<*LL9>kX)Fz>uapvo@4ARz*=K`bh9P5u;aFwI~vT!tp#uSrR_FYvM#B7oOsWi^+TKDd8BH>dDv z8o1VQNfHP1@@gWu>|trC#SZ)v!ywbZ%^+JGPYg0KjCUL#$8Fe>BZ3>hOu*l;?1pHs z!We@e;Zp;^xRNgb<3M)tLGtccej{E?oAglHx zVmKnGx?S|ythtL#UOvqzDBz$MKeSWe`i~eCWY=Yg3g&1YhH(r0rnU=QN;opo+<;+z zT%nbo;L^j9FU$=9!;RVGx{*~t^e{JcLtUG#yUP2JLM9AD*K(AI_Chk@PEB@YffT*X zT?QP~Jr<(TXjsOnlgaHLGuc-Xlv*hIDsHmve-%Xj< zt(HsxQaBi78W_8F z;6E4^J472r0l^e=-`UCDq*c+dFL`j1J`_jwK{4y zxTJ7sm17lJ?*T3$T>BB!X;S;L#Q!$8_)pvReK0ODyndP;s6BjvCGVhm@nnmprry4D ux91rp!vaeYVi`NJEQ^rBnKNh3W~qO|3tnxQ`O01Z0000QCH#ab z_kAk-fi3rdEcSdV_IxS$ek=EUEBAaV_I)eU$->;ny5Zm5+Rn!1;osok-{aKD;oaKd z-rVQHs^Z$#)y&4#$-&9U$H~XW%Eraz%)I5bmF=c=C&z z#>&UV=Et%1^z`=i_3N*Q*4Ebl|Ns2_{PObh{`T|W;Na%w=lc5k^z`-k`1$AP>FVn1 z$;rv}_V(M{+wAP^_xJbA%*@!>*yQBp;^N})@bTK(+Sb+9_N^6!U-k$`}U-rwMagpPW8g^i4smX@BAlbm*Tf_;66nVF%AijS}9v@agArbASEq>9e%D&CSxbx4p~D(5kDl=HK4o+uPgR-}mqA z`SI?^$dN^o1wtK#@Er(@|kxs-vW=!NSMzj%}x?u*=5CR8?g5xuo-> zi1etBot~$opQW&?u*bs1w6C=Bm36qYx4^r;`qjwuo`KKF%f!FIOH5q+<>C3vy-l@u z_{X(&#g<^bh4rnK_rk71LsX5x+@J!M*L4oa5T_poG-2-o){%ujg@i`{wAHtflqA#Fu!`wV&R-$i%?o z&e`wc+0O31tK!7Z%hAK?&Fb0ja69bx@W{93@1UjgwYig}pXBrE>02x_HAjk`m+*mA zi)qU)FhYZvk>p=zDJwpAj)o*AIqHc+HK##>}9(E0oVgcL_t(|+U&uB00000fS~=U7pVdO0000000000 z000000GNF1jklpqm{0@xQhgrxL3g7ZD zA%s&E;Y47!&KOxi8`KZh=2+mmB87if&zMZrXC1s$7r<@VMygm5*g%R@J8l5pC1*QHpR z9@b<^Phc03w;e)w42*yLsX~A)-LG0*WWT57N_4o6$8wC3kzL{)k`ThV&|^zLpN$+) zISjqF*8c(0-;E`$eA&H|KLU9B^nW?xr}74%(KVciIkD=?#11q7w-Aq z!+#fHpL8OOYa67Sx{93ExWE1WgTxTR$vu%xnr#klOKpZUVCB@HZBa+OEMu0%-*)7+ zjmN>8h1A3>%1${qdb%D$MDiKp5S9Q9vmf8=^VFKove$7th~}U*RLm6AH;jFwNw6sB z5&qOWy08K0UylpH()1aw>@RPq$x)##@m-D|_RdDFbr1%_OV<0pW-TU)$=clO1W2# zKOGLzRPRER$y*Pk%}ac-Qs9#0MZ^UX_$ZG+(a^5-;U*%H5M%@c&G8b0a5|;Iu*#>n zw2`9lX5`X-03e2lFe9LPkt#X8xHPddUrmx%AXZRMZ8s2bI)Y|kHP9UB4KGAL1>Qc& zqO)&N@Rkf9BgRE{H)mqy*#+VflSk)jlO%aI;?}w%b;6g_ddMJp3b7Y)*G~UA48@=s zpl`O$t#dH z@2ZV`i5qON-{m*T8!2ftVmwE*JDh*9LYPctqBuA2C&|;1u!+>G+c^IUO|j>5Qc9H< zahr?jxExp>r)XsIOakYG#nRmI4U%5BBJ;{1H zM2$(}_c+EqNq#rd#~2VMJqh9LAqN{m<10Lf4xLIT9_vt1RmSwFGTcz=>x4vJ`ulEZ z8cC^jP*tVN4PHrd5jk=b!&R`u2}tN)yRB0m0nOS<#cQ^z)~7i)Tal<@(AFS|RyHr< z-sUoGf{)6@KNjX{lnp8L^VuMhDwiY=L)Ik26>FQMk$PdyQ>DnhR%9zg3t|S{hKOql zV`as+%+9Nfe7TQG#f_8t&kM&s-ylZyzojkj{ITRm@~FHS5ydjO3C8*mA`T-&oy%eevD# zyxtKxjo`Fzd}*8LJ0a+srMFf94uIQ1m%olga$!S5Z8nHcSo1WJBx%>TAztOl|CR_E z2CMV>B3|u6pRZVLLfsaOWjtu{V}p%nLhYEN0L7SM=h|93Xc~&fxlwt5aFTe$!ZGSn zF`?HZK1q5!Bh{GHnW{~&*aJ`7kF+hHSZi5Z#ci04Yc{51ewO6*3O66ZId_17n?peJ zv!|1!!RM&qGY5v_2hqn2ip8@Omi_4|ixpL?P_4~gu`zfQ7L&)~#aPN?QtU(tJxk@q zd!Bfq$ve4CKoHl?>7>XdNx{U4psiL&-XO1q`S4AxLe~%oI1k(qxTu$pct0L#T5sYRnHTfhIRK zJu6g|s&z4HRSQPcHZEQOmD@%4JjW@lDC?C)ug|x-^UcFvuF?+5h6%Z=EBQl+V3Xij z(VVJCE3DiF_Dg;ftq*%jOycA~-i`zXswS1nzzn_#KfxVfd(>(q?igVe9dj!)*yTsy zDma-Mhw%GPkxTvn(jG%5ZOffw>9;3PGOiTnomk&y>6zOIKZcl%DbWT?HO(M$dn(J) zlq9fN^#^Jc8p#_6bSiv(4{jn_y zniBQj;~Pu|z6snR24;QwF&=|G7i@*d;%5`_1yN>UrHN0D<*_|S8Ut1UTM-Nppq{dB z5N8rd*{Eg`IG232*DF!qo<%?JiuFP8l;swZZ3QO?E@sG4rKB{#5pVQrQX2o;w;&J<+&f&{$RC77E}?3(x_ zAW(WN%|!3jdjBWOO?(bGy|Hmhhj^%E^z96}Q30e3ER5w^sT8o7_4|g~_e@~IU52l6 z&~srL=um`cevJu%ojA#DRb&F;APb{s$o*D~^>N|2U0b~#yJq@ff8%ldW_#L7( zI1Y~ommoc(24#%TTXG?Q0O1wy|17gK@&5zfBCwDF$MN4khRcKDSj{#}8C+7_xe*^U z)7e&;WMHGu`pWz&y=bbN0Gl)wk>#r4cChZoY&u-=tWv$?*|uQTx<|~e>Jr;wH4Sdt;HnziDnzxe z>!w#FRq%b?wj|_iIw6YCn_Oh0O-^kxPEs!3gzZ|`O!#`9{cQbh!(?Az#>uPgvyufe z@*uJ(?t-p=iM-pOwl0n^zR@5YUrH|>0sj6Gvlcn`J(?ni$g@m^1+ z?(?MAG4Lr@m&fE2P^TDsj{`7wlT5rERoI__+h`Oj*XEj^Vo9b87|k`2(JK`RC|}fF zX2WRnek)i`iN&eMX-F9g>uXoe+GSq!bQn;XV1{dK)|nD3PocBg8=_&C_uIMe+6%bW zcv6W8v$)cTRTscN?B3keg;sD3AXT4=o z+iFhpp^yotnKIa}e1j3TTjw-o47N)X*dRfs0>$lF`i0Uk@LZaaw{Jq9NyzQYmK14- zj5oNgr1V(ZAod`w3u?x>!d%QzW(m)`SwC~)m0%G~m7{BdbBgm3Dp&e=D303COfXr- zN|oNa=9|+SG@)X%xPWLGiFuUdjx+_S?JoC(E6pKSU9W4JyYA~%&!n+lE@M?xBEpUA zChoqfvK0Ya8CDnwx5f+z6VarD-TKK7)WLMcyGM(-fMmX9Rzsh75td4a>6p%v3!pVW zhM%u6kbM;~Qr8HV6lU|4eu;a!SEzOx2A6b|7wQZpXr>N`R3| zRdZ+7nucT*B~c5;l3UBI(B0H7z@VRtLQIO{St4LMIB`DBX5yuQx*#s?iehk_g3!3O zm$Tq|PrLb7WigNwJ9mM2e5)`VK~cvJry0W--15>B*Ceki9411#Q`qaFeoHq2_0Udx zAzf|e#b$;BFrDW|boR^zn$wm<2(b=6u#kTP47f7H?&E|a9)&9~YmiL50Q}o}bFUOb z%_qEr-^O!J4L##6iCp3+32j46mzU!W*>Hm;2zREQb+uJr!e|VHyXCej@P9=;t3d}d{i;u;6yP zf^>>gIS@2GPjGAt>6WM@VyZ3h+g74nT}#5?G}A&RFjbAhQ~`3f0Xg9;D2jbEQ~)ei z!D65&+pYoEwIn%+F2_~$5D`NTCa7&J=slczY_A-|Ws-oE9S0|N`lb`Fz=Dd==2C_} z3F{SK*((@wiQ$E%;g}-6$}yOHRYLtbGuW(W6+iVN)E(7GXNT4Dk=YG^J8GPdC$BVa z)PTFttNqj>eUT1}RMW#MbIxSM@o#!e3xVV!!S(Tl-q&t<$qW?8I$S88Ia3`b-Vuv5 zJc*cY=pswF4O?%ZUWmS03G_K<9Osmz%Z5v88{I0?ZIh-P#|{u>H4ljHIjl**0Gd%1 zDhjyM-S79CynJA~DmL%uv(_fVmj7!%rBa-9syOG_;+&_+Zo+gGnk#dpvw~*Y!!o=e zM_GEd$&uxSmFH(By-mD3!lqqA{FH^-r00Wr92a*?TNRK3+SDe^95fO+J=5Kj1@=`W zJKB^YoY~eZ$Oan5n#hJZc6Z#exH0U3i=X3a@&NXl2#Ec%L?_U~0Cg6GGFKxoHAJy)13b6#NWn3uScxiTBR7GhE#?ge9`)mXKDw;mIJ5lFlm7; z!Gwn#3j)ZbfWa31P;c;=TV2LRZ};099QNbK{f8^o`-yZgWR3}Nd(#`}pMJkxp+hRD6ud*-~Dv@X#pf{ooayZ~N(~nEL zbKl6s`++#94;kR6I>+gRX(w{B$g5u(Mc%+r$aE|C4eVO!!?T}!14v13*#lLjfRGII zW>E66?k9U^w%j%hL(y)TdeX&7r%!F3qVfO#VtE0=M~H&Yhe715OL|L{l_SW#<)K>{WY6+k^%pj`6mU}SD z8*RDWW4W>Ek`{1loJJ%m(}Bip(-unWoNAjhx+eZPh$G-!*wud|847r8y&cc{BjtX_ zdZY5Z&2KAao7S}=Vf0DGX(n`qG~Y8^QOVAnf}obAZlbBBT%s@+fEym@;uiW_mPa$> zgdnj671j3`N7FcdME{Q)7Va+q;D#PV0N8)5gZ=8y>_4>0l z=EVO&I{-MZ7HkSm?+Bl!sQgAAPSZ_{eveK3! z7SYnpcxqHMuxKr!J;M?_Y7Vh(+=DIadJuSgKo|X);JF9CMt9XkKHlqthkDW+x!Z>l zGLdcIF8ZYs)|%&X6aOBoAG)YoE%I}>+AmkdvW|2&XRS{B^X`_kx9Rdh^6^@4H{O_SdPr_V8I~Qkg>PDv zt5^CtiI#>-=d1upxIgH}{_ZZ?9_XzxxU7md!#Pl2RI-P7)mg4A3GrSfIY1hV&{w~D z7Zea(R3ryW?wIC2Cb_`xdHKM{18nr(>gye5J^sW$0EZz27P?sqyX{vQHo~6OF0`Nt zxr^PR`Ufp~yEQo>^2I!+0QxjHd{U|n93*@!+Kg3<-IxyW_kUzCowxIt+#~E*BHE^>8zjPz2P%g0MHP1(L+K6+uWegKI1>t>4 z3(mSoUW~_H9p%~O_3~c4(b*3F}#miP~Espt(pXF&yeP_EJw?At5 zjR^_PZDV=Dc;H5KGKfu=7-REn4Zd;J+2|rtD-cZ;aW10B^Z6qADo9GyyE6)Rye~d9 z61*7Q6#)i&?s}0?heb#E7+$L&Da5H9Il+-N7aRvtiW>u~_ZP0FEz8NF6ZqDXoaw+5 ze}4ie348IrsaM+jl7vyrE4i_U7FV zqD=9gc5$e9PvQv>;izcpBbL4hHuYHLv#u(2ebi0N_LmgW%C0rRN!;H@j#lPHqS->| zGqaLw;_nPNG*l!%O>r!6U=!obqiIP>$WT(Bk=scNIKPz@zNyGoQ;G0ZxHjJBiE8s0ocgOxVys-;xIA{)h@)y|jPP>^{2BoMLY5Ba~gtw2G zcHKaqvFZ zaoPpSzJ?HMn-N@Av|X{CvfhWrk{ddqx2Yp3&(S8U2Ypj1fHnc?xr|TE)ZdAF!1opg z84knvQINz?>DHTAaowba)Oy2!lRcqzYC5~+A%*tLtz5F%v=(K70IWBMSVPzOECIr@ z3bv8n5!p+TSA5e0K~G)UR^AX?_jj~R$pO(dr~!$mH8ygE12sRGu5+BpaVW>&LX#UD z7cAF~(7(x!yR}hf|J`O96Qk$I=!yHVirTkhEG4o*4S{#b;Dy9^i+KwIA%@&~3*R)l zya|yUk`|cK41}nH%(vR_18&1DjYPF#A&J^_Rd84)%}g%8GuqM)zI`jJ;Kg?dcAiVR z=D*|x_}2@{?BF~L(?z<;Q|Glpb?$dZD?Ar)xy-w#uH{s*a%M%VB>bmE$B0 zQN%{*A?DgJi?unCD=KCj)4Cc%Zjb45`wgON$Bxtd+}Ul`s7~Bh!~j`AbXj)qE{+2G1Q!wG|o?c_Jxf3cCz-RZ8*)tkCkB=xX zus*dTD4~lu#M#5G{lOggA=J8kX5lb{A}G$Tn5Y;dS>d-d6S#9$^MR|9DH!Bt7l&~Y zvz7V8y$K>@eee@A?uTFTHyte@xIOFbgk!2p{6@Z(ipRIZrgRi1+pYIWiIjRhm2e?^ z(DNFuC?05v=dzwFj}=G&y`>M>7EkH}?WP3lNgaSI+XrZ&Ip;X5u-HfvCi>vTx76gW zLk|)GdT07b8#@gpdxx4X5Ti8WUpn_z576iSHef98tZtaNdA_p?Jb^04KGS-o{FZav zUgVT1Sv0e(mP0c&Ms?#&QZLRa2_YU;Sl~7iK`!MIw-vZ=Hzey;MBqSfV?u2~MzGl1 z0pr=~OuycwlQixn;;KD(&K>QV9}K$4!{TYd5^dWht)Aopb|PmBlFK+QFLaaXGhcX`8TCO*U8^{WbB61;H-Fb+Ee(di|etK?)Zie3kn z!<4Iy=E~MpcdbWxeW7nA10R^vI8q#;2Z@a4j%!t2y2|leU6^ijh{MDPhPII_V&EME z2fMClOIsuL_S>gnUeg>(#0TPGk@RLoZ>LiVJKC=O@;LcUp0fZ zvfzT`W?7a5{mAo%mQ>XH+-9=g%#7&TyJ@Gpn-iN0PTuIF>t*soF@mm|mk|*Jl02~4 zI9P+uuGX2@ONbq6DtF z!D%G~x1JIbsuYub68faLiOh;?4?BY3aAIB0Qs1)h=~grHw0J?6>K42rE9k92A}Lv@ zAR%OE>SlRvz3JARw}}(7NW$|7x2^)xFwo#E2`yKC|CTWL4SE2k3y9ppzYPwp1jh!) zH7FkIz;gygjYV>Uui7=33z~!FwAxV*UliV^+Tn=f%wOHGPPyk^Cm3+T z{oNXyNDGLM=`#vdb>e9r0iTrbvotFKkwetWHh==tlsay><#KW<)8yxu!uw0%CQrY3gr~nH;M3n%kJs-Dnm!v8 zZ8}t&b3$#sYFjQ2or7^C9_Z6&i&wF8+PWQV&9t2$u5YO{rNF(6YY4Q}5n_td$4y+L zP*DTt!YQ?f{@h#GYj;#67VO?*y#Y9el5a)%9C8600l0d^TfIRcoB?bSc+-r+4ujbW zxYedxnGTf;VNwMFC9svSEdGdiF9qn3VKOlX4RT1ii)zf7AY8aiI)o|kU(R}sYs8#t zyJM!D(9NV9e@qkrw=~$bx;ZG^3X=T(DTLFvTAzIO@$K6mfA-nNsOzsy9vEj>aFiDD zb(3-NR&_4O(cC73a8FI*oiY(A^-XL4bX%>r6mKfZ-Q?cYA@6n@bbB_%8|X;5mc`gG zxj>t6s$keHJ^UsIl8TUecCSUMcKLqRns?>i^=2lY!ETsgl#$ z@VZrt&#G^nfbkCO9HHv?W)Zu^pR%YjF=8VcTMJVzc0TN|*Nea$vq*|+U?3+)%&_Xe zx}+b!XR!lW_Id;1kTOS-y;pVTOe0=+-FTRdon;*~44iE1f9Nf8ccy=XRlSw|I#8<7_^W~I(dmt{Z%&$S`b4T=6AkJ=sj3c>kKapQ`u>JDsu7PcT zcK7`7=;+|j!?W4({8U+Ya&~lZcs!kcF*5ENs@M`}H*7aP0e=_T>I~mdC)^O`o31$( zZPmHc9B*0Xl(BAF(M#YB|J18^;hJ2jtOgCU&1@yuW`u!gdIbngUYZ1YDa905;V0pk z+`@G+3HJzI3pi0JakWTdCv2quG39+_h4B&i9w7(d+PkEQ@Ag@72D$(?f^4Vrli8WLqDO~&cTe^K8$$EhoqMO#pEgEa zA0-u$BuMLiQ&RK&Avg%v?yPT54iRn1wdKJ#Gu`CGeDfLP)P)qmH>UEYC2y5y>VT`7 z63lcHMY733ge~_&^+H2t+Vggz8?6^%U4^@eKPKkS8tNw4ba2iSuRtz}2G}|<=bS@3 zx{5*}I`oOd;+l%wctfNXD0_QcPj9AA2lHlLsolvc&?ir()T=MdvwJn1x!H!z7G;PD{%EH; zwi*GxPB)g2UHHT3A1&h?m}?Z3v?@uUoKIfL@(wOVT)tnLS#rtld>NV$4%EA4b@^u1 z&LG>t!Jh$a$DLq1-9J9LFJLQlcA}7*A0Ey>{di;4H4<%D7EwFY^7(aT`UdN~>&ih(3Cy_zqxp>GvK?vd1#6T%@3IZ_S-CBWnxJbUnIq}Ivo z+=6TdHU!wj7d<-%W5bM_AMKC9xa**F>JdGtWFLxxU;A=s|0&%IdhZXv5rH$d$q^-V ztClSkcN3)KYJ*gSiXyt3 zsn_~!59XY5ax8jSvEvG770j8kylQ7=wKn3c5}clVmzC&}Js`+-(hjl#w&iuUFNiys zjaJv*q4!t(W@r3R-sRRnU6XY;GSJx{8LzsiE4dj< zO3LIzT`R|UInhpB#!ANnkOOhpaWD?Rsi%r+D~`cRUZT58iLM9P_M1Vr**(B^37sh7 z_NQZu!*vk7S?QWxq@N6_XXnBLJ(Dd7zqz(dUCD)fMV7cn?EE8jS`V+J&9t!JsQmZV zgp_VdgjjH#=G(MobPd~`Ek4YJI z*0f4M4#;KQT-b2zN^7~QzY=8YD$#9c4_2ak>Zs0l$BgRL78N0SbJEeu2lj><7%o;G z@^IFkmC(&LIBj)rlW}FwGB}Xupwbmi)b&W zuROW&>B|k&g=ei!(3!MmSdtNGY|bZ_q^1Gt)S_@~Y8Qx$Ym#VN7bGQetNZ@#)Y!<)km z(T!C$nkwIX_>e~4vvh*CB7~C^LMzAy+G?-~m4@}EUiD{fRhAu;E7ZW8x!rEcX&tjF z8zW5LL*CnEVh^jA$Q4biG8b4GS1oqf%C>X;e7O?c)q<@Zi#s~r{dHs1RTUK?6p842 zJjk;)thdGpxU@e@j@I4PyR(9IGH(2W^ybuZSl$~H{5yggwu zDLWFt8fXJ+dM~96LSV~oZz3e0^57X?{a0(lZYWyUorO?QriuKK7Q2fka-jvty z;5&Y$F!>soL!=|D*tqao@mYz=xtP9`4I`=SIMzJG0utO+@6HeZ{z8!L8-i^6iy&Ll z3%Z&h+sgga(?7<#-z%wP?ps>fo33aBa|3j%om48|TJw>#Ly^gru{SHvozhq@(k1no zhpJ_7)ov^R2i{n91>n;0ZECrx%49+vY9sYoQq517b4(s>y%7#M5kF-z2#PT9AYKMK zI{=-lD+fWdms+xB$SLV!_nNnM0XbS82*|yyTOP3DR3XUrO`1Iz4zjHU*ifMF*=&r& zU0o4|W3P-#-g%(b8ZAt?fdw;Vej!$e)Y%11#cf;W)BvY)^Ub$6i}KWh0`6HkS*D=z zbD9;hoT>K98{isjYmg0rG(G!~_Qo4;7}t7b-$OeD=Ztwsyy9m1wOK=lB^N}woI*|k zS4@^8@4!YIr(Epl3PHASFHxd&1RQYd&V*vGVP_Rgi;v67 zarn9&rSNAxHBn_=QCd@iB}YH2cHA;HIPGEWYrXu4KJ06tc?NX&@JCAW91=U-&^ei+%BeCst5G;j_=7Lb1HnR+D&gp)xK$* z7Ov*oq@Y`#Dy5VyoGyG((gh{hmWSFq@5n9O--++xEojKV>!KWKiqdtp1aDnChs=&O z%86*!*J3$eTv$%dl&r(|-+%i}RiZoC2fxl>L&u>e?(XQOUO8>1RlfCisISNu#ua5E zz4-+!xHuUpU7N*VB=t|X%XNUV|0!Gy>aB7M(2YIwHoejk3$_J@Lf#8nZZ+DVnO@Pm zY`{#eT5r8GApgA@bM^pusK#8YrX(4;=?PX-I$e(LRqN{t60nsex)x+x>+HdW%YNr~ z#_rTBC?;Rp;B0(&O>yFTYx~45h z^8>?3*jhOG{ic3<4PnC2btUk<>2F# z3pwKCf#6)4jOb#V+$zRV6nrc3S6>vcbq3kis6-d7GMjoh|7By;8f!CqrMfrXxXW{J zgI24{KAbvaznNfoG$Jnc*Q>fmhlqQc0nU@Bf;hZW2K8Fmy|pYni}99OZ&uOU)u~V= zOc$nGU7m|_t`$^sf<>4usdw9;E2tMgs$AIy=!|n^6>O=57htK3`^4)$SUU z=%Q5y;`UBH-57Nhl%S6Nxxj6B%`>K3S>x+!h2M;BjPIqw7rvpib-^9fvU4G*D(el0 zd8e#qHny4hR&GwHYk0GCLc=L7JyGQJSq^IN+M)8N|KQ(u3(z@1+nmE^)r$@=t0uvg zD?C;SvVHOSBFJ`CN_5c*>qTaFcQ;0@uQtojTjr;dOJyZZUww#Om0AvJvR+1}9-7kf z5USrQ_ms-sotQj@Z?$iF)16cEPPNv-_!ir-M0HYuNg!P%ZiWSvrx1#`S_5QqDzEJP zTWZ;P&N)Zj8(pCDh6#Mu+Ln9$``-fCu5gL&%5{G_J=vR%$<(!%aI3cCM7b%~pFgcj zeC>>Cu6LHu8+@zdZ*k)(b9DdZ+K-k9<>S#+ABR>G8Qso@RfKB=qa zgf7Lh_rm#}-H?1PhujKb@44~UpdI#HtG=Y{Z+++c-@f|p^Q1)g?o}<(UAgYb;;k{6 zy5@3I%^sl6z5s3Va-mUhHfOrANsIb7-MpYzoNoC<-oQZdTeh%1oIAR}&aJnnEqe;y zvRO!~eEj%C)SWhkZ@{gCZl%%o27$Jgz1HHHG3~+vcL*6vE6QQdDU(qooMG-Q1KV{f z(OtRD!r%7MbX+XBz7l%Thl(?5cmIeKM3Zg}QR3Sekn}LCWkh5bI@%Yux1pQn7ZSR$ zZ6S(*NZ>69y4lVcYo$jGyd`0`!H64p18tSjrfib?KcQ~g)vhZgCTRlVhaUuN^uY(P z-MKC$x+_=s`#%`_w$@s7nZmcoG(A}}!*>^4#n2(9lWTbh8&DhdA=``mX|@pX?R;i$ zJv2n=ri9De>1oHR=ti|0o~gyCMWg0No-uKh+wX6(! zCWB%(_o5RFMnqL6q)fx29@qCmxtf1ID~ce^*$NA)7ZDbc2mZMOme_gGeT;-b>>QqZWh++ z%p(|ET38b)mHr7eOgb>Gx-n{FsKvR}pbbXMN<& z2Tii|h1I~*O7*_oO&=-Pz8*_-S1+cE93PIm8rDzXt;nR>w|vWz`G@9Qzlv z##g5Il0NMK+;fZ%p*d?OkH6*i+ahzSQ_G0G3EyhxwB{RZ%hL+^><{!VT8eBRfVF>A zAH^UWCA#NhiSDXJ$?u=j&o(zkt*abx3OSy{;FcmB;Wy~^P&eVscng>DjSYb&_fN%o zEBsR}@Wpr|y^t*fG`!KbFsY}#6|MB5b}?^00HpRym-0uKr_+ZY>M8m_p8hi*+9eM; zQi(4*T%x->W)D`Ub1C^93*y#K<*rjD9r*>)4E+(f)*gn=9#r*C!_A6B;(q6PfC%Y@ z=aS_M^e(sxy!pW1xS*b%`aZtlo3{GriMXK_-RjKfQb2=T%c=UyfB1bY(f!XP6XHg~ zt*I#T#bKog*q#lMUm(38SAPhOA`x$}uR83*E_8EGvj+QxcPdBSeCh+qZ#v(o3~?p< z=wE3Ty{YQn`60TIARUge3DEwg09(66cRpqh{w1Bts)n)Tcm2e+Q^Im447nb@=~Wi^ z%8C7kEPZM>>tYb830E(e33QXyfWCLZ{ZqWt(m%Z!PTS+0RlZGn+*2kYy;o?h?*xGcwzRfs%>0cQ(lyu3G@PPoN#8R44;;PSypHx+OAr!;0Yda_z& zA+%wURiOE*nNKK@7S&5N!P>AcUZT4{mgxR1o!^@QwqPWm+}+zdI{4%MbkUZDV8=5S+_tAsMQ*?CXmzg;&gk}ZR^LnRRPzgz z_68G9^*q5hRd;1bT@}(Dc6$%ij_LAt8&2nd#uD9sTj#+SJ>H)?*!IIe^1V9;e=Kxw zdcQFPb9OeC{H~v{;ShL}s{?Jk$RUuc)}XfRw}ME74+4Ys+0?dXqM=hKr$$?v|F{Ej7Y zYox_Ik@)H&(aSj0UJxSB}fe%WKGjq<& zSwL(a_LtxGj?KTryqq`wIo9%fAyeE|pzyN+ar#1VoKI4wZ}J+V-2x|&Bjs)cmsW5$ zW@$no{t&uT1LzHYqk6Mf-%uBX-%@&e0KLVssczs_+x7TtqVv8}2F&Cr{|Z;s-c4*BjFdh?f1 zSwR(rfg?#qK&?RiX6en{x2)|U+zzn>_fSCj7K#h%>`kRNfkJL7wy!Rs6;Vs(|{f!XrUizOkG2jHA7wy1=#GnB6#o1i|T&DSfF< zZQV_2i$ymN*}#ZHHXtI-@z2wW*bWS_EmHm~OWpGOww~~0f}0fn$v67c0+!cnM=&-u!b~koT8kz6>nW# z>G_6iM_o}>*`hmyMHgaQL2TNP&EQ~dhz<2L!C`rZTNOW3_<}HbKH{ z97}3YQ9TKjGnY|_JsS+w4H4f0$AXqcG=5_MN8Tx6&WHf`k zZGp4sGGs$N2m!I#A=|fom`%exTYk43MUbgw;eN(bQ}sPm`2vWjX@5!*s)Jk-smaX| z+_j3~2nclfSw6wBej4pBa2g4AQ&tz?H;vwO(X?ObrdxD>s21Hfc@KVAi|*mM)B}jE zH`Xnd-#6xyl*_m+fVW&v0uA8DpjH@CT-GjKLT^&NxenE@;K&HXAtDcP_%eVaT?Tp! z?QdtNLz>dEDBl{UZ#1X~ijA+pX=Kq(yBZh3K2Yu0P`Td*+aMJ;C z0iQ^a5y*A}Z<3hjhO}pSQ4|KEo*g2SZ_RH+FgS^U`YqzOJLXBYOigbrpGNeCx)88i zNN*?$?erG0TV&CF=UH@Dy)3$U$o3~Vr+Toq#I{B~_)YAUt2WDTk!6O7Q9x=qMPNB4 z1l4^h^;?v~K>a2UXMls?h_SaJ3>CexV}ILkP-Fj_fgQie!yQMWKfT*nzs1IobS0}v zHVqMNT{RBbvPD#zh3-~M%P>cANy0f003oha}P)Vp! z&a+J5sNZg|fhu1v-x&v-kXtZ+!~bTUHdiKIJw;tey&)E{?JYg*>mBdgJ+*KM>Z%fPF z@bv?<^b1M#b#nY6K`P)~zd?=7A&D;pf@`hbT8>T1#3x&H8?ap#-Qj@cSB<^@Xg);Q z^g9I#UuAITLm)ZVu=c(Khk}~caE{;16OS+5slUS@;DE1TI5xBxz*%?5ng8uxpf3o# zC-pe_!e}gibXd^Rc!`X zbo&q4AU1y6h`{oz#@@f-6PJs(aX}2KdG_EokOnbOza>5Dj&q1`r<_COeoo-b?PwWP zH+-qzZa%6N)Y#z5{4Fit1WNgaa@u4J$rjxap%2;iwISP4G-R6|N2=xbV6Xhlw)cgs zsg482aOkJxF~Dy<0|QBXPEK92gz74|Fz8t@a|N6qGlPIyQydCIUA_eh>it+hjr-Fl zao$4edwcQK4B4_pw-f&WbU=&0fJJv`ESfF9Cza**wOjbkhvLQs+G)XmE}b!2?hQai zRef;WNg`>64plI71m^?>6o%B`EA(b9A~Jrv*I<8(m$M>%3%b)6+8%ti=qec8qI+-- zi|$r2Tc_n$V((uu2r^$;Qym3G{YnMLeM@Mr(|C=4aIe~%w(?maQGruup)34}ei|)T^(H$Dg^03A7>orN{P2R&F1$3u) z;rKT|PE}M?AF9Z(vvY~8XB&oEui0OtfU^WA0)ykXI^)?q1*w1vz1i$68dU1HpnUxD+8G)~~yuot8PYHIHla(lk} zC5o!*W6{mM=${YSN{ikWT^X|db;O3{SG|F`n7_Xuz)PR^8#a%E!9XZtcT{kx(O0#> zQNCG<^TSZFz}XVcv740`SkVd<2I@BslvBTj*)mT*e90EwZv4L%-Jx;XX88^FzU4J9 zacvj{f8_>oT?fYj4#E|2gL<)LjUmz)NYb)P_M)E#XW)V*hG@k;;5P{Fe(evj^!8yl zvFLtZ?k}@NcX(jYJ^c$Ux_14HV3{e^D9~yGWGAaLGObxTqPI z_U!1537lXSbg1M63cL9PC+tQ91NoiQYGLQsd^C<7BAY1hv2`_n^L?xvy<(!lif`= zo86|`d5ke8SVZXKJMaA7_Zb-2d!WyO=u(B~%2J4KWXgHQ-s^E5dykH{RbQz1tw_}6 zm`fG5hT_@7+yKH!{^lNOUzcYuXK67Z=NUoIwIHoc!r)vQa9eEtRs%$~4jfg8ZgX`S z(M_yEL>Gz_h#N!fJv!d(m9n^q1R?~Sh_E^9H4d-MW3>>c<+2M7_J%Mmr@>(n9CJ7M zh5;$hqJo3JZ5}wf|G@4+6;;((t0Ssvl_I*(3Lw9|=(;U$QtDDmMj{cWwIst6&*;Lr z7jNvz&ERBzgEojuR5>dUVS!?ehzOc7J8;{uo;66aG`z^JKzQe_V>$8HYcLHOEuX#h3LlrNklg!;Y1FHbH?5uL>2(Pzf=VmrEG3y z^E@Hzvt}OWd50<_mX!+#4hDxKsz5~eLmW}zZ?!3g?E^>q`}!23+fWM8?VzZ6J1p!y zopnckvjX{*xAq_?V4ccR4PABdNm@@pMU zaem^@%HcvE(855f4ayOp!1uVp&;Ob^k<-8dP72NWwOCgMD9q!)I%bRS+P5D7|RaDi=A-buxgy?!2n`p8L zd*^c+H0Dd5K%78+aZs|gY(O~ zh&hWYU!wA#$P3(H85}dV{yv51Hq%-{bSV>|)?aC&xNw$qQZ%D!)``4Ux5k2rcq6}T zsKJJc;G~Mfs&coml2Q4OngAZ+rR;1qdF=CbR&Zvh7Pq(Vo?bNB>`_%}i>{;y5#98v zLv%&)MuiFqnwZrI$&$&JB!Hr8vISRTH*?{PdScq;?A-a;HimFPt5LHE1 zR8Q~nXPVULsnYlx`l@2wzVT<~tZ_eSOr2(*j6E)s$b}{w5Z!>H$8J6RI{*Id(&FNL zOqMtD8`t;9tINAAU+YqF_#p`YRlwf3q?MzP!{AWE$>m#jkJs&it`J=)Tw`0^O*W^V zOb$J{R{m&|wes|f>Ip+HRZdqaEzXhrwW>yLKfh8T4n>LR1~<0P^B+DfE&cj+#?t4Q zB*fm=Bfq2RU4R$N7g7nwe^EivA?!=C9+t1+wWq})SPKOlGqzqkqMNoMx~j=$spE() z`&W+|(WSPRZ|>!Zn;w*t3p3*d%_vRZOI1q0hqKjmb(Bs_&R(4;=jfgB%UJ?NhUf-1 zw)3|>e}}Qjdfx)_dqmo%U2`=IZmEpHz;ea|V(TRg&vR!v#1P$HFGM%1j1B56qN|_F zv5HMInKlxo#HE3j7e4p+;>?0ZT$DFb#;N;-p~BFHm9D|Bfm)x=souNs1(&;LW(`2o-xn>e54U4 za>O0cO({e-d;!tbVQfC<;LnZn+yfF5BBsMp5|t|Y#I5LTSJz^sVz$F3D*qf z0|m0%5QAv4wSwp-6rvjfqML2)_CQa=*j%M2n1)Dr(h9@e=8{iRi7Umz{cPpBarRsJfN?;*6@P-|G- zadkuuXl%H!0WIPfNQJRE5nVM0FDpt!*E^~zB{kXnwU^QfrVI&Dl^buyjmmWS;;Dt< zcf}Vs>E-cjm#AJnm(f37C0co*c)^>46V_(EWbX>`&pUzWLO;*6qgyda!^gdm-#)3x zK-pG%t!D6p-=WG;+oqP^JYwD+sHmzHR1id0H=ArNeM{#OOd1JM)At@5TIIo6BUz<4 zj7J4Jb?MPfI#e#)D_3S|X0DVKmL&iCe)ayH(oUT0Q&{F zBk>FDjLi5zfhkh3TIVMPr8O~4{O6qK{GRjp(pj!8v7_Z3HeXaPrAl#TE|4f!)WU1V zDbb+09BUg__4lllqvafDJXlYP?gL7=Gc@68ul!1e^7{*QGyFiJFPU1-P@3N7qSA#1 zj-q?tjP%k6Pp1dHBk(%GFXkKV7KcOiz~M6$t}VMUrNYg;W*?=U$YhB5wA);xikk7% z8gaCKqct3DSet00=nj_Z*w=7qul&;3drf=MaGuccVx0myL-4<51=phJ8bvlo(H$Ux zkscIXkN2j?hQ8qAb-djM922E@N?nQ{iF(v^NVvTTVb@hq8RKU29JSD!(PidV=XIR< zhZ!FfhRj~k9X!`#cc6FW*W+^k-8|)qeAN0o&T+%tmvA1BKYplmP-S>CEQgmOCqWgtWRgR+jZ4})RQgr)@ zz4GhNaI$Yy$d!bxSf(sxGe?)6a-L0wx$Vkal;eboDtBJ+Sw_Pc6dEToG*Mjy)u&Rq*^^YUbwJVetmXh?|-I~2J^XTe1Ow}-$k_j z`n(%O_k~kr`zE8RG;dRpZP4(YggE6R7fw{l;YxnH%qOQZC6*~=vfTO{i%L6L=8IgG zm$?>bk7?#v{h2CiwEgV3{$qkUu!|Jkfy3bzca|})e^l`S#9@oY%Wz5=cXBpX`%pc7 zKX&%MI#jJL)V`qnDt2S<8yz?kYnSTWGV(>9{q=_tTOAeM7tW}vhpp)LMi0tw=#meo zluPewa(RcX#>1(=&Q|<@DV6!%Ox(-Ff}*wiHY?sJ)XbctqMG%0ZB7pYUQc~5s*%q( z5SAHC0xQ`)?3DwsNN=jy-RxpM@8@vKY0Db!C4jYm%_!UdaDBKoc5yy-__p@4R$Hs~ zr2MLS!s*H{L0M**B2H6-JwNO5CTW5*s_JtdkD}X0Ncol9s=K4pGl7b_IhVdHl}pj` zPE1@`VjRyiosNzg*&cKXN>-csT>r9F(G3M%RBG%uD?x0|EuzlUD1clfta6F*c?H5a zZrRA^b216kGL9681ZM)EUhK#if!-Yy^{z-5p+CK z$s#6`@mU#rn;;k`z=Gr#_#$}ZKd!)q>wdl*$kitfW&vfV+5<0#Z9+NjUK&iB=? zQM))lxwiJAhi6v<47Oc&b*q=JpF?Kct$aW)%O^4v(*Z5mZ| z{%<|l_D?O)OO^3Fi-Kyh_7(Dx>zJiln4&4{on9ZoF$5UkMTKN_1uN7(P zQ!xG)2HyuCKoczLlSZc&k)57n*ePfUB*f_`ATvZWg@jPtJxGM0Ca0Mv?Go{INPg%P zyZ~`kyJN7`^o{zvi(@_YL3CDrY3#kJ{1VisN@otv1bpH*x9HJ-b3-nNuUB;WZ?EVQ zgHwJ9##y;mpL^hy?nBXqJh2nKc?Cj?C5EG^NvRm-BLyJhLpX#CCRf)1F0+TgG^7<_ zxgaz22~9=XkEjHKl;1B0+rA;gd>CQ?(e>DSLHgaCUq{f40lJLac*E&zFnI1B8F5x{ zc8ad{xD?%fBargj4#ot*?1Pugi7vPgUM~Z~v6(#-$ES;k<$&b_yj}v50%rV}l_#fS zVK0~S03!NNFxc%azZ7h5^?mvp1)Ck-Zj@i5`CNhG&C%kFJu1(ffFnisf%n1RJ9GzT z+go%S``|GMBn}`LFKvWy!&VlUPrsVO=;~?`sVJa?v0ehU6)Y)`*aR;`B-|g>4n?-> z_n$7V))pAN}7AF_;Z&3{$%^x*JVFS4Nrx>J5- z_phz_)@B?ub*&oDPxjKd-`s{M;&D9{%p#k+PPdBg_giG^Qu)2dTXgxPr~&+Tk%Ph1 zHW%k-i<9%Nt)sn&e|1{TK&bcu9K{q!<087jhekLIgrN^qWcymgj$`jt(f#}+Hwv$vjx-{UeUnKmhjNA9ee-Wh_}{&BQdT)hkNQ)+;cZmzGG43 zX)3z?#xvlA69OA)DB;*DvQr*EGO`xqcc$O0gZX9b#rb$pjin}`7X2E0xEs>e93X1r zLHIZ3r{tB?t2tmYJMvU56c+iVG^Xq%4AyQT-X3AU~SbmXcZ}S3xixehUVdJ zsFJ&88b%C2xvuj2#+lcz;4Ug$e7MK!sc)|>kDO9G{}Zn&x_8a3{Hoj$X-$YZ@k9VX zSZncdJ4!h%Xv)HN$BDv7b6ysi)K(IO3U^Ca*f{F@k%xUk?X)VUY zB&~gwn`<}0wK48OCeB^+7=|IPt^9h756=Vcjmz`x{Po<$2=4wqsGdHgp5{Ts^WS#q zB%8R*{ix`=?y~aBZGCpag-K>AB8l9TV!1ry%wX8e#p2r5sGn#hz7%`$F#!!9D>GW8 z+=P?UMzB!TDJ}H6l5dQ+A*Tb~68;RkLoJh_nkMc9>lq&I5*+$@l@O=mhZp&kQ#bb(Y;%1oa-hpsr-g-8E@Z; z?~b2*$C91$WcuuD&@VU>C~rWm@1)FX#?c6NrR3&f2`XlI!jhh_a+|OfWNQ~ESfBc1 zg7It@j6KXCNDzSCVinC?kTYhb-4?@K7LnLpjDiJ*BfU`11n>m8YnovgJ|J*i<@eWw zyFBFFEOYOqxv%%&e`o~$a}TM1ysGM&imvB372Q|QqF3{Jr~$;HVo@mbBB`8;@uwc0 z@GAj_Pru=tr$^7^$&c%tX$@)5sRH&^YZ9hW0{!a9DH?haCq?Tw`bvY%sHc$bb7=k1XHcO49~T=#_#_3Ov6@KiWMde zvRzTpvh{ojLJCw^vd7pJCkVnLSkW+529hZN1N?)Xz@o{q|f7R&@n zDK^LqC1iE(m@o_@bamx7a8da^cSY%U>4jB>``YzA_3oOgoQ`@uFSAz_-Mb^em9!a2 zY`^FZ6C;>clIcy`VX`Rbxyw%Z75tv?_rUe!;2DpOj-MTTd%(%FXW#JM3C92jj}F&a zvgbXC4=9($@g%FsMFCX5B)pX>8~NJ2%2GHVpZQ7(X|t2-CVV(~=x|05D)6T?6I_!S zbXy%n(Mio@UXP}W6O>GQPOHUYB7IPWNv`M>WqxIB0(YNq5kS6wRpobi2Iln-sqPqV z`SD^*cQ2P4-d)rDFX@i0R~6kmC$gLF=bpV&+s!gtWQMyrQ3E^0z>cMYj!{>u`bw2w zd$R#9!7+zVo*o{rADq6QJQYtKaSX8j>^s44DZ0v)HhEEFZ?+|^#4M+klhJe}-`cHn zPMx>=R7lQNb1D8or3;gK3U)v2h>#Stz$_KI)d5QnuqxBN}I8uGgAri%2Q! z2OSXn-Moh3{jbI!qhF(>4|CCwL zA_Vl{IKA*?8ts08QnC;-D|V4O#Oe(DRIJ1z%d{o_cm}4%A812<|MBU; zAy1FKgKxQonr4DF?SxtC)xx>Sd@#;;{h3xxtM9f_A?gd%?3Sa*U|VowsbVQ5!nTdv zNz}?_BDpt3$pFN|lw#z~!mi(zS*H*Vr-wmqlWY<|7u5RPOvEq@fVH)&E58?oI5*|4 z+=lPgeotTS-=UZ_XSCfu*biH|Kyy(}mOOgD8toV7R4#PoU;mU91lXkBM2HWdbQx#D!>m z!OaAo7ve%!ReqIEp2ypz3Fi*h&V%m$9;EcIC)}+nx`sFQD%e(_S<6qT0!J*vJn2fQK7vriX2`&6!0qRT_m_g_zu(vs>&1rF#nf2A3YA|WxH)MyePWy~*eoTjX`KqOG^I(W(JuuoWjrQl zS&LMY75}9ge_$mBTcJ_B<{>lj*JVXh3q&)eK8$17IhKYoO6@KyQ%3?f!!GF0(gm&qxHSp>{j3 z5KQm2!Ia3qa0Vu~cESZ?G1iE0?J3c$FTt5nHd-U?YsS^|W~Re1RSclapPVl6C+GwD3zo z`G%4(<2pH-JDVDa)z88-#!b#`O>C{5bzrvW6wRvFiktbIW2rTuF^*KNUfM~iB#V7_ z=FZ5*{z~Q7cXj30xAxW>Z@&2_3Flsge^2M&&b%M?x_cV_rmL#Ff$OiTawpk*QmjVx zaTC7((^IV#tpV5rE7qQEHuSixEu2x@g3;Jt@8+k|Y01$y24yoH6WkGHN$_XB{SI7T ze|HGao;>;X(ea~ombwYd8u2bf;uCv~U=_T3wuz%~3#y$&TR0EQ)^5MUQltcR(im~9 zbjS~_U1VpWvnr9q6{?jp<5;t}6_{)n8fqlA4>hO;>Y7~aIkh56mXHG*{H5#|WkW^N0OSDv;WIGTpTcK{ zhbQ9M(ZS(YKR!A-;m2=)Cx@Id(^lDGs|m8S)u|&i&b1Y;CN+LT)ojkhTjOxED4k$G zy=?&|)RO)@+w*)6vEkU9_n?tJ^RejoF*Z?S4RCG}=;~|;XikYmA zpSu@6OStQ=z5n>lYnB>Z?&+7m9%p{vws3urS%WhV-zCL4EsS#?H&u zLsS2L!w}PI)n>o8|FS1scOy8H695yX(AZOV6NyNC)4$uXoq0B%$PM#d_+40fQkL$-`{rJ%{zCGgLGvK?Y>+3A(gM~y02&G4hDXVEN zI3r=hX;aC^(>hD>;$C3XV5hBDlLdvz14*}-)p5!ys}L$KUpWCbzlmG<^yF{>*A>wpxTh!my%k+| z^+5d#!S?(^m`fy0ertQb)mLT1=zkxp{q#5&{eComeEQ`}zf7U$WUyOnG*rEVuUejO zg_4EcR=P1RG97WSxXR-1ly4Rey0{g8%}%ajVu#&r}D06fMZPfx@3L z!vd5FfCQ~Q)i6^6Ya?MZ-yq?wW{Sm7l|Wvr+JhEmCN%*?`Wg*Ugvx=_a2HT4J;EjuWHE&5u&8IG#CWcpvb7B5SKHlPj+11Me@-@Kx zxvl?{3_Tu;%zh{JLoiY0Wu-}J&@c7Xw3+T!l}5{+LJd7L+-b~It=>zI<|3XL=sho} z{4?Cjujh|n3(qDmdh#tle*5IwP3<~P&5M=PXbkc#2GeN{nb&|}pN$4iGP|L)nHK78k?Kq( z|0MOI@@rgo?fq%My?Je6#NDR#-2EE8{aX3ePLt~Iujt;M3>k*fUy7QOwOIr8!35i_o}hx3Bz)U5ghzJK^Y??+y=-P7b*xvC$TTMWVuJ z&`p>Cp|lpMm#b~t6U~I{ZAWp+J(6v}C%K*84ztPTpcbR<7q!s#V4j7z7Z~kPUNE(_ z@vI3FNa_5u%!@NH!{U2N?cTnsvp49FIHH(=;Eqk^`;t@ZY~Xcq<6$ z5PBDFTGH4VRIGe*vojh~k&AKFtWY5pq_?j@W2dO9evjl|;#PjW{DIv>f@^&p0r2z@ zH>1URl2NXrh{Cj|jI(5{R6L%KU`C*(G##1CIlJc0yc`H|1ELh;f~DbKY4Lbz&$lCTqRK&buurT2*BpX644LswURJ!_x7x#R}iRgbFAqwV5h^`1SXp58>G zDwkJ&y>756*IZR)x|3{fMfVO#E^7Ws2iDNHCT#ng@PF+ zkme;_^mr{v<{D8S%>8Q(|$*$O|b7DIsb za}r4BneHdmX-h@$&uVM9R1qBo>{2O|n>%yJ`r`)qNFAtJs;|&*q|EJN)-&3&q-qL4 zb!=V|X9=hC2T}@dLs$8tm}{{VOh&6zB2?7QUN2?j)lfN^Vsj`4nTEuta~l%b$nOR9 zL?y~_&!3LPyHv5Wg)FpfLJmulGMkIGhnLIQ8JO2re*N0oTkaUn4Y^CaaO|bKxO$(a zp5K4}InjJ^SHs zkV`_Sngk*S|Ca38-~S7FXZNB^8HVwq`ge+==rjsCyB>GvnHib1Mx8lu&_-u;{FqV1 z`~pEV6cI^QG1SCrgNg!+pq7oGr64+}h)$xDI@p6KI?7qyCd>VB+qR|stvm1f!t%nq zaP9N)yYIWh13eKDG1+0$yVH_NqH&;KPj$GgkOT0HYDz-ClgGNrOj+C-9Od^)e7u7t zT%{65C1#cd&68wd$)}$JKmB<4>8FQZeoR*Fdyzw_0wB`1f)Iuh zFVc#-B~L1ckE?*n`(kXBrR;Ki0qyZ~4kKs@ROZ?)vq1T)F5=4` zWGxV;9L688iFwWl;$Mfr2`AatlYxFJPwg=WLcfco2hRj8CW02s%VM(-z zvmpZ3ps3=6X1r1j=lrEHo6WWW_yNuG>P*~B#R17E;c5_pz^A9CY*kH}6fp}oihSYN zdzN|p{`>B8HmQ#JyRODL33vL%>Fw&BDQ`GYes?z~oI-8^wx{rncJOj zD>nOK9L2wcpJGJ>QxQOU#FTW6=%djXpyiyUF;?2Bv=I}*XS;c!pvGB%7dc!OMgknH zA}vnyt}KVcNE3^~sl5N{%lB~j#n)eadid$i*Nd7he2pJJ2Jqe& zJ1@XS-cK9T1becR$ zX}v=Vo`@9-B+4m(0A4nfapyY&6-jU1m$?zONTgC=27+>5on`C?&27%NxAs0bt~ zAvZxu#f{?qL-`G#sr>HVw^+k%*r=XS*YHQT-xYVKdTTLHP~5Ei^1B{#?a=YIu}PT| zp7m`I;%*9J(6Sdc!Jc9N=^P81l zihmIne};quO2lNE>3x58QlHZBD1rqIX;J|~5kZQL%zk~swE&2q*n(*k3s{Q?of;%l zuR~ES)tA`K6m%&MLvp9vO@yTe5==>*q?#m3agC6j=U@2b{SWP6d+XiLK6vNF7hc$L zc;Tgsdth36f4GTy+{&v=}ZMM!w1BW}*Eag`u|D4ibm2>`(lrXsDZ%2vl(R8#$c&-;U zc&MZN{yR_r8JwU1Qo7WQHDh9|?#}y?(F{b>rxzn|0fi=$odAV&%~ibW=r2Wfms4G~ zp=3)5Uy=^0h-OVq%rW^Q&!xF`D;VSAYS$})1vp$6gf4^arPmfKw%3=zwu!g!2b+~& z$`fA;aM<}~CBS`&Eg=C8(YRruX>$IX8Q(9`28G+&|Oqv2{ z!3?j5TFI0LFbh!~0ACp-5bUSmwNN@DSV}j`GWGPB{5Q`F;3|ZR+|Y z{Cn24UuSOWow-ZB_nt=?A}pKwgUUb8Lcu;i0&2 z3Frp9EZU^cs5z-z+SB}tj|Ut;u5~Z^{_2qJ^~GNF zJtx>U^2&u{?`wNK5t|^mJoQG2jI>c6&v>jHvI3y!L~3+2Im{z)s8;lQp4SXpIUTz%u$ul!rjC{r2Fsx88l_ zrPo%6Y?tEQoePy;4p)K%5gw;Tun{_ZE-;`J0$ z%T0ka;`y?ik5)lM1JJ`ri`G8CME1HS-!L@^X#MxiX8x}Hy3bdBIo}iaIT5!WaOd2q zE|+kt$K9w;z4s~cY~`0FXCZjWI&;S^d5g@C`uX`H`4r_yOU#D8pX)@= zkVux$Di;}&E!b2+aV=f(X`X|Ju=!rph)6ZFEySXxFTnvZAI-5blK~kPTh%lIC9YqF z&yDTi2_UwRXZHvS>I5(c0W+G4X340`Yi?7_2ub@^AsPucg6>)?Rn|S%e8{%AJ$U!z z_TaM*KCy#sJ6`){^Vs|CdHy}DDEE1UDtaF&W`6=3Z@dzlOiE}}N6k)_3uuuNtgW&$ zDuws4f}|#0K`F%ylv*VNS-+^#SVRqg)q(vspOBe2AnduKzFU`Ue^q|P&C0KMqWn7f z=0x0j$gS^ISF1N0XUqEkuWq>aiF1@+*9HOiWEbihmWTuIBGH3cfso$McJR?zM9~hy zIh}Nm`OeS0>nPKaSn{*of|68zP81Ff<~8z93F0aUr*(+jiOH6c3yqwZE&2V0bWtm? zv+0P}6kt@45~Q31Aon}eg)8AD#>j)wu%*TuX2=wCK*QpPgT5y#H1~vLN1Nukl+c2> zI#leKmBw`+vVG_rvR#4CKG~rBZV#~=oPV{R zB11ve?;UV;$nucw-IwfZ(br#n$2nxXIv>5iuKdclkWZh7lWTb4V~S6|J^bmtFHgs9 zMMO0A_#}YwxULYAXaLO$?+(YVD8~YB)i9xR=_pPpl>lS`q>6lk)51uT=AaS=$lFB+2Bmyvj65z%!xi*`DOl4e#OeZwg@a6 z*KhS`m6D-KqQirO4k2U~2`1-CWJU<-97}{ya%7P5addws_>Q||c$D=jpDu!PRmqk!_m@ALm5tTsFtQ-@eqgtr~2pQ}(X++FP1q!PJ@3by(1pmSz+gr{d z+ja55s~0K1>#TZTlAVut-dprA$4_7U_yzgu@Oj{cFR-NFX7da(07|iVwv1DRHZBh( zVG5gFb=re&1tQ2a(yL!-p#}tG0CW?By4S~TvEB7dz^hpSh&Bbl-6|4S%4`T|PI04f zw(@)K*n5wE*A6%*-Y&83N7Euciq|u32ki>ScNX0N1YQzPqOAUNFSn&1pFTjTGH@bibpi(bhdQ7V3#{$j^DD5$=n^`Tsnzv91AP-q1>LAqv z@_m4$v?8k54u55Tp9Nn4pepc1W=74G2*oGtEQp)N>4E7wQ~BjtTlrnFI*4-uZu#0! zyyXO%lXn02%B_X+%U`VgwvI(onCr(QZk81azwVkdGG8zwL^&Ff@~>j)*k15i^P{7K zsJEpz6hhf+%C+ss-YejkLK7&@>Iq~K_ep9gqvQcp>YOIDTa{QP1ni}Y9#SO)SAc_3 zNY=p2EB(-D(i+ojVIf%gX;8u%{o*UEwfZri ziW59mPSjfE865;j#S4kY)hO$X7p-&zKspSt$&&*;v&3lv08#hiCL!l5zy6i-yJTJb z-8h}b*_NI>ozHvkfG%>+naZ!{Ps*=#WD!0X8qT~qozUM3<0F#Ix>zqEG=4N1=7jE% zk1S9^WiPn_9OaiP@E4sLd|{m8_G9tztf*H(lAI>1V!v)B!hYlcL^INul<(J^E2zwI| z4)%8Rr>_>{1mFAmV>{dqzXd+V_Ap5-AVmo$7^;-@Xyr~$jt)k;)EN145b{;{jCeAv zjQ1FD+*N*<#HSG0g!QUL0WAnLLIBMJVuHwSB>e!`Oc6JSqx{k{mET>u6ynxG?gC}x z9w+LSxpqgoVcKKwFI0ZT;~L>}!~Bt;=jiTneR3Qv9_VU@5+d~TafMJ9B{zsO_TH;w zT;>WjjO6GDh)w*|)yjI(q|gP4$PK9>Nl7anOQE;@SP^SKX%t~2LTnVUfa#Ued;~h9 zqMQPZP<>F`4`%9>w86K6#0tUYUsH6ieJ}b?mEY&0HH*Mu=WBfY<5%zfxbyYZ$88A# zMM?Jci4ZBR8X)!e5U*uodqomq%DSSpYB31YFu=07G$bA=uD+6^g0w$Pk@$Yf4I5!t z=@39=fO-LdHmVTIOo@;Q09TS?3vNRBT@*Du@TY{k&uRGUgxp%XIkl;dEMoKj?=bQh z&z`0H`n+UC_^3(7GjH=}d2p;{XD-h?NhLM)3?=^&j`CaYr_f;s=S17mP17iRS;p5?syS^cREP2It)JHYej&$cl#Df2O~1Arc4TX+s~KiQGOp- zM4ZFD7yb8uV1MAQp(h@35v_(GX})c%%qPm?`Fbk06<3Y>T=d6gn@$^s;br` z2uIl~RvNS2iR{wmrYNamCaPo!gqXD7ki70%63CWLOOb{Dd=A?Z|0>~_UHMGq_o@5r zggfgjZevNqT5GsFa}akqh zAqncfr-_ui5%&$Hy~=X>dPd+fFw2pe#0=Wt)MgD){K4%~bg!FN-`PU>-3~WsXEYAN zrYD+B$v{0tjkfMH>X50}afPk6agD4g zy7nR4tqHa>m0v%u3i%i-!X**%G#2ys+JzKOD37OR*hDvO#ic|j9E%vT#X`iTVT*A5 z?Cz-OM!gafrC3WBl4T)>k(!o{w*KdSKWLfO_*`BlzPes2l%g@s`Mp5XJWDZf`D>2Ve6@Fw!nTsu{e z!=_Oi19BGTlnGW-RI8}das^e!tKfQ9Y|lH2?kx|tP0BBIgt&A0=9~3?aXWZ^QQolg z<1arg6RuT3c^_E-zI4eARE9uj5R#B_;R?_&2hue1#0-ziI3a8T&;(#o;dH4vfg$%A zW(ZKQCExvk6F&d^`yT)!ft$ine)%($UuL2F{?6W+{WMm^aok-K{}z`RjXt_0zI_kJ z-rka0l9sj)5a_!MkbCAnt2#`Ffs8#O+1&j zTq@K8g4XJX;c^QTCVlzN@7(h_zn>UzrF#wKpTvnAy%dk9*7AhGCy&P7E6)6%LPDy9 zcYgWx*I#}KsEw7CUy=F}iAmMTwWRE!o~0gZC<u$_Iil|4ID1a9zb@!y9Ipz)!vtP z?|gUXODI%0H`lNHHoW%4!;F%Edyoa(Lnj)VpH4V2$cme(0IWb$zqMM#-7ns-C4Jek z_toS0q$3}dc= zAaP6rLjY;M>R@~4-TXtge_gO0^HxRWw@%a90+H>*Lu}^`_H(y?)*T-H@QpBu=4UW+ zZ_t8YVqWH<;(<=5X0jdUqsw!=LF?&jR=6D&0|XTQb~RiQIOHKmyA#Mb0L&TYpw!~L z@H^iDx4zv&fiLgw;M?E6y2syR-@Ek{?tc5-w-AcBC@S-JD!(k>O3FAEZvthDIB|oa z#D-(9YHzIIah$Wa$dunQLR|GEobJ;%NSr&Z{FXxDc)Q}*d*ejC93s2BO!?janjJ~a;r7k1 z4~2v~0AoA1Pf-Ht9*CkWN}QxU)S@C}%CwB0g(0)Ht$o5<$lXr&z<^caRd)_xOoQmq z#pm7b+`B_ScJ2VUxA!f(x%)kKzWP$gx;yx84_|(@vxmF)p!!^czl)}GD8DZ~{Fqq7 zogPb5riMFq;))?xOoEzQqaHWEd%oP*d#xbERV~MD+tiwqYGz7{Ieiqw)ooUoW#)O4 z#*a*=)RD?GHLCQ`D>L>X{iGD&rKmwg_o{jC56UlpI*C%P~gS@xT)IlcaqH?YQ7pdBI!0mnY zt?-Bqz#U#lGmgdC&fa(5-U8z&;cy51SKohi_e&Oew}9pIC*1Xqz2{ZWKFm1vB<`W| z30JbUa8i=1$u>&teTm9%86mE^6R!EVX)L7hj5Rmf+}){Q%3<`&0Hxn;mb5kddnBB> zp)VviNcGnC<_@$HA_s8AitdM1qv&3F!o7(ijb|&rjT%` zQ7PI+3=`W?9N18tfh~-^FR%PoFHOiJbE%e%mQMXhyC>K=1!qUMeiB;0tciE2GcLEh zUEUl;E5PGFpy)ELOZlxkd+a^egdP=6u*=~#IN;_RxFGT-kWT9)I74O`rVtl|H=}oO zkaK7aA~o6SpXf41?amaZ;-!ScrTOk#woQ6x2f&>#g+F_@gxejqK)rSE7WVe`z64>r z4F4G7*!%jkm0y{l{66&h?|tFtTjZs*R90T!*6j{@Z^=(T$JNe+{30Qc3xhhPub(IDOD$agb{l?3V0^!Fs64KA#m#_ zR-x^E2N1Rx{fWc9@4q9s`xRS@-un(!f&6&vy`s$6`=+L63JLe%2}&AamLEwYa~r;u+jBh$Cn~?KAOrqP!!vciUDH#9Qq0MG{X z&ch|K%%h%+jJ2D9u6AGtRZda)Z78e!vW6K?JocE7ZvthE5plDj6meBrXop#gy)Z@F z@0m*}zg(SC6VetJr%hvggUvhS{k+R=FnEnWsvDMCCj6X#LNR%xgIo*J1~9Hz(f#mB z6z|qaTWH@a^+O*8I9Eu?!LuZV#xU8!xz$a$3_PIj z^2$RYqnD-=I!z05c}ujKR-%;>P62=u8=IAoSAHMKC){Ht zUfd}u?kL|*6>}xlSgm#Y72b}x2EU~8tKPb?<&?>K)XV-Mt>3$3=vb1}r@qX&xk1belg_1XaxFASt5(tV1*pKVe%KUGSO=+NoVIPu3}*w>ImmvrnxLz(z$o>N zp7Ynm2IJ!~x~QXzi!@^2Sf|t)Vc)jsDBakk*UDR`#@<&#cOYEoZESzNf1B*@AAF3T z4*~2Kl22-o*E=xP12ow6dJ~{R9?}23Nb{>X%cRWMrn0Mh-2TFEQs^y>#2d{xfF4s1 z*VDsSS8t>%P<~ZH!o8HQ--KnVxS?7}{4FM2X~fl9#+}fc+3hRmQGPjr;i4#SxRH{T*Q45UHAJ&~puX@pJrBvn7-cxan^xBv6_`#t!-M_PUf+09Ukp<# zyO@OZ8)W}raGYKj?^PUo&xbr5M7DPiC=PaoX*4<5gy7#!1?5*FD8J7?@)$#? z;RH&TsTGU3LZ%(v)?OneQ5Ip>b1A=LOB_c$OQx;Yyg^Bu&AG(#t9y$mZ`5^qHJ^JN zUF3$x9_%CYQo>8g-$V1?t>^|=^u)6)bv7v@9v)AuuhXz=wkE{Cs3fQ>q)a4uSC=J^~fWSFs)|4UtT z;~RP%T{NsyYROI~6G7W*eAbX2XLmGdV5`yTmHx2K7tkk4t1)Xa8UwT}zIa4@p^Gmj z=-@1+`gAZ$KT7)OF=o2DmM_~FzJB%_vk`V9{EaxhK0f&rMdW8Izp8dwN(m8RIeK$@ zfA?_z=GSikI*Lo6P@#9sI>)K)eP$jE-oVT4E#Bn;jmiplr`u@u48Y^$AeH@_7o&ec z`BgMM^EeB*Ld5-HXu}_BxRQuFk!`}IsKnJ^*Wcn>Wy){avG+xRx64lm?=%xfZ?B$= zY`GkQt}$1~ss6zZ=Z)mL6XNt2ef03DvD47*TPt*sAQXVXmJUD2I- zN>`4UE}73xZ#YPGltuzsQ<|QTeSeulzpAYB(|D$~75blsF^DrOka}DOZ!T?~l)Iv9b4(N@MR^Z}f)jO0R!Q z)sZ12+R;S*0eDhE9fuOg6)3tQ@#7E~9A2f=5~g&#pG95PzCt&$RQO~#0@Oyw2R~X# z^q)(<4Hy5Gd2}X!(;qhZoYQI3sF`g@4C-lq^=VJ878ayy2KrC(Mn&b9jC78*;tX^; zoF8?U3UfBg90{qrrs{(P=yYNsHRqHyaR>zggVo?cV~3{NhQ?eIoY(`ijPk3LzV>t> z;U4~D!kvB=CsLAgVlSndYw>$RZGN@v*!xy-?7c>B?;zS}1NrBWwtLPTdk>H+Qgn5C zvHS|mrXz)nNjOI7>L~lBgGH1s?4k+7Y9g_jFU9h~WzlD81tR)Y8qfL#|93P_ZHZvv zu^Y32?ZbxtH7niNNc10to;4A+W(!%kxkmp{-v2`n%(Ko$)!~xVb*F|P(6Z3!Z?M-B zzf|MyHA&4Jv_=zp`m|>EawB9Rfd6kKTS*VhipsB$aH95GwvE#L!bOyQ#f&Sqg{!qz zzs2&j_*S1$G_kSw)!pTnkcQMiUyuAO<@bZP1x5GrqpD)@V%;{Vqmeaz*k`bseat*d zQig7JR=-BqW`zJuCq6QmSn$Onv=rUhi5c_7V&intM;Y-5%TGc19oMg#0;5dcJ!Zu| z>(|+a4Pl=Uo25EQfL@Yo8+(6Q-r!Kcs0KPl%qh;*<1aMt?Xvn?Z8G>9g7T;ry?XbM zGz6^-MEai!wu_9t=U#i^@xqh1v-TQFk8k~90{$s#Z8UT~z=f{@&(+tJRepI=Ee!R^ z8^=N?BCgoOlDvI+FdZ~mvf&kKO~lgXk7z(hIO{0QuF~Pf!iIsbAf1eF(s7y2A>$jw@Z3{Y^GpX7s`3-&1|*P|a$15yS4<211%u9EWm;^U7LtGH7| z+yiAM;ERYSQu8ls14zkj#HXmq}F88p$n$R>E`qx-|Qab4s{R+O5xXrx&SOMdjNhZg(ib_ zUNYYv#sB<4`Bk2+{3?%>-$%rVE8R~$t^JlEA9006-M_wjlDO6u+q4UJ&#L%;K@Uu> ztS<64Jr6=k#SHPIyYa~ICQY)O37duD%5bzR%E)fel|fcn8C-_-3l@XN85*YJwiyeh zF7{&0m!#h8>a5kYx)!Gn_Opa}m`)E*7p0FFt#-aRrTkWc<|qI{5#Rn{8;AS3Z;-pG zD@@@Muq%l}-eCjaavRLeP!b&B8N>gaR(>1HDZjGk9%npOT&SLQ;)IXV1FB^k#?_MX zn|~iC#lM%z3UMkn_P%<#Si<%D0)fp{ADz}4))wsvdopbeP^wFM12m|k)Ib@DMH(5W zKH47{K3~Fx3;mB`rqM7JSaaHDeWNBSBwQXlP1?ea^h%s2R`Ld$Nc9IC{q%#<9++DE z5l$KPNKQ_-Kcx6Nmpj4@jeIy)D~w5(kMU-N{wBC0{@X+(uE=Roug34Oi;@mSo(ih%3r)H8zMwY<`!oN(;)b_RRk&erNAof7$&gWeckuf$&N22h5NyE zdYP3(kbE(_E^nzNUz)qserjFxCS4KoguM2+l@bw(o8rEOoWV;Au@psU2a&}9Rl_H? z8Wxk34G@KW%RN~MX&944>bk z--hZ8(WpJDr*woUtkXs2>y@5NDnQ;Ckuy#d$@CF97H5cJ(ZbwQ9|@Q!WC;_+E%e`j zh`q0E`30m|>7`I;zLk3O`Mnp?7QMK?^#a##LZHL{*)PiN*!!j`vG*!|1M@??iYwWE zcW*Wb_R8LpPhb`}(cS8{WEbD_8n>tA_xMt}f%*NP6KC0(h?73M6b|KbA>vgUkv>Ej zYoc$)PMk!Nh~~4DbXtO;h;XviO(^K4NUteH!E}Bq!7=zKkk;oYoeG4gcZqzlM5Ops zE`I#S2>am7clW-5*?Ac|*|^Q;rdwzDX6n`kp!>hB9R#+w`GhE zd5uf1=T5%&cl*BY_N$tf-{VWk^82%26GtEP8j?cS9P{%Iis}pJg_dmIfXH>u&xp( zqNkMvrRIsE7NVeMAswc8kaUp%{TH*h!M9B1)@zQ;2kd2+dn_y48i)QZ%+g)R(a(h@#{(gtAM8Q%IRb3dN&7 z`XAsYy1gvFEgJkEnL{?jRk!>qTfZU{PDtEaRd%=DElXV0{Zu&?`kntB!O+{{R{V8@ zAx@*-kLCC8fBw^7C01beB4d60qf^ZWMh%zPYX;@8Jd$>&T_m0$qScrw5?WcIeY3Ki z=oidV$PglmSQ7J5Q&Jy~<`OZnfI#MCAVBe->S)m=cDKC5N&HW!+e?<;u7c(F|GY*n zODNFRq-tvGnQd>ZVfp>~`w9}b$qUNoPBVO*s; zZEAKXn@=@YvHW&&%P&W0T%|Kg@;sG))LPyuUvxUDkKppi-oBj`oKQFsd#|k>d;dEj zaDV&dFIk?meIEU#US;1k&-fLO`s(y+61Lh3*Ckti>tH5l4e+we9&pSDP5FKJgmA_QrSHe?#H_9eVeYDB@DEo_iZ>!`~@NxcBi{!)}3-Wp1~?$-ER2FJ7J4XL0ha6gf|YMGs0G`aI)q1=RdDY;VdbG&!Sse&R7>RL^)?b)Ni7; zqho;AorCcbJc82K?@#=`PFi*6aE$V4*`PF0Zr>DmT@S0G_ZsMUy95!0zOC~g6 zaNMTxTddy%jK$JTny}#2x%>1903~qe*)8ScM|b~u>&er*>)3n-C?7q0#Bl4$qdORy z0ER8LIjPau)MPX$+U*V1ZeR+_?V_m@j| zQSMIFh`nb;{`Sjn|B^SVe`N(!tVEYn@rH!0T;a+rzx!n=NsG2rf2cQR4J~KlVG_h+ zJ~|q7LUMducmm#H$rfDIg|iF&!MGul(e@mt%A3a4M(GZcDH%kSk@ z;}{MThHaYkC}n>)TXBf=T<(x2r~D|;LE?yi|0}2C*SR}kExBmI$ z4xT>Uyz^}H$)oj0cQzkCd3^WLV?2HWu+6vc+`6^-^sW+2-wJdVEx(QaYlCf4Rm-o4 zz5l=mlwj6t3)AIYb-`=?Ki@&mif|4Zuj;}Z^^%O8$mcNSC>!tRQ8_bu@>y1hT)M)}ZXT~S zqTAc@TRZlCs2%-%P`EA|3XERU zPr%#)w7SMx*|gJXVi=xAk&(ex?31n`08Ar+b@&|$rEx)I#TYf+P;SX$&Q+XLzcJ6m~_SQQqI!5IFuC%Ovl9%u0D=B5a zq_F&cPE*D5d*l^cRf&_jwk^LW3vXneD78!!Hj66CoDwGiC|=lfohOr*5@EA7krk(( z>SDDoV5P6p5hM>KBH|C_h=Pf2t&i^&d%vgUw_~<_vIX_> zVB-vSKpPt<&}ag>y9cl0vc)urUYn@{dDu0E`ca^Wvb7TpJ>%vsh24Ll373oj)K=tS zX7!{KdNrW#b2=@oK>;YPVL!0Oj_K`yEU5jx)={Oy*{8uz{S2cQ8=ZjE0^sgCuz3fN z9^ZY6J8T^c+uV9~N5adr)sDT_h}iqDe}ETpMUg8i+*=g`qUEgyS>q}^vMPQ$iM#jj zEITez`b|oFrB>|yF%q_2hPacZaG{wgu+C+ zpdqUdQZ00}n|W){L8kdsBE3k40GWNnt;nJ!K`kE z#(2dEx5*9F)z-=3c6Wmv7#ae{fC4ttYzLZzTDy$n=TQe8eN$sC>LD(e8n^RY!#TTJ z8W_e|E9?N2bJkJ7-v{97Ib755B8qj~ ziLK(^xhtVTd@35^s>j|RCuOT<`8`9&jIwR~NPLC_OSx2l1c8J#O;Np$$h8`Xn_US} zz|By&Tn=?qi>RyA?&ebA?SFV7;Ypgp;r?8XTtQmMtjy?RS>4L=jIW9#MNpsRw+#n@ zJ-|-r0pk*vm$@yh-P8eFYtM^+L~T}M%);&bL+5P+=#sFF8hdc(DTxAM~l&aO6f=cGYYag`5Y%9r9!~8G3(84DBRslG1#ZZeraR?8ioOAPJ^AZn1>qdV*^s> zPu*6qxl4WB1`X5_`g(dKgO$G?lky$+tH$2fu>3Z)o%$NrH~wCz;qrYd|9E)=zswW2 zOWem@~Y(uOdY|)4>M1o{&IY^|dOM3#8a%F*C(8;QAUkbRXDwjXA1^_R>`f@^sA-e z*=}Vw_M+Qg91}C^*ob0UZvcj`jgL!L+ZqipSezQAT?tH0X${66^R-J#bX+?t`|LFC z##f5HR{&7dw)}4Qsq$vSTSDM=XEw;}j@uIK5WJHVELirVmbIxn_y%w3tKGnCJS2rv z+KmlXXB%L24B1Cq=pS^Aj~|}p_h^Y*y^g><<$ju3()GJm$Q!f@Q>{7VT*#6);O#qr z-h{GcMPVjvNqH$~)hBg}X5z#GWbg%I0cUPLOnMvT@rCKo`O$$D?&j@>8^D7t{_5es zfZNwmfcO^-xW94p=KZbb*SY*X0Kl+s23+9GPC)SF>E_*McQ&~NLn%E1?%v{`)nBxvAX~r7U zhx$eMjY&i`_=sbsNP_f%#DXn7ld-N)eI!N4nH6~x%e0tqSt*ky^4av6{J=GmC=Oi{ zGu}){&*us>pQZ{N{*_+=!!DXs5@gt!4V~^GHeNjV*F%v$b03^F9oBAw6}!}YHTU3N z7+|O$Njm~GT1++w9zT6_=PsT+{^vR%6z&eYE5UhM1nEy`0R{F;>-k9llA#k|-3;(M zTYKDKeXYIPStll^z_F>|X=eQjLpO6s!%nvbW%ijvR=8#W-11vJ_WsKs{zP8lc6dg( z$n6@ZzPomMs=U*1qH`Kbc9&fZYc0!fTm1?*XR&sGjWhs8{kYoXH1xQQuAafurf#c> z|HWwhroP?W>Bcx_&$qvtz{!^1AJS_^gaEF1!wV)a(R`pV^7N+C)=(^GTTIUf%;a6{ zUm^Vrkv>Ws8+n%~7^+s}=TLFWGLduzHiY}}^%c(7JfxZ4kKb8TyjP=Fm|J^|K(N!tLX ztQWvM+&Blp+i&0H_OeYpdXisefBO!eFepqT*bfq9)ID}UdHSptS0*e5%$+srdrUxY zTfgXYJFMW$4WQj%7($O0op`-;a)sL~_Wr9M`*5*I{eZl@CG@R)d8*#7 z^}xvXxTJK%T;_|7)h)k@Y6_?P^KXCgXEh33v004Jf^`p>v{HYkWy-^tscH0@P1P}F zpBig!RLo6}8m>*|{gaK5Z~&N0P0R1$6F29GGBc#J%uv$qu<2~+M2;>biLS;BDOb|# z;3t2xDOz^fC^fTVTp72dgL~>(#`?dGtd{0J)@4xlf9uK>sIM6Yfj4#^PqzHFR=50WKL0T>Oyz8Q;&w>f?lVdi ztGLpJle~0)g3@T-x$Dcy1BcJ^mi}|Cm22Uu3ipd|bAD0$!mU1KotU#|FLcalU>LH& zt%0Mh$vHQsv0ZjuYy_Hzn^jE=^~0(ny(vT7!s<#WqmSeteudI0oz7#`XA76)0^Z=F zop-6r&NR_}ExHjQe)b&YNm`$z{TKJvctq-L9e^_jSV_HiM#7nmhvG5b);$Q?OiH)HI+kx2 zE#2t9eAxqmk}q}MLcL7e$L%V<;0}zPH(Y~l!fyleqt%I7OrD3W{o0l8F$|gwXW_Xr zhBuG4w&k}mZ~2uKu296iC2L&ya%$z}Em`8^;_YOur^+T@Ib))N#C^3s%P$0f=K6Ny z&o};rJkE3D{-&|34G#DxVd$`H4J!2L^*y8Q7=c1@sKO?5_zE`@+@V7@U#Wz%B*~Xc z@#=m$+`|#oY{L{N4e!m-+ ztxbjwV9JQb;pt8soPmoNzC3WPR|E5Dsju^b-sG0x=myX+jt(a-_efJ0rvXJ5U{GN{ z^cZoND2kTfrdqN0LgC~hPF6UfaplK;-?dZV#9NV4cY2a@wn{&)JZ6vu?m(8`VhMNS z#$W#W?O!0zu+zxw9foc@PMgNBU~G66fZ|np(Rvi_yJrrqAI%DBi&7+J$WYRnB0(X3 zCY}*P(+GL{V|-pd;MiqTQXaC^x9*dXlS%+2tb+yA<~!PgtMZqmICaJJ51?F<<5F#8lR z{c2a@1_u2d5ExBUqTRK0V4RCwbDvQ%0#oi;fLCtHZZODb81{qO)(&8H6o-Sl6nkIM z@+;gA&7GesDqNYyRf@nb4O8D!Ej(53Q%_VLO+k-KZ!eko4rck474DaR`^(>Mloamr zAaHTc(sml^)~?qpY=T3y{Q88SEE^K9=6$QyEX6(P-muG?;S!cvB~EdA>9Y`p7P3S- zAMFd>YUyavGErdJOG%44L5Y?AKr9k<=w|E`_M{?#u!o3Du|z7Eq@a#MAv#|C{*Qac z-ZvS`H%?K5GXQj3x52qBUY~hTY)fz7yNR3kCE(`swHG2epY!}C9z`kQX^G{8o>uwPosl+1W;2vC{;j8EY(kS)L3 z>XzSzQ(yZ;UgKneE99rXwd+otyq{W8;ADfvJ5E&O){b@5G8bQY5husq*JJtp)3<-| z7k)9W*rlpeI1ucWXkf+I`=8Ol2qnprTnVl8S+ZntEroqPJ4IvUT;UJ=yMV7UtiVoD-|XUe>(zU6O`Y;+`r{_B|`;f zA|VRu1LVjC$Qw)Q$QCD#UA$*gDSak0jTa|?muR_SUykTCPn=&~(EK`W8Sydm2P zg$zMcYRM5xiSE@)p%V~G_`Pz4Q{fe0xSwC+S|hNH7x%cOME)yr^IyQ)y%%h4>)wWF zS_ub2+c)HAQ+gYuzJY6V8ao@FYIeHqO%SxSpvWvkfsXUoo3jktw3w!Qq;q;FnA>lg z!v)3AoL29$bpsYP&~9Ug7<*sS^2;;wKKU_G!+j#(rS9Bpc<;;csq!$jQWYmp8BWyR zy_^nKdiEyQZ~S$+<+rnL%dhfJzxd_v)JLLh>_C>^zzT7KM2nVHGFdHYvPVUgWlmE% zPLy6Gj?GIM3P!TaQLcXVl1WFSwCr;FlJ=zlZ)xKcSg<+^Tk=soB`;)RGpTf7CUg>n zO;)&Fmf!l(>0WH$o{qMpEp7zPJ4%4-TVjEW5d=54Za-(Bhu3j1ls(<(nQ++H#8tl$ zQnQH}4N&xJd(q)mcX!)HQH>VtNzasM(G@Tb=-uq>h4EQv(Fd&_%mXvDCMcviKVWGA zy011LuH~1V(p0zneo54D^01+(aYEtV+F8WOUFv%(PE*TYp1?v*9+1d7UB;)Y$KGp{ zY!|2#_L^u>$g%gkkFXKVyOLBSG847*W&*1*I|WUlSkfFO4=uVDDH4ct?J*?0!T64& zM8O4KZ`CAD+xSQqh!SVjycC_WCu2)N6LFT7d1092FJrL)g_9JDdg%o6-CdU7`q{Y8 z-?*&Zr>&b}zd|O0Hc%ZSW zhpmGQb@N0CKzR#*p#?ZU?&=saE5;{raR{7#ps!8sfd??CFS=|^FkS+nwMuW$^81yK zeQ;ajwk>h_4g5-;xSCg#1SO$3F?NyCmKLXc^zbrYmo2~hxq*38lbc(3`26`pJ{i6DLU*6Q z7g7Os69B9CoTa-RkeV1ePMc>jaqavF1fx#0y1zAti3tp!Q>y{J8XYcc7g_-&>PHEz zKYILFdFSrtCSU-JdNlQ`To|y5ZGHxrv>3JMjrbNr1M*Bt0NRvq&ho3!eEx$%;yx^| zrxqL|{KN?FQ_G4tp>ee?n#=q4C+N6{+?JL{r0$lu&+jMpUgpSzt+pX9IUlD`%#tD! z#ge)B@JpXeEi7e4Rc9iOhscyL`{skRLUp4Lf4yE;qXfx>QU2D)${l#rf1Vc5KTS9#|VUz51| z>=|EpHMc;eXN`tVkI^&+g~QYVRIOzX-}2j5)$;pAu5bLkk}2-J99iGw#fECDTXG)s ziF1tCEa9Y& z42fl;l#jv(4zjl{II_VQC4%G%QPfJ#q&dA3UFpqUogofSR)`S!PSQiReei49@~hZo z`DL?QVl>lias;)Ye{J29aQ!+%-Rq)bt~moVJ(pBk{RPWF*U-gbOq|7)!QrzowQ{#u zpB5Z8p?66N&t)SH$ojK&JbQNcZET9vg!LznL}U!`=;@Pnz-mOxoW(Mr=o_A{CC{(L z^4na?^83LL%5AE=yCo~!PRh=izpGMYgFI+BQ3@yLYZZU=UA*hFd49ijOj$XyK40Mdm4#vRO#5 zkR^|IAs*AwLO|S`Or;j}3#o7-e2g5rhm5fN*1CZyb)1H3aNz6!YQgRk_QTL1IOiu( zK*^_;1|S8+n% z1XZpml{cyHl9%syIpc&{oQ%2Zj&lJO74BW78<-V~VO8ZTzlSAkEovhh>3L;C+^@bv zoPK>S6q~1Tz@X#RTF{dwT6G2}7^Qu(LV=~s5>YxzUfy>`41I~2R3@GAdLoo(F++~9 z*W)BgT6L5rZ-9Kq*U5HBe5bbM*EovK;ii!x)MSHUKV0tB0&R^W=sagIYqaLemJz!G z=LZ}Oa5OT95`apNPM6cr**FGO+e;gP682)aPX#DD+adK_vbRd6L01O^A|(cQ9s~HN z#BSYs^!DAyU_6i>(z+5ryB~R)8QvJmq9LwCSnp~1m1FM@9VJtBGQXwcvcYuT-Fm6@;0h=0wEUVy zqiBd3eDkg)UE$=>-h`9$R3yDZ$yme}N?VfoL1-woK;%he7pUJWzP&`r{+N?^rAVYp zrRaZzGvBRg`Bj|*^Q)%6g#)sE=Sb1Y0ILQV*E`VItDL>!_gKwmE$y%kI7d6}66Rc2 zw7@LDI@Q=W69XvHDFMS{z_`^6^#$YU@s7Up`UyZOuFPTm)}2iubDNvbuzp9$aKs49 zueP@3SAFU;A1yE9UA^5QRuAOvQ)-_QfR4tfq;? z@_W)k6k81}k}E3auJeVggn2{gzh{hafE$=9l%bvFA3g1;8>QQA?tyYdjm~NJC8+dQ z91XZKDU`@AB3+E`Zk^X}#? zsg-TN6^b|^5ao5zF+UWyZElgTUsiZ0ZMS9BnJ@c@zDD+)FXk%`FbSq+l#lMwfSghTesVkph~ zRUef&)(ckkQorR_RlyLa!~y9=v!TmspTdxV8)nCyPR}n04`|T`d~6oL3QD@b>_NmF@ z{8fE=OgcMk{-*z;vt6NZsm3)@|L7HsNir!c9hP%_BUhwB#lSc_Fv^FVbCBj! zhUK6=Wsi_ENg0t@cSR*qj<;Kqt28?!dlbE$R{Eai~r^@=i8|8I7(<^O#DDE_;U-ur^~*fmT4m z@uia&akVYK%GR%Ng)2OYlUExGkqs5^ndhmL*C5wx{G(XSoq$N1@}I7#bi4FT@O91p zEx$6--~Xa!1w{6D0~1K@$+LaZtn~_GgHvrG=d66jDzf6={CS5K}a#GciA?I){W}|e} z{Gev+{lW3LW+kA}4RwFx_;iEb>gp7`@DlciR-+QyXJ;tTszIKfYZuQcA-p z&PfU;yhx<|wGxZqMh7o2_HJPql z?s5%JHgz-BVLvrIt`Y-Oc4#yo=musa6U*fm$u;8$S>qJ2FA&*%!fq0vS-{uv%~ECkU^RlVpIMBC;!T(c3$fiO{>HX89DZz_Dzt; z?9Ig%`x6V)mq-_KW^y8do^(gwloENJAsq!{B{4n*Biol_@3j?ML)6XXj#8Ap9s8r* zk0N52I^qImtycm2xi)*JeadMrd~5Rbhy=m(7z9oE4E&RaVmtP}t7`21r#~(iaa`g` zzTdY5`COXacjBt%L(Ah-!Ev85?4)cjqn5&HEB>FN+Nkq)Lg3WD{Ds5>gAUio^re24 zo%M{5*t^^|R~J*Colr_jCb8E>mu&gnC()n!XwkNqj5q_uKm)q&P(3@f(y*cA_!XazyI-WVs8xHA zDyU`oZDN)A?TtVE;%~qG_Ai-0RBlW^$WL;0b-KniTze$;`jKPrzeD?Frl==5AuqHC z!WlV9q^BVwGW)eYo)n+ za0C-37u0>{&Kfjk?lpqK4$jzpDByzoLfwv-D>Xx0T0ujxp6(bBj`9Z-O@wd#|HF z+($l(-lcOa$C7jyq`jpRC?OKeJI&<1FmGFlt%N;+1Q~qfmJb+!z>>7`Mq!E&%V+5F-#y2%+WnfDDcfo2~ ze%tbv-%oygN1rMm8IiYB-_P6B>YliY_eI`$5}YD3B{yGHh0Cj4HOsHKfm!`I+%Nc} zxIb^(-j0AW%kL}UBN?x5H4s{gWqi31-BHLArDXFZI!C zHnXbJ&v@d!0b15)#L>(m!TF=N7Qb`@Q^l|otsg-1IUEX?^^<@?H3ZDgS;m<4oO`Mm zw1rEvfT$3bxRV}|F_WmP6;y(D%e48puTe_}_69DDzrU(vw{u7*N-nbaB;e<>);k`?mZ_QxeeN$3Z!JWxGIB149$QpiNkQzo{}b_Bt&t2nL)Y7Yj}! z+%C%q(A|9GTUD>0y)D1WQ~zP_Tw)q4qd2ZcjgKI@ccsytF)^^5g{wF+BD#KMJ#p1@wx)JsBt3h{?j zW;`I_wKcKqN(6Bs6ZX$JhoV8=J7q?5Dv8ku_Mvx`7puA;CVe4)vR}EY5)#XA`+kLE zi2IBkgu`>KpXA6JUI(^oY^9UEy5zBf(NhQx>@M8UW!~+9b|6?sIN}9qVPJF|ZWlyD zTx)9$%WrdYbJ;PnZ#z|hN?gT2waO7CS_+XbC&(~T?I;}GW+?HjK_|5Ja+Y7k!ge1$ z6x#0F@Ghn>8_TXHvT<*EDjZxzL>mg2<8*`npoD{celpLP@}UEfA9ha>p~PaGeKLcI zINH$3tuEej0RR=t$F(iLY6j}VX=Fw;=Q%g%hJ*{`#*uTL^I&p=0A~fx0A85(xX%Hh z*8(L>TDV>4EL(oNWi7w2G&es{sNx8;PbIlq)vqYg;4S&+RMGZ(0*=I0WLobRII3{9 zEx*d8914wfv%0?Kp0eZG8rlcY-0!mc=hCFZtY;I!99&N#snXUH7%i%#dCD8{bLMa; ziUN@Z>F#~CeN?J&wPNo(8RbGPzjfnE z+cSQ9R2W(Y7V1&9qHv`#NY4L>A4qpsUd7`crXB2xHs=rjcUKE-JvvTd(D} z6B@((@QN0I9niqA>8u^xo@+*c!Asf`vQ>5LP|I(-tmXHaX0eESSX@uNUvdkxu%5dA z)^CNxRYCNMoRq`{d9MgnxSFx|%B2&Ed|ZF*eIOR*!x%Z>Z_Z3%i4eI>k_w}MIhA#0 zU1{|CEtpQ>`a%c;!)e6z4B|#YQB#9@xL(upTR+QJf#B%|?)-`gn07UsvOo5|w&nNb zhnkz|wBd14;s69e`@RaZh6g1NaZ;5#ND(LMoWd`fOIlOmXEORGaCg!2Ym%}2Dwne5 z_d3>1Jdrfx0g0reh~ey-KNO4E(3?rEA|V-zrq zMZ!p6F*@aSCa|xxDsNtl9rVyif=B%CZM`+AY54`s1i<+0r~&Gytp!f0$MUOheXW^D z9C?a+K%jg@i9W>LQ=&3hu(m0V-s&j)P;xX4`A(}w2;L=mg(H2r(9+k4y>A?EGdgQG zLupX#{TCl%g9w`o7C9`F$ON6Z5aax7jx2|vs2MHXYAW0pj|TfAJ_{1!EK8e{ zW&`!a(PTlzC#cdDjC~axQNRLnC^g6DF0IIy{7f0haM^vanuw?PK*eMh7qAll?%YH zUc!l+>_zQRy1a=3w{M(!m$XY2cwR2{zPmm{oc_wSG2=DTm3A90opq>;4wrFw(rx8B zE;*MQ*z}5)-%s)AaFI|k&}PFPL!Tv?HKRb>=I7$W=AdIC#&d{kecA9rB0&1q15Grg(+?@jY{U@TP(jCkiv}V0J_e>DU^RJ_FiLnwy1FBB2FBq(ngg~ zA#l~&)VubZ;>1c$;X(L!(*I=zPLQ~w<+tOAmS0^DE!n&sod%E2^}ELHj`yyP zI=jbaT!R`N*V{OSqoL48AWVMna|wGiR!GGUhX*j?5Gy-p+`_L}EIblnJ3JZQhkYBd zQ1P1O#u^p~$FfN9t;dJ`xb8(I^YI1hLak!?y`lp3sIUeOr%)OidoOSKRlV|%AaP=! zD(IVlSi}hkSK{syOurSTxVx)|r&8#X8(oRIL%CyzJEi|oq>pd}A@=_8mS4@UUw{4M zA54+4E6kXO(N5~mj@c#|o%7m^+H=sb((CSZ3XCvkf%F9bMu0 zRMg?O;bM9*6&~;*=SRc=^z~)aK@!CngqkCM27czW8tCXLJSv!l6!FF z>*P}#!l&RODjA98x1}6=Pnqn1H)f~Lo+=!rfwA{GfVS$EUoElxJ}oF*sZV`Eyb~w6 z_AAYY-e1GXXdE?(h;MO9&K4AndorU`QooVuaV^WQiIF{^B=F^*Oo597oi3;D;>!6; z7kj)6GqBvo44mb(J!4}A7h~^T@pKu~nodT|G?aF6so@OPOST`mVAPz>q%$}iLOT(* zs1E~)Ob8E4h$p4rdMbdnsMnuJC)O~zMy?7+Y?vnEmtMmv1VX4}zWb`C$m(yq8JNW_{3^(h+7S#f3c2U)W3=)`ofrA4fdJ6=Ea4yJI3Qw48yK~ZvfHe zBU`X|^7!fJ3d^se^A7SLpfjCZ$z^BG>!g-nj}^M*EWaf7{>g_P6`$fn*RRm0mL*O= zbj4AD+js1@O8z~4<2o_MxGD-)xc6DmIKlF(lC%8E-@yFl%^&~#@=KN>OwQZiwzmx! zhO|THT?{k5w5)2Ajb>=P<@XD$mpO0LZJvt7Bj_{Z!3d;iFY5E-0wz9)Cb0k)!l=!a zF^^z&F&;~9VA$b~$NlI- zmfz8FP*=D7nhKWR7at`GS6bdG3{xL3EvM2;n5FF0$`d1mYF%n&RD)O?Dd$w2%q|(V zi&QFHLE;2We^qGdIdsvE6($me!daB`UB>;@>~X=R4mB$nUldC<#Qeo5^8Yotx3t6K$)lfJ|iubGoZ zl}g;*@(L&FM1^!Huz-_v%~j2Cm2(C%U3up4mfzomFu||ieDepEA!vF_8w=`d%)bS- z{9do$@@rl{nDS7Gin~Y9H);Zm6lV*ajH0JxbMM2n|fcxVnfY& zYUR_G(yskdeWPV&CDiImAQJaK-V$ zO9p({ha0KIq|fR1@*_c9?~fs-_x@&Tpkq6vSVpjqHxn*r?H}Eb#_D7XV#+SchIQP z*RcGutuHl|B zVfkf$|ADwg{yV7U7eB(|ATX3k#?#38*86yW){gE77O+RqOKy)3L^FcW`A=;7c|bm@ zW%*T~yVeOt6S-!7oV487(iT5#8~3!(l?S-Fxt%j@@9y5v-e~D*)9*pYF0lC>4Feje z2fYT4i+8wlAi+8cl&jW`0W}!MO9ID{tO{^+9mMiWV(*)pin9iB*dS<}Sj5p;L!nQ- z?-q%puDB|uUr|d$6qvguJzpnx`;toBvj)AK<+oMASAHdixJD95r_33fZ7w?;pG)>* zG%{sJjBwZ=6m7hi4Ta{CtID={G>T8mHEv*Px@S}XrsYX!3_YUVZcF@N+R61WJAdtH zfv~%m-|m9;ZGgQu*jBi)CH!i~X1)zfa3ENyfYCvb^BzwlGZ8i2Kp9_R+d+cM3KX|+ z2~-2`41ollL$v(Xj=fi_o^N`z=~2-WCny~C#8vjG)h>(>WV=)X%@n*-IlK4Nzar6I znY&4%ya@=F-xGP|*K`~T=NnoGghtX_f`mp#?8xy!8{%9)rg@a&JCUQ&LviNw3)esW z7HV65Cjj(4W9@K6^gzgOfq)vcd%1iM5N;S$H(A)zmxOOy-C_cEZ&Rh)%L|6GiNSb|LWVzm*4;B@<-qP#6THc6+_(M zThR8F=8_8Ppp&T0E)>X-&dj*;1~9o?POsg}>wv)Qv9R3&xeh3j&uy9j)F!A04Gg_C zs(~4ga}Y*W3~blHm68%&x;QonN5LQ!NN1Z4DE;TvoE2CVlo%j#H(#M?5=U+*Nvb%yoLZQt{^LFK{fG};iVD9k2Q$!}`)V1fBIP1Zu>8u!-Yfdb zZ;NR8y?_mbOcWNlSR6yiDKy_c%4R-u;is>DzWmAimp}RR+wZQQF(VZ>+1PuqP69K% zT$emBIU)^tD6q3dO0=zQ(C_VZF|d_~BAYwbJ%F9p;b?TI$GEG*?eAWj9breTqZSy^ zl5c*dZyXu{Gjbj%?j`#biG0n2#|V?(_egY|dCzdrE5F^XZSt1i7e$4mj*&`wid{ESh5h821o_D%M;mR9Zk{V8ky;sS{-Yb_D77Bd$1vZc@BHl*I zCJ5VwuRi?r^825D^3lg1UcYb#S*WC@A&%{adNFO1qO4T>>=qXi_5k#6!Yu z-}1UTN-Cxtq~FhJTt;wq4!vbxn(R}9v-g9yXC|QE0MO=|cN(BBj82;YDDRAN6&*L@ z1vn>)=cs)afD!&`3eye$1Y7$CrisMf6NPKq=ZPZ%NB8f`3HU_c?kOppd=V!Vw-Sm; z7Nn;{$TwHDh9mk$5#+7NtFp28y)^c|vAm^b7`9=t_g~>Df3tUHF|Acm7Cr#O#^)@Up->O&(q zpwR>qVoZE6aqqULyOs7xORER<3*mC_Q}2EF?_O*DYyIsC8*Q?^{=`#{KKlIgFF*Cf z>s;9M5GXRb#nu*pq77yG3!sPv5u3%*I&kf0GCD-eU^ebV8TBS_F1t93gnd{JrN{j) z_f3e*EKXw12|yOGXG9rwq;0E%sG-iNLn>l|}(@vA^DcWSapElWE|L~R@M1sA+WAD2}K>@^VR8W2^Z41D<2uXXIw>KPh z!-HgE0cs>ZN#Iz@q0A^;iGd|DoDA6qT{EdrBne!jN@y2la4;l+GcYub8uRhR$Ad6E z8?r(^_#2ow!PoT3JqQs8bJXGEC+08r3+P_YFtO zxw^GbxbntUmCJN2zY^IOmS9>~Arw)P#c9E|7VD41{8Nc!K61@|6;3kOV(nwkDz@l-BmaO2wSKURIpb>hoGWDg#9)*@4!mpq4{LQ5qIHn5x_*mgEAv<2i&9K zHPvZWVaA@20Bahsq`{G_NMyiKSI5jy_@5+1h>3C3F<{#Fm0t@0UCZyKOyF26jy2-6 zd*-Jx&gJ{}$MX|PM>d(I>WiF#*6wR}lK41PN8tFNfpN9UEWcfhy*Jl%F+4fv3rh=w z1Nq_jSSmF8bUqTxOf0A7M0?29rPzDwyZ;~ECL0sBr{3jFwoc%&_q!ps4%={>8MKRo zBtxSMaC=jBEImHF2>(DbfG{FLZZr@`BMcxjh${1dfVTMVi*JF13ST6Idc+Vx4cTl7 z+c5ba>JVcRx(2uWs{N*xvG-MltKNw_rFvq7XQM-Nf|QmS6cY zR;AV!8&1*o8p;Y+>%>us+Z`;ZEpBoCtZxRZP-`@d7>3%Lay?B#B<%4==aU(8TD-<* z*rkSp3$y6H`xL81?|->6Js{{NW`W{HJ?Y5|U`~yTXcvXxukc6!EQ0{dK}h+@xC^U; z63PlUK96BL`~ooIR9uKJCYD9#;J6JU_Kqkuh}$O{pNil8zVSKii=c!-(PV37Y%vQw zoj@HEdGrh(d#`KxElV69r&5k<=)@h%rm1CidE)d#x43p3Vx&ofCT_bqY*&?9)i-YW zbvON=!b4i{&D(VM{(Xg%86Q*~BjT)oJUf;NMW&)l()iGP!YBK)Q;ONIhO#S`Ws)K; zVs`Yx^7~p}$2@pP-$934boun4o9NxZw7Ld>342yTH*l+{XDR7MCc9$8?C?y~HtGxp zkjUBqjM$J~#>_NQ!%M0V8Js{uFpasB2slno)tphui=p3Gl4UsOg(-iaG@Q zuz;Hsgyj*hJ1pT^{{%@6tpw#YBr||2SH&>0I1L@lfMoI2jbPpRIY}P6DdAHCnYbMk z@(b|Qr}*L{0AUHr%tL`ykodKz!;ZfVsTO;0X%Txb(;Jv)QianTBRo5mE8NkH>Ltu$ z>N|1V^s5&Rttp%lJaL-VvM-%JT$390$c`+(Uze{Zz4eAf7{QSRPeS!i410z=D+A+> zXfm{xUdy>i58*d3pV*HfE_!p{fkJDt)mwCXioWI7HxEmGBIiVhWUAH;UoH!P$=sAb zFe~~eWH%;eK#8SEKqf=(5g*1BfJox>s@>M)SgsCpi;x1bm?+wUImq$BIWjpnlO=2- zF*!05cR_RkUwr@hR|MaE4j{QK!2^mph7Rd{>^-ymmdn($%M#a^oyt9NM`<(+8+Hp^ z^Kq(y>~Naca`|mSvpEC(Yj`W-oq`w1ogO6pt#&;eV?{x#&SkO}sV^vPEestiiVEdsEQEISbhCPzdewiJR82L|Hipass4ZIHg(_y{0i z3?xxOLC?{*{7Sm9_oq^-+o>m?L_2ZkXbQ*6R88al97(gw^xG@|-#{x38Vw&z=lCTr zQmL$8*vj(jZE5-a;kx&K{DD5>Y@|$<5J%8VdWC-M{hj-<{61^!)OpRK+uJ5v=aye0 zphHFlmtP2rfIPh5Pa~KBkj{cUw3b&flFZD4g#9VNXF+F3pL~Q*KZo=atb@SQc>u~7 z0U1f;(g-W+O(3DC=dm!j=!Iy8!6;jP3pPE=uhf6VnN;C;nOb?pHO?9s4YLNmy4CoK zYcXjs*6w|y!s#CtIlS%-r(^lGOQb2{Ti3n)#&y?yN&Z13Qx`11FB&p*o(kKCjTT)a zL`Zej@;eG|SoH&#$|5}jZvueJuPU>lr&r)xp7C{T_>l0?_n&+ZYS4Tsl1;;!n*^84e@3i)U8pu#?u8g4g2 zo~o~l}O4JT}Rmfy}vxBxsI%EiT%;h2fZ=bkknH3vW$Hdi-OovT(b=#+p?3qHY5 z-+lKJHonLA8vr)G0&zG(^5vylW+%gK-e#>wyKKJS%x#jmDQL*S6WO}eq%P&L+FtxB?K~E9JP(@Y&DIh|Pj!a{6 ziGgeKf6uR(bA_uLrs@wHjNyf=!eNgzAg+0T-wmc-AtadQ#h?q)lOk*reE5MJC|R=Je~>MQH^CP zm$=4RYP}JT{a~CsO7+nZEm8!J*rjju_1do+dv93UxBT9A8{0*tA*YYsdjs3q>gyTY zqI>`S)S`Qkg-ze`I}Uh7M-Dy0HRc=40W(QN)9?pJU5W`%L7E8o0z=XmvEVi+ByKaEcatzh}$uJxRe$+CxC2FAh-_zGY+ z%wUjGVf$C1WcTb4r39D?STAlCQCMH!E^d2y{!VK=e|hCs>c8@=vnpjOlQ^vrcODbC zb1A3s>{O<3RN(kHmFK4F-oeyF%_xC1wLLPuoqNWg-f)-8tz++PWdC?bPY^ZB@5_CA z<&FpGHvbDRa+_r{`oAe(GD=)~TGq!d_F^Zn=%?vU_w4#=l470#+K6hR49NKsLg zo%Ny##m}|{Fm{B}=GL!8s$$}fO_X*Z(}J}08KBA{0b{*j+sh5i{{GgM-%HLqt2}F9 z0>`&gnZ&V1+zE}HxZ|0^X#&SBahzuQ#y1K6@ndm^@~`T<@~;LBN766GQOA@mzp9Sq zw=>kTIwnj@tD-$APmp<2mldx4Q0QADbPe^n7wAs`w$N~6*DSgRzRA|9!m;dhbeVJ+t7iGVWzVAqCTx$~ z*Vp#~D_5U=?%!?E?IPUr>%iZNF*;NEo1LJ){D=ek2o%I+Nj~Yd$#=E7^f4|D7%_;8|%ZhY0ZS z2(5^(jSCeOMFkmfF~cleWD$iRE+PgjcIP>nnVcNWL|fA&)&97b-g`H<+5G!`=lg%( zU)5V+MCy%(9oF?4kT!srfqDaNtfo0xmyrTw8H0ew-c!r3f9(B@ZH2p*DO{&dz1^wd z+ErXvt{~&$zx1gL(JvxO95=*`(EH1~WMSaYbJEab&On#e`dNNM#D{O6X1Ef!ULle+ zzgx}6mR2exmwk@R1m~o!-Iayl*4tz?RVl=jyiPcSwfuhePut5+KBjftOaHtV6|U(1 zPkPb6HN^c_ku*tkeT2e@*`^w3+J`DAa?@;?X=p6~ElHwZ<2D=u0Sp+)wfxF`Ex)u) zN4>d1IzFK!lFw# zd4-zuRN~z2)T`Ncs?(?5+S$Kn!-mV9BJLt4ar~M&?^C(R2?FP7n=rf_zQpmu zO?-so{3T+mTj3bgDRa-!Ex$jpm0i+L2 zyFnGsZh&F6(#Ejz8koXG1y~I(({rjtK^@0Uyo671qZ=5O=aXtD}KRX;_ zWV`n~ok92~AESt{bnj~1WtaI{eiMBZ?t7Kq!TkIe(&oARbUBqJp%sY`jc8K}83LMT z`F;O?xCcK>{4Kxf(6WT;)^Z7_hlDB?p$8M$fN5Ajq6xqKZiKL;p3P+D9@gh#lhVFepiPGs$N1I6_cB#H+lf)j? z^TJ)scGjK<`dfZw(i7$L&%gbK=4}xdStmzeM3!I9v;PZ=F6YE7zgSIRJ0fk4-5qdW z-0^^Hxk6_hHgq6v0BTbL8V$g%Ba4s$AYj)6AhLvE!!5sYf6H&|UPs~F1mQ{%X}iL0@`InZ3;c#}#c?!K7jV~C@^LeiU`K4*Db zKavq5bqsN57TuvR*~9X?6~szOTAsu>6F?W1VHhM+Yc!)^IFD>v6F@7FMxteAN!_qO zG>nwVPVD`pU+jJG38rvN;yUA0Az1J>9@yYM#IgPRHh$Zw{-J`7#*F}eOug$Or=W1> z3eRvJju(y!9Obd&Sbl&0_M7j2CL={qxW2;n>HpHA%Q-pA?|K}{_Vx} zn=%;LrfS=D!)`^ua30x|8h}VM0x0S6DCcF@@|)=wdw&a+xb{mN-@kWHj8L3#Xkm4W zg>Es86K%gjT0;+k<5rQ8Xq?+93yLQ;ss3k0*qniC+}(XGzvD~ij`@py_uxNdPPTW? z9@$RB^4lKxjq4ALS_TwiAKG;^(Q@t+r0-=QC{H043Phk#U;n@G{6>*-_^?rpD1dhp@Q^Qe#yNFtT zS?v9(yYeeoennxUk8G?LJ)0hI1{QmN3Xm*IA$IK`=GAcRN46FL<1kXQB*QQQ7-u}o z?_@vA?+xuDjwzfMO5;l0RSc?dLZ2#5Qw7I}H);?OUf_76I8;1+<(%s`Nj*<|grmRR z8tx+h*!$3U-@qiUA?_>w$kwswzH(;K<@}c|zjEDB-9H5yL4<0}6lzw(v`p9*+L&uV zEwyH#jn#-qfP!O?E@4P;%WrSHh)xZ0)bdMH;ZtAqr!esf6MRq;@!X`AHH{H(LGtX!i6w)m|^#8)2eB;T#9C%VuG>0}fV3)g6|W&w3wsKd-^6g>H!%GzzceH7$veC*m9?pE4afUb z&oIKXq1`|lE^$l}Sn(r1!-;rn?xS&vc>l5cee}^c2Kz)_5yutoqI=TmQ+efAgM;+S znXnCmAVb0ku~fK7h{X{x4IQXw(Z;q=3m@3DL~BNoP+O*9!r?f#fM-=1P&W56*v)_N zRA5QY4E4d0ggrxC%-{0+(DmM|fhwH4x^=U23Db4`Uh1@|6t{-E@Xr@U9Es~>Zu!aE zDD+JH*QLbhIhE2@IKI2}PrB3*$8L$X6$+O)A)tYN zlJ0ee3!PUurf}TyJJ1`LBC)i*OeiWzPm+NkjkgQyzen`1hPY=o5(w*wRe}>>qMn6| zB*|IJPFV-`q292gMl@0b5Ho5f5UZn2yn$2*;fRJ~A*Em2&{AbQADgYbQ`s!0kXw7F zSlp9#s(`evZR|jQa~s2gTYmjx@9(|-4yTA?dkt<8*BPhsK2`9G^q4g;edvG9oPQm` zG?l^&I&qlFhYW(>_kzyx%gdZD^}fEb_y3Z~%HG>-nd{Z{m)FVLh0?sblOluRe|XAg zB>2bPYw=g(0J15Z1f~qg;d)4GAkwJUYE~;-gH<}Ckd9KP>7C41 z_ia&rVdJ^V>Mr)m#Z)f!e5H(~;PjTfh4M0h@-9>Hof3uxxBMpjEWe=#uct+vJ8QVw zb&R+Vao$VZrI*ub1HYleE$s(2c%JxQfd%dd|i zF8*(0?*o!Plb%|slKt|`qMH1mJhN4qk_HwEea0}6ds%+h01T#i({&pHcBs7=e}EQZ zM%*@ot&joBwv2$K25JC8^@fbBRm0JFYon}!@!HE9uYsY=NtyiG6sXFr&%}ybFBI}h zrZWEw2s&(GNN~$-lqDdL!ax*XeOzir17YRZn}C@ zF$tP0922-p?xqUo7ki&RqN43=>Vy&ss4?lS(DTntMHlxLllgQp^iJjFXJ)HOX{JEV zYPeHLuN#=DX8?Hv$A(1QDENb>t=8nEgk;T4NJb+I7#Y-*CIYo|t)6VjhjuH0lxb;@ z_=QRZ|Ab{XSwb$;Z@QFQ+z5h^PHpa>O-`Gfi7r;AH&#;R^lMT&fKj4#EWa6F%kNW8 z4abT&O7|hmTiskizMbl%3OcUezb@eS7r5@-`ys=4)!sQi)6IgB>=LP;=|?P^nUV?&X|1M{>#n*k-O zO}}n%YLBP5ey4tU)ik0WRdlQLtl=2q?MsH!qdGO5zvVaVZ~6V@OSXIOo==M9f-$mM z+zHQcG>J7 z#op`f*!xGWar`1o;jZl{9B)&_K9#QDdu^(aEa<u;nK zX(m8iCfCTIkWcmL#Z$;&Ex&Q+s4g6`2GCT|Laotgz`=M*#jz;tDrgg3C_T5mvy5UK zFUNqXHL^$+*Oq62;&Yp0-2!-)U&Sx>o>_h$xQ4yNUH6wWaaUeJyHw$Z(&b&rsh2q= zTpx++HBcRyTZC&Y{puG5-Rp9aH)o*oepi%QenUr!z5j((a6f`r z*Vgh+Juht)chqP#Dit%NO?91|0J4e`BKzieZ!M09&_FG}b_hTXE}14^#$bb_ES^oO z;wY#qL8zvsQ@HDe zLzsVEzZWBEcoBEL5J@9Mr*ilVb7YmAtFK(%dR`V`h|h3svxuv3_aD{r%M|Y0?|=N^ z>8F1p3}w2!{p#)q(|Y>ty*SZ{M>auD5M^*Olun?V)UPE#9=n4Rqb591g)<3%h69w? z5w7SG1rGb5ghD{6FuSbg-pIxAbqGLcDh5VyHn3gPU&t5AAp0NatCiF9lk4v-ze@bR zYp(GuaW_$f8%kGowW(C&xM74{`=y3|-{n+CJ4DVIS+~lEsk}|)2vPSg+zUs!$dR}w zVn??8{`~ouKmJJ1V7fQr^vzX?q-F9C89U4GLIE7*)$Q-fWa;a#%Q&M)?EPR+Y5*(? zM};7npL-cVP{v#D?7ocs;%)^mOy*5bKfSFS8~I}(NwN$BH|?VJaMCe7I57Hclo2o zu>5}e^ygIIz9pk1ib0T^CsDdw zK9$vQLO3)FPNg`W-%A~d6BZk&28a)F=l5G`;2ZFa;awT=l+{zXzu0|!YB+9&bDPx5 zA3BERSNem!GYM_248ySEJb(j=6X-(SsyMq495!y=zcptFF~Ous772}+(=?MYW|C-3 zG_jUyY)jDA8nud|LL5*lC?&P4DhPF1#88KKT7)@o#1y7W9Xo=zFScUuuWc)wbO`g(-l^YCuHd=sC9W5`)k>scXW|a$-w#~Cm#?HwE;yB0@DAxM zTS;XV9J`#v?w5C8;aWA^pR@dOKa4=%CcM-Ttf31@xdDA*%XM%4m&AJU=j75)nYB7W zb#7zJins@SK3>QF47dD}`%|+ZNq1!iBSAnv=?I{+6a;8op)?oEu6y7qbj-O(ZK7Df zfmPVWC@WIaOM|m+v(U z7&fr2Em@yBk*4*;KGW|Y2Hy8QrSJ4*$16gA`0IvPeziosSPA|hFKhwOtgdV&LXDr7 zxoXf|+q~}=Gtv#4&?s$g32?B0b1)WmTS^HHf+C4(=j>LmU`@~GFyoClv>~K3APZ#yLA5Z zTMKEQp~czM@+&G>e${>DSHN+xqyRott2T5%=BGsEXP*#D*nbXtY6;yh8DyGnQd?bY zCQV!Oe6?AvSGjOCaWDR>aMSb_{L8Pt`s}MOzWKmV%df2hZh_M*<4?kIXp;|@02D2N zIPWpTA6~ZDmWrUPWr)j$gcx)np#!GOWUW}bk60nYq{7>rJ=kb$MFr>< z3cCO1kb%%+@Na+o>O0T2E+*6L|95GW?0fGVviuVCV@-nO(-jea7@avIAmwzq0bT$y zD*zVCfH&gyTBD$7L!gwFk#?56!io_Ym7CZYqqF>Sik4scwsiA_RN*L65hpi{oFfa} zYH1wv#7Rfu*lMcc{=K9&Qp-3|*ND7LWiqE+!$}IqYB;KJH1=M>@~f&Vzt&P~ub-jg z(Z!rEtqcA8MSO9^PJCOt>v=|@HXnkm>H=Q zpwnWqkR|;65blA8KYQ<^S6jmN%IEKW))F?xi$g8HBmzJX@W-i@vOYt?1`tokrG+Nk zK2ih{_hpk@%*GjEh-Z_4F$HMUvMxG7e&6ydP|GjnqOK>dRl{A-pGZS<1=;$2{|%)h zku<|sw|aUeJ7hF|nA^DfA#YQaqo7-65vp&Jz)@}zc_quQ$w1V?RI*Yj-OCNI{JNW4 z)kX}wHMfY>TEgwv+H!4qb>(~AKb4HXRFG|M)oV-DI2?_20QFUPV<jJV$O6p4 zKp8O3s(X2Kj4(qSbvYtm?d>SxgT0 z$o9&!?>zg;=O4WN?z<0AWmD$Wuf}8f4LawRL@$6;0J#tl&$~ffem6;F9V$*?oftDc z%db_@@_WmLRN{Kesr|Wv-Er!<>?Ka7a8d=wP?k9M^hROXrDKKl!ORDza2l?oafcPI zt#3@>?id<-uM&iHZL3q|1{mV3TPxi9Pe%VwZp31%-lo3lNA9P5bdE%W*xCe%)kC#e zAz1TnWB~lV397z=##;*W`ML0d#V5ucbIB=;876EWzuN9aUzG{la9(~}9(&JYG=$dy z%#}4+A?R@$0aMlvkpH}}^@l>+@OzfuuEMpdIO$9ryY<_WI7#68h7HoFfpU^;HB~tq zx~Fh#mO3d*+=;!iaj;cH+PXhk)}_*Maa-fs0!M%NTmRv)_o!`En?7tbY>3uhST+k$ zuaNv%Q;i4eF+$Z2Gs*c`;qQ$$*9fyqoY5JWjRiEZ`&Sn|MiQ%(5Oi~8uL0wOO!pqy zX!bwrMUTzL?;Vc49}O#I0ESY)0`C7SF@rGh1ixeXz5Wuaa1?47k&;uXVT4UnuRf=z zaGg7tXY@?J9f6Zu;s%7$$nCz9yfJd(k*J0tIf5q*dWMq}j!GPjy*K?MWAD)=S}SXT zOjCn}W+7M7d48%ji?AfZ_opCmm^@q_%?a%Sq%R{gHf<Lir|mJt0_syuDYmeiQ$&tD|&5eH#@cE7-8d7 zcIx-+BZn}l7AUW#%8NKAI_>`^Cvq$l`iRCU4N^OHIBNHmDO|_$Yyan3emyl%ER6`b zwR){cR_bOG{#J4@W!?JFT+$I+jN9RG!6O#3p4Eaevq7RdZOD}Hy?{bEV&i8p;$HMU z`pEXqJI_A*`3Fqc{ya~-0txP@A&y^MgpkxO{!@pYi@-b&aK8Hi%&76Fn7UU~u>5MM z<@dsl#L*f~N{8;Xspqs5?&^ULab3%=+%KX$YG9enlN~giAkV{*Pg0d%;V9insw8md zU(=4g*Z<>U?|YWtl?Eub4IXIMY~_Xxe+ybnWjDFy@RC#Sjp&Pcoy!=-su_qOkjn)k zbIF3$wY07m+#o#0VbOh-J+h66H-}k%EtVt2l@YwdEx(sgh3iUOXPnB; znRlkCEyDb!F*cQzZ!9)-@`c=xgocxjtZXqi+?8ivNd@jqYWY<##2sJZ zG&VvwpNX^)CZdZv9)BPF8~}647I8VP8@`olE{-_t331-LlFnj2qqW9n=Qcf@A4WI8 ztMifVIJ`LY1}2x}z&Ubx{6pB~u>IrLJNRb*>+it%LD956%Xt7<6^=ZH#@^pf6;2wb zwu?BahGR#RB-5{?a7^NkSbm2kLra5(i4Gf>1MjfH4UC{+H5}c#?j!ry{E#bMPK10q3-n6Rb7$)w!_rxKE4 zlJ+gap2LVOx=%K1ELFJNtItA>->)YhS2Y5o4RQ<=o| zDV%KkUS7s^WKH^NLc90USVOjZKkT~iN%VQK{R+q4;b;j*IkRo~HT^-$ufsul2s+~Q ztd@1mU+?UGvvUAo`@7xUZ+G_h$-~b2T0A%mr$8Ee20kBjCh?f9Zu|Q1_PT;3(MrF&d=9Y^uq;&W1FPA^Wb&5E4 zVWg{YEdKtKA<@u>a_`wf!Q^T zU4+Yl#jv0WFZfHw6(r&2OG~;D3!C)FHhE!Fw*0C->tKId*xTQ3OC9jt&Uatz?jIa{ zvG?Q7*I#dcL4O0^>}+rEef{+ghnXM`&dCFL3jnU5j!8pH*z9_(rh{p)=};D8O}WS zwqsd-c`^5XFC#Tq*XVd{wOSRUv5iJ97+6SDY_JN%PVGlpjifwOiXJ-%FP#r|j z@m8Bd)F@z7wEWVHysLT&*Dc~M>u%r64`H%PzihAJ@OJ938cyy@o0M5Z(m3gpoiJzm zCSST|L8-Dq!@1|SV(*nKzalH)v>f^V{_gkL82u=EN!Yg*oT^(YRd=l#vl-JQML04k ztOTQhOTd_Sr;8o2|~-;iyn zqXA_Q&Ojg8^rGhXFb=l-+B=rtiw`Lrl{k5FYX30x$}_qW*E;iimXy{Y&rKZ=xFvVq zPHfU3+xJ(>f|k2|I|t1B&x%}06^>T1ceO3Q4kgPkQ@GAY3vsJ*uisP;)+~7sS<&zj z!xWh~Qv&`t1W4u4MbrUl=ZfI5de$O-(PcLlqp1@7CX>exh|zqI1Vj}41;lv{6!lqj zpQQgkJwM(0oE{4=4plgT2ZY&0E5^>j7YFF#Hv>T(VE2lFiYZ|5aA>i=i!TJw78LvY ztv|f`o!WE_XipEMN4DW;^)@RnC|~)d8F}|e3dba_J56OpoV3@_7uRsAME_pH$#U24 z5u};s1Qm|$-wzuz90|KWtxa3u+9e$2`eRsr1yR&bIYg;z&Bv-QaKUqI?Z_Nb^2ZK!D))8UCoV!XQzi)ewXZg8ta;S7YrB^WOtA39_)Ov+nyYJ_nkmL z!`99!g2t;atzmA%SAssPQ=zaCL3eCnbJ%94bdsR$W9Lq1K)9dYO61d}Ae&ZrfG9^U(nP}Y# zI9zZVmp5G*l8PG;y*IR;-6-cUlb+?IQzV5fC_4NlMs3l3z4ba+%E<3fI!@N;3i&>kS8Y>Ckly3(iD`a8BfM{ zC(dL{+jOSmxXZM|#2xDFPVH^CUfA^oo3*)vRstcxJ^@c4E&{;=FZ%~@elwo@JdMX| zyH0SS-|Z#sc2n(2`{_Bq^F8PLU2n@T(WT42-D{f{hB$z@Jm-u?aRmCz@Bj3h-~9Sn zKe{}g!O4LSr=+-JwTD{>+#@V~Wk3_|7cM2;DKL=kP;!8DcXxNgD3Kc7QqqFb9TL(p zsUO{)V+fKm83<15?tkz7xG&q@_q?aic^;UsRPuIqds$)Kp<74A%pu=xxu0B-xt{imo#t(bwo_5yD(Gl6x@(K{wajWoQLIIJzo_ z<-)c5HWBDKxBo_`YWl@ECU8pp?EY625Fe;%a;$CIc|lGbA9p`I06OZ@sL4CnS{9QQWF81_c5*Dr z!bQ;_SJBdK&sc`H$p&y({@hf_^i^mlMyukusl-@n%A!IW)zD4o_Q^wmyxV(toel3j zS!XfJWc19__{{Oy-0Z%E{673;{NJ9=-&ttM*j4ni`OnQrSPaG--BU`1!7C4>`m5E* z2%W9AlUs#O+WV}u)2pkE!RVedd!+TJ`-P&d?UmR+_t}?;z#>1mpSKS)lz3e9yE zTDaO`0AIOS7IED9K2@1^{fllBqC0n~+ovxBh%gz%-=2<9WU6q%w~%d)or}4&tM655 zo8LQ58h!1((~7q7$EY7nf79I8CR{l6X{I)>=l~^}XboQ0qISuMtnmpXqF${GePpf~ zG9c5#f6vRR&@2$`o~fJj0_opP3}j`X(*EaqU&y$i+R4}< z)IOXE?==IDL*x9njM$2E)f|gZ1dKqo;t0jbO9K)@);z1th2X~O-f3VjeW+JpMbfp% zy}|dzH0*VS*caV~aG5_Sg{gJU?q@_KkOmQSB-Z!udW;>uI}t89P!%hVb6 zuRek>qL#EE=u9Cj_LtO1+lkBYC;aVZ7J*mBN{o6jX#DJQYS@{&xf+mPK3E%S>^#)k za_+QIR^77Gf@uRK^mzlne*Ty3NPkW!EBdOqqgrzN8i*D;+C?q>NCh1xsg9U``1KWX zoAnstfYB;~KZ515gc2@L+Kd4;=ny-$S90}XU>v#c`io_bLEUkl?!+Q?xq2|6Sriz~ z8mT|L*;8i}Jg4X0x!?LpS&}7v;botZX;F#*q|>~9x$-03<;cd;E5nP&r+Um9d(zAO zU;M3?VS(c%TX>g(KlCeF4@I8c5Haur^FSeB(?w=QJ$Bbg=TkJ`yuS5DhKfy)0c*a=7m#&y7rqmGd0Wxfz3(X@ z`*-=gI3so3Z{TokY?;zI@^M!Ulx7x)6XHZ~mA&f-H$_1c&i61L?5Q&NHSHU1RWAvw zS`YX5a%~0&knGZwO@j z*V;|^(U~rCIV zu23*|mDCaWhNz9!-&eFyN{bd%tw)Q%Q`YTF2l#QHC7AobqR=v(Jzd8TRW*UjFD7=6 zB!!{xLhdZS%g&WH;cUsA0Th1J!@QoSQ>|%cY+aZyR~mGR|4!^oWjex-X*KhGpKZu5Dm& ziCB4ipfPIfQvzbfjuolmbe6L^01plUm$cREEN zk*`BNv8PPww{=*4ILbSz)|Ny*%f5+Sz~kzqv~I`F(H^w*E6~R#n#IpX{|FPKM(8d_ z$NS8{kC?`u07v)xrL6_%X8Ss1spo}Woz;~{h4-@8rb(CsF zlQD!<05q>GmRSV~vnlUH(IG+)(WpbF5-Fp}D6({d9wQSRT#Vmsq?F`iaR8&ML=TM8 zYU3A~N-qMJ3q2~sghxXx!O1`INY)}A&u2Q4ZwtrjeX*P@1J|)zPKa%J%|?T|9{!EX z{?kz^K4+f$BHG^@@fBuQkP=ZpP^6&^VMwUML2L}jCb$CORs3`cKY9s|(dJZkG~M$( z28IXjN0S8w^rqn`?jy&UpyT{zKCJ{3aq-_5zKFHl*}r_^WOq2XuUtTr*AKI)x-ub; zmVJiniS|eap7mN~nu_`ayG3o&txi&>^!b>6_7+kWy&vpuIg~H_Gw9_isfBr3sLwww z^kG?}Nzc@!c{dgO{NQ1-f!%^@x?nDoKZS!DwW34IU9vU<_l@o8TKDyeO9a4RS&}gl zrOLt0vZbqJHtHi~=`3Kk;+GRs`- z`6=|H#X<3rQ4z?=FV)2FThjTJ1^{_pa9q3RclRxhHA?YLXbm(wUMrMAl zY*deQEOtjrBxiVyw4YSBXSJ<9wz{ZOA4C^$)ry@Q}J6Rr2}5YSfP;M))Wx7G^YhK(&>Mov(`3F~|1$@Q)`aTLZv= zNP&Ns%zx|`KUY)l{#c_VpL@2-vn>K)`QZX{+Sla1j(%oJ3P>+xJ=puvKMLaycw2U8 zcJOv?bT^Cl{Wr_IZ=eeE7#knxF@yhzc~nIW>^F3U2c|17WIdtG9^34C?w&(m|R@5bg_RY5mAS%Nknk_#!8OU@g%i&j#?Zosj0|@p-}9LQG3`mRWNW6k)^mKo!r zl5|X9Z#Nx6wIXvl>Wph~EsC~qblmpvE@J@74w8zT6#qvTqkIPx&4??CY~Hx<8X&@3 zcb8ZVQaD{nHta6)$S@=k>$15tJ4lk;6@rH=p1R>3XA(hbrk_6Jx=s=Sm5Rg8_c)4; zMf{HcTpMUv# zeegotCnd!!7S7OG%{uw8azI2OrWDr}W+*g>S3E~iKi=z4I-?m8P;+h zU<#tvqzY&YNtBdgthcygow%pf+->a94+2zZc@*g6#wX3bPyIzl!66xyk^&hHHV^ix zm;X*bf1VBtv#!ROW8KD`fVMrPhgg7XuCRYfU9qP#3fFMv==8b}aS3_8r@i%|KDc;G zhgJMYdiQWkH%AX54of%5@%gNTemn@*d}p9Pr*o#6bwms!1(ATME&qX0WTy{Twoi|~ zT0VAszkHM!_ufK!L_~kw!S`I!)g7N?_)VpBkZ}u8k(w3OzR{C9C8`nWg0o3Rz z(H68UWkLw&8r=dg>w7?e%(HJ#q(u!u3-wl@I zLI-HkWN_kq(Db}-U+X{q*VShZp0^%|4=j2$5Zo=6dUoB0bdOj-sFAvqy{9QKvF z-b67=rhdR=xOz@X(|Eht*ax|<_?~{4N{S{?rf8#$c=nSL+@@X_BHgH~79jnwLz=5% zyt&oi8|is`ar*T`#@EQB^J78VZAg&kqVpodn-+$)hkt$l?LlV2$x=yFfhj-)scNj@ z8!MG`sv>8=stW(pk}UD zAV)N*!0_bz)3?{QD;Fz^){9l-W%`0A@pdt;6)GKs;%f0#%FdvWsT>DlY^EYU`PP{R zCTH~bt)+@496n-l@mOg5OWz^J+BHOE#B48V;ksOEJv@)`*rW6c7D;OcETlPg9h6Q% za>^C~*4&e~?nnlS+MtdTfe(>K4aavr)_tMpYe(19=cxp&SE%{}YO~-$*f)U2PwXHQ z)WnkRqG&igkV8)FN~FY4QHG~2x#~L(OT3;?XP*foo9hVDO&AGWuoJJ-!Mj@%$b%|F z^67sGLc;&ex6iarZ6l_M=gS-9G~2%9-muW?bt*W|%_xdw6FR3-bAM7E1b6N8B^rsE zsU3AYaNuI#Fu6K524w-{6Fuj%~Iw@w}jsedFt)-tqJ8|>!u+ijzBqX}{HSE!ZhsAX{I1PpFk^AM3& zYM;e&@!o=mrV&zYk|>ZM-HzLRSTj3rS|fTMM!ZV-mqexXhC+1f*f_fSJo-{}xoQ^0 z`Fj0qD@B9)Di%`bLl=!5?Do9OTqXkj+;~2mQZ}cFWN8Hh@lx%CxtI@!y;-X~G5VTE z4vp@g)LOK(G*0!t#^_DEL>d1I!B#uO6x)Ry-9ZFgd|f^4BfY=z^^U9=q=G1{Q5=nI zabd@0f5K>B#S2Zbynf%b+1Yo{_qkx1ek6f(q6^%dyD6_^pl&Ipccrl3bJTetE*y|k z^hjZ4x&6;35)@AOUT?HSXXla+O~}Fh>)O%YSh2bVG+~e;4zm?@IbXTzTBbC24`8Yi z!2mI3AsS^|5`Rqb= z04}2yfy+w)Qr?b8tM#tvrU`#(z0;gInarpII(o;25n>x@Q;W(yxXEvRm{d<{Ek={9 zLDTOT_F$ozoK~^8j0}y^XxL!FK6I0@%0vx9tY7Mq*ww!JQq1h2~dA2ed|0f_( z{NxGB*K0J=`7jq^1~&aEb3+`{eT&J8W#*wiaIxUywc}=uR+-{DtMryFg*2OplrFPa4 zAMBjp&0nMKFa%0jEi-r%i_G>#MmI@a=2z50^C=r{rHYBVyTEI056z)wU{&30weB9V zX}E9LnE?S0L0TyG3YMvHHm!}IM~b(MCccB-Arl`BIUXZ+zR!O_Orcf+`9Fk|j>rYw zLljp=&18y9JaK1=1#Y{6RjHZod29>X)3o_>PfVo*{gVX7L>BnCne)dfUUlCmLq&Oq zT8=s&bQpz3-+)~r>7?o&VJ$~zimnTj@6OBbQ!WSW7a2M^NcbSjjCr;*6Zbn(R&fDi zqTS7-pWo*WtjXy<8hjs!BTuLH+tuQ%{qTjuwYp|~3q=Wv#M|MPu$8a&#w^I{8tDw3(OY` z#@d#)U#yZ;nTF@{4-aHFi}qjHtwh;g2v9lm`u7wbAMPnz*YH`GTn$$yHOI%0HfoeK!17UNzmlfFse$g z9D4XP^;mx~l{Zx?Y9v`Ws^;xz3ZJlAHe0~)lnl5yWAx_fGH0n5REk<<<%JEnFtRx6 ztE^Z&tJADXzS?4+s+z|%x==)|7p35uuzLH7s{z)cu$ha^3`$*3Py6uVFI|w@Y> zMkNM!SkV}FyW!t~l}+nhf0T1Tfge2>s3oG?aPP``s^${9SD8tD5Q}JuB6J+wrkSAZ z6~z5W38scXoHo}eJ`d%zgq{c*7d;dE^rQoy>fdZmLQSSa4dYrg+=%#8F)dv5sB)+J z)wphBHQEk;4h}XtGz0nR7|jHs3G(=l!kOJK>=eDmzV$=Ur!9uL*y$ubp&slUUk047o6s-J^e(tMZ@nh}vWK^q^=qyT9|YbVu=ZPqGa!{I z9c|9TkYO|{5qM$vIT%$xY@Xo%I>S#h%Lr631Pm-5Dg*PO`TNK@Q(!3lWog3w;kV$6 zMI;o{rJ%Js}tS zB2gEZ=%qvYZCnBKOb=Jvaw^X|A&fR6L2=kT4>|@j2M6EPa6D4*hGi`;C;xT6EK8lH zZ(p?k2g?Mh?M<$}>q}x3`{%Ats;OKpiT8nm8f}lB_A?6D0rYyoC-qKiE$)}qte~hbd1a83TQ!FE=(OKV52g_qM z1$VR#BTT;6`sien`8b*7Q@i9oUe^zorsMz8>;FBsoEQ+TBczr$5mMTGkXLQ!cXNt` zmVr$}rv|_7SvD#ShO;O%5>tSqiR*lSzW>xTq)#u6so=F_V0H8QZ`XXdY)6|XBq5;J z=;7<{IzQRghg+JkXOgzC=Wivg-zW-;>=<>dY|{63EOgTRv>c>+o=7R)-c1Y_@~#(d zz77aWiYxM^Jt%f`hTdbNC7~OTUX=-*xHR9{yItvPc)+e(ChZPAsot znVI8^1UlF`nEr<`mahINpU;x7<7wgqx{jm$SBfF@5^8Jcjj@5=A1chK4;vn1sZWtio=^( zmOjHFd_FzBOBGvQr6#~&X4mz?%o&_QiZXH7-PcO<)Ihgqp8C@o6fpT!ArlMzdnq(7}`RK(|>5Zz)czzXmtNwaC z?Q(463ZldGwUmLgQ8Mgk`XY=AinMRMMmw~CK9$)9M!8sRGWAXbqG-o5&JwCS-@iCl z8}MO7T@b1TjA4^fdBl6nI%PYMdT4(ZH$dxWmS@kHA*LBJNYm2rUugf`vgZrAUvY#! z?V%`tAAP}?J8*XjQySkMBERMPd){^wrF9Klwyi9E7$LQ~i%5;DND3RcCO!5s)P3v5 zuDC5~)yJM*ik=G0NCb)>o{;8-OS6pXivz&t60W81fd2?pmoLc<1S_O0*Ggfl@R7W| zbL^jS>(Sj`ACdXb;r~2s+wroDUVEC6Hg-9^Uz3R&hTvGGo()MHCNAV7R;GA#JxNQ% zT`N2Sjwilx$+XiE@5w*>=9YE);2;83Hbrfk9-|l>u78xg{~8a>w>?a|VV=t+eDn$X zbhbChS#A4p;A1HDMX%fj*cEXY&Fov%i{HaITsjFs_jPPY-M zrr?sN<5z?4GFMu_DAaDgpLQO|s@6N`v(7P#21!F;I=7P|`w-!%e1=AGH~$3PVaO)p zbK#~xhDav;tk)Z(IGM@>boL3fQ043*3(qteSBGQuQ9S?J$=q^{!CEo@`W0OPduts2 zQd%aB&fTtm?u$+6@1Q86rt-L=>$Zn-i#%HmU`;~6)=D#~K98XW^!DPn=#j)adb9q5=yHxLVQ61E2b)aht?y)=48o{>npbJs4Id~cZ^wk~( z@1cOB(1SGyanLKt{ciI-8|ipfM;og@M!gr3=Ms%U`>WijAU#=RCz7+KA>Y44<%auO zK5FgXAeT#y+*>A>?ilQ741zJWhQR5UF=<6@rIhmdW+%qvrtfeW&~YlHIH@ z{@|=8lw+g)!DJZ4crvoUrH_dHRfOeb))sLuC5!wiPUa=;;o|#RxaTJ!F|3${ zxP(n zP#=G_YxN_(D3EWA-iKL9S2`Cnfd0K3(mjrh#Blo7O^nHt9gRJyY@9h$Rc;@n&nnRv#dGqP%r_=eu!gzmyIqEt?*7gG@lBRD z(`V*9=zWIQc!lsTKZGLhhz6A+g}ILJ8r?;Z0jkT{=kTcl*XXzob}F#9s^86MdP`3- zELIMU{Pu+EDD-gFVSz+krd5iyfdrsAsh!FB54wAXreIeobMPvf(;rGEB7_rEJsUk! z!+o!jUg0ezHfbux4S(Y_R#pA!;a+9#ORg6VSA-?gap$>at$=!Lttz_awqU<3e)_uM zA4s`&J;QPg=Jw%yCsIYM!l;k_XiP1!Z`&Rp`k;@uj?ng3o8iHN-yXg|zfJy6&sIKp zpjKIXQTR)D113brehSI|lqW=TS3v3PbIF6_Pjj8AJ7j9he^iQp%rXp~ASLDrett`0 z{R(=AcT75$5(7@VCQNKYPb?kZxJMPv7PBKTv&k>F^Nz$PUE?$H?VQMh^axC&_^%9l zmrh8CQ%2+w+*phwkE02#M5Nl7>M3Lnh##{c?jli->Vhn1yigOH=sYvU#7{1rLalUe zNOpD?A|P2zIj%N&v~m5iP9Q3vgfFRfGP~a{?IDnbtw@wCR0TYc)>o+Yl&0{*fkd*z zsCL_f#)&3VN#)`+phHsoTfb(yOXb^^XpFgz&Z8us)Lt5R#$#E+aShRq{=Uq zJenET>f`NXxV$tsKLueb3#!uV@kQku^+4{HT zZDo^1(!@O5W9B(3wQCiF5s99kmRGjA#?h?nJL(i~|2jXjtrOUJZILmfE$PIS2Gt#&e2T$PrOAdEJd7>O( zH?wiWAB%Q04Ko}TX6Rv6(q3&PJ?#r@!`4z~59K6$%Vd>dk?8y`gf=(L3qAa*0<1J1 zv#aRQb(Xb%pBNv8ofoC4nPJqwy^s+M11CgUzSHpXSNsqivSYEwEfPN+p} z%f(cn@m1E*x@932T9Ia~)uNEzJicBgdK*;|kA9VzL98?S3}EA93J+oRhs|EVuqk1a ziOTSchR_AIB>9^#rNA|fTzA_*s@yNHHUX)Zy0yx88zO0*mK9|9j}SqZ|K5!9zqbU- zUfZMqB|YEwJ(8;8xgwQyzUgocj4KpZ}Z^2~{ z0d~RMsJ|6~IAsqR!XW#u2^H<8v{v`_1ro9Wu&j=Ss#8r%*NR|2%mDH-iB+LdX_#>f zo}BYxfP0Sz#_!GA2`Xd8zYd>XMwOZ9a4Kof#04ypuSbE6Du})cR9%0vZ-2wORX{l1 zQ%Yvwd-&HyqR}PUQ-G6DQu=+0Mc}jMCka_F7$yTtSLIFWMm5#Il<4b2OI>KSwpc*q_!ctCzNo(9IWvw@$ipz9EtCe`l_EU{?^&k(-qRo*fs?j8`BN29yaq{xa`Z%On?6qq(jC)_)hw5`) z&Xr5y+w}?GJ`JWTt|0mjosplP{`QgjC1#8+YC<@zYbb!u9 zpP-35L2gZ{{t;#vaZsH|MOm)AqctQvb0Q=mY})lLkzeYr1Dum{JMw#D<8sGpE|tr% z<^6^(_E$fOAz^(EF)C*tIQDuA$P(|udN-Vs1XDL1T6MG@H2#)BC(hr0#*1kqrBeQz z)pyj69K~TNDVPm^?J0YbhSE4*e&3N={{7C3qijRMd3>cgzPi`9!p1eM0j$hv8U=hk zBB#Zm+kI7Bugi`5H&B#PqawRzRQ^OhHxTiR9V{d=JJ@B?1^S|a7Q9epk=}hAY~cyA zJRl~1=ydgA81boh#U-j2RS(htN~tY$v;~oqsq#T=TJLzjgiFOzXxAiK|2#P6cvm>t zPH=7dcWk@$Jb7|dQHrqr?lr~MVIdm@ustHL1hy_9v|f*B;*aw67TW=x_0}w9w*HS!a2g*y=dlZOBjXMoOcAeIR(XmCaO`2_5Mc+%TGG zEX1V2>m9V#@-|H522+jlr$5-!cq$wQ*rFJxrNuKeXNCOSlUJc1@p)U< z%9!%MfvR#b8x2d!Z~A^3H)ErHBr-?`hfo(gpCM*qTn^G``6 zYkgH~Yj!JcAChHW+D5F>{78L{@hpkB4R@WFxPQGMJWji7%WYO<6n>K5ar>|7Pnv*o z=thCPzx(5!aF^F=h;+{wOqf7Z(_HekO!z~?*n9xxA0Y|Z?qEMZe|uyF@{gc4+9Lmm zs+ceHtqm(A8n^Y%~ZAvH*L)WYnJhMui zSY-qROa=l!7$BvrCboKyy9FCyoraoU6~N0=n6K2^3l2oo9YnzU3vT{hZ-7$Y!npjt zzxL-2O}|b!eNY5PlC~OT1>XzKY1^15A_0jVt@qs(1k1~*tk*x`9|T?IojK>7(r=d1 zjW2h{UcBQE@OJ?#{-=`Rg|Wy#uiELbqO>&OONxP@HyzyzIL!9|HYD{K(uw|_<=?AG z`5*evL`6c4gO9WnB{H|%h?JhiRlKUrQ-H7$a>-3TorTIv~6?uS`kcfo-&ZZ z{?Sit)nStvo0C&3<|RQuqw_X z`aYrd_fDmpZ8=A|(2YIeFhLUHoB1Zb;!nj^XPdtn-%9<~zs>L(OB#FQb}0Umwhl%4 znf9M+!HD#ChPlurR&(M+^O4=11Gj=H+XLJ*Dy8BhP?1$I{4<;{>G_uLi+i+``?n%d9E_QP+Ttu&n&qK3wbaljVBaRO|3= z;&Ezt5p~9#hcBbBs|UZ5YYnU2OTx?2Mh6gT!_ItM)wKyG$zhG+C z%NCLTJZJqMw(0y>Ft!c@?caiEzx+II!Jc4$_kF}pmcGR$T`%z6&mFD99V=9%&ubce z0yYU*#!P$2YZ&%qfxr%bbW6Qb!vA9#ZcL6pWW8H&4MBf+b8SLlJ19TJAbck5N=wDDKv5Y zVrsk*P^li@XbbZRh3g&8{66!q%ugU}2;-!|#`UMj@Z#m=9bcz1Hr`7TIAld^w`w+g zLkU{-Vt@d#tv(^RWyDPzs``ayi2K^|#r1Rwu(6tjhK|HfAfld<)hvY;GmrhkeW5u~ z*$%`7Q_APjLAADBf%UL{)E~;01NU7_2qGz+FRZOz?yExFC-?fL2mO(1KmU?ulri&v zRiVyPou9QzY1HZcRs2*#YOP6!Y*HGdMVzB1>S#lV$~EIhqLa3#I^j*hz@Z1kt`WE4 z!W)lFl1EACJ{HAsWW|}Jdp9*bQC|J1LM$blTMZ0E@6&J!Lb*KkiU&%5IdG(C7W66= z-W>kzyTOCXy(#x*{214~YC-_bSFQBLKQxiPtW$k7$Lt!noj%mKHcK1 zCMzVjrXvgb{)ezhK1$C>{KD@T8|s$QxTaAweOj>a}O+yL%pSk`^xW4 zVgYNYS^X&G-;>X8VgnpC1rE9)$?_xB4}UbjvrM?5!7Wv>O|Q8W!OJqX$oMUR;^)^I zwvMpPWK$dQ^q<%yU%ctwh&8vwKWNY#hz;WCCN+bdmM44*@g{r|$&nEs0mO-5_S6x@ z;&ap*XmJYNoZrd%DX40@H+WNHR+CP|s7!rK1@pZv7G4TR$Imwaa76_yg#U_cxJIC_C1o0*)okgq~PT_$tOaHk9oz z_mzc2(OQ)FYK942izyz$3h5=eY|K)rfwi+DEm8zLRaNb?fgO@(ay#IX7|m!2r> zbgrJAr}v0M-o^DWsK{_3O=8CFplCQEW-;=0aCdN|(B40t1D-4>@e9PcgmkIR&2!v| zf(#49yYE|!UI5tyoYf`&tP%EFt0@=$kaEv*YtK_%SAP`NP47Zfm5Bfrd-<7Zk4xmNKQ88un2wy5dM%{7wB=3 zTP@A{U#oPT*7>EClYA^EIsn8ag!85uld@%@2+m< zOU2?~*5hvB&LG7PehOGSXZxg;$hu4`M9_oKa46{MLhdXoONaK&+J9i+k>Z7PCR9+g zoIb9cE{OoePo(>v6@rm8QfA9-*h1a2kWTN%bfD&+k8d~1?$cmIPqZ-*EKga_L|tr>Nn0XuA=^haFN}u^;P37`T{v{kIkczY+Hyr z^n0UR5C+nCY>xsloeCUWjS`?zvhr>^1UX^)7xAouUU#2+XD28)DQOvMk`|`8V*=6( zn^=D_iUQu-8Zg5xU2{RJx?*)%&;mpUq@hPRa{S@RMUi zH^SHvMc{8`=oJZ4@QxCiP4{Ed-#lQ6Brt`4(98i5hjPeZ&jzIG~qtOU(EP#5p)?|mZiqnq zb;k%5Z`y-N`KRmFJ8tKTM^+*tvq4v4)sK#4b(+d#G!AW2#asuY4)tL~#~)RH^g;Yw z4Y3zXHJ!&eEH!r7g6oh~Mjw?&u0CMzRfV!bj%wZ_8jeGi)mR}^+|dv3xUy`gCckvdG6H@8u^fQ5*xhdjCO2wdaCQR(0`6YPka7(HSN)z57UFd2jRoR$X72k^mJV~@ z`;5>aqK6>9Tbt;IykjD*=m(EPUJbgW2Dv8GSK}S#$C4Y%?27jQ{)6<&QhtoZnk7$P2W>N+UKlY!oC7i2-RmdaBtRSK4`)3# zZ&3LK`;;0!lfbX&hbilN#|HEY?7{F?2g=2PvdKtgDkQc`T?eI!YJ#S3_ub=1BhL|7{kj}@ zTKQ_QJyB8=(Px8F5TOn+gl6j<%F6c|jVd^<*fHoGL|a-`<^usj`MRTcR5nPe4Cg0Z zLwkRO4UYzJ`rtRA>)>$m4-g0l`0GrpI^mm&kM!nymFT!zM}%DMR%=~qz00jl7j=M3 zAb7n+HwvMjaR+IZjkpa!!g%dQSkNC+zOdI+0>_d0XCIf)5*?xG(CutZw<{_^{1kp$A&eSYqOo-Ta z3jmq{paaLK7-s2v+``KC(S8_D5?g8?x;oJukIs8bcb!5h4kGcEl5~z$Fd$vDPRn7* zZVfcicPzRihDud0Cw$AjvTT3$+e@z#XrR2tuO)m1{pg2!2ly$Jg49)tjPsRHSAlJ# zXbasXSTaVP#qc{idryKQYS?mo=Z=vo^3!kKaTUyiQrb&`#F0Omh^`0Zba=jn>k`Wj z>?Pt}Eq-jTreNf>$0iH)ppNQzH{EMdylG|^1KkK-62k@pfkIXTJuE0D4Gi0~N~6M0 zcPC~D=^5~1xD3s%7u-9&ayIR_8if>SbSL2sa<9oT9Xn#y>b!F5Ha3<8kgS1gH`d=Anhg~X~@vp-BzAFwBL z(2!~8|6iR5h>z(|sbM~d%)s7-N8M(Srf%WlVnLe?eN-Z$4&Mw5eGY8u7yq#Ymf3Fn z0&vN39Z0S|$Y(WN7E%IO4Osh?W7- zxG5cJyRz6UCy~)`R%UzrXZ~dvqh}h8AExlX`(WrJ8*y&QOFHRo8Mn&4Puk%-VNdy8 zmclrRMSG|30~T=Q-lRP=E26a8ErbbzM;!*A*#0=5y7Gx}{H}B3^`Cd-^IA<^IQT-T zBOdkb39)0jQ%7hIk^ydUkVTwcBljhctn3%8L<%X(orC^NTPvRDQ|G0-$ZK0I2H_7k zgD0)a6zz&QX;;6?o`%Y9{VF0hxay)F5;{g+aPNFyi4Q16sDNRh2`%L!i!O%bn5E0d zyzu-f!gM2c7)!l6=Dt-45%KNMp7-+tsRix}2eg+QjUqcL!=+_;;&n5yf}!0i6>t%q z6p?1+>)89p`&UiB3$MCrhAC7KBguCe3_@+m6(cGuFU5y{Bp9F$8Hnf|>8C-G;-6FNX4!^=4~;}{9k`=g&-+p|>0Z5G(-~!#gC+pjHGt|om32v!vvV`_ zd-iW{(tN?^kK@tJFpSd1A~z#u2&fzK^da)D?Fh8{=*K$f`wlAi)5|Nc-kZlAi*t0( zlc|{tWm99Q4AK1Lz;8UzNI8iNL-IYo!p@>oTV#wY6-+mS;nzHM5N0mpfxHyDbU?ZW zP{!kX5KzW_pPp#vpkSyhq0e*M+Gawqcg6Sl;p!+qQlvYK)S0uFoDn!9k6vD6Ke& zoGrCtdMv(T#bO#(ACe!sJ~GAxk+sN95Xwv%SPvK|M+M;cc{m7q-*AFcXqm`NhV}EOrhv)R(PO#YiXGT55z}XIZMPe{iq6!`;r`WqWTE&#|_P~Lg<3`+j>+oY$X7# z{^+eope9rPQWWj(4h|CAnIblTIbu_b0{*j~ZiGZrOakyoL$Fph( zY(APo-e34tB#IiDIMD8yc9t^SVp!KvXSV$>tA4Qnm}i5R@+iU{;Qy-fiE3^GdR)~3bv^hXv5-$3wO46#5U*b^wM;j|dk<~SZIFT2k z`r4fdCRZGu_~Tj~E+jAmKZBRmZ5sy$X4?u%v2WB~@W+tPS-R^10V;Lq`!w`KPjr{Ug16xQX-D|@ zplyS!HJQa_dPtc6gl{KR>H|1sbmjXNwLhE$Q5n6x5{+J|5N6G@WrXJ#R|>U?`uj_% z!Nb>JgmWgXw28zL4qDn*AWqcxfafzJr0nIveJZRZB@b~Wfd50&cLzfK|M6Es$Qgw? zM@CY1Atxy#3E6vRWRE*L<0Rw6Cwpb2LY(Y*=HZMB*?XNcGY`de9DeVxDCSnL>)^oLV2`VKr24$W=f_&`$y6$i~gMQz*okSw&N6M6(# z%h-s2=yr(0{~sx)5?4;MR8bdcT0HPo_7{e^URCs4F`t$u_)5njH`}BNAD-~Y=Yk~1 zb(l4OWE3r1NssGG-6~C z_xA*7d8Vo|2|Nm1F97t|cK*O-n_xXVGogyMLk!&t8ibn*-Dk@}RcVIE1z2~k!5SiV zM19&HOF1b(q;5hu;G2&iGAIk|&2X9$sbCS&0cAu_RX9b6ZC*m!QX1Hos-;9cVen8- z`4&XWAGHmmohH137i*}nGm1+Oy+`M?zi6+(`mQNih`GUg@e)pZ(bi+Inp%|y&rwa4 z{>0@FfOy2*R7BK5nCtH~Or31I-2Mk)S4g)p*bQKtdp-_TMg1;ZSnd^H=4&!jfEcRU z^E`ejZTm$z&!DFG2Nhf=fiy36?gvKl+obdgKp=47&VeTOorl368cStP>lSh%(|DV7^L$IO+ajF1th?R;%oV6u{K!;%-3 ze0Icg)1r^?BCJ;QCD_c>Tu9PTxHx)t;-c+sj-Za5S6V57)&gQE#oUf^V=-dv3cOl1RWIxBwqt zH)ua49mpAWTADUKG-~*>{9$ASjea~IaHlob+M>t^LH2EiFl9hjWSRh*ITNFb*ea|X zV{R6`gv{$QSZG-(l=F6(izZ=>wKXc(Hl3zk#sg%lh}Lev<+KI&AB zW21+m%d;%%o{bk z?)qvo5#7Q_s}*CX=$uaJxoN)y{L|JT@~v^;;sE-;%uO7NTYy;#q5CA?9O+52>3{bEW8H@ zbY8q#zG++qDqijRq7Ev}m?AjApXuIHg|n!W^efIh8+TIa=k%?%L(;9uqP>+K-d((y z@_!yeciQunI*-^3%Ik%OSjH&AFc zOI??A_C9kDXi_QLr`9Ke!)%_VVk_WRFG)9C1aO_d?d;q{-UKv4gwJ?vbTYrwcQP8hy(j2_%M

zQzzekS;-<@HFjwn(#C6g7GxOCPGG0q)Gs3z>X$D2yCOON{4-c@P+_N{=rQKn$%L_@ zi*m1Q3?l3wqLSDZlVe!pOj-QUdy)`TeB)SoGiSyJkN6gU83}9ce`qL*oM?%!J}Hm)IakRnKx0^bl%($0s9N!#*eMbzhT=3|K-VM4Xv6XAm>!in3x<-Mi)G z1UuJzWZ{r#Ei>t=0gP5*n}8RR%Pu4Q5jlLT8vE-rjdxlv&1ASgbOgj0>x5)bRTAX7 z`hh()G@KGn<5tjH*um9WP&y%L@rjlZd6S*s2yMpRL*|`dDk`Yw#rO`l07i1wtCD1D zIL#U~h%1j!QBsqda@HZjZZ8o6#@gJ4=#pu?m=^gI&2GDS(TTW(MN~8D!#lINv~~(w z``iH)i!pXjE_+w+zSA#(Ex}khx)m9~(WJ&P-BTa;)^Z2V=}n`n2f|m#(#cM+TOiHi zN6bB0q>O^E8irXUU1T{WO|OufTyn_3X*b&(pd&$FvlcS zMAqt7x_I_rLCcLodun}h?!b)E_u?ARgDQ7=f zPrFQvJ-beXSJ{rKNwt3#m3MlD&_llmq(Q z&z5EY{dZo_A5FPr75}FxzG&_t$Qwn&$=E!?47CB7ti0HJ1Of8uaGoro5Yvx$MGYd0 ztXXU=*aHVUlRuKbFAdUe>5uB$9xcuMSMeiK$x$Cf)->J65r=R$_h7&Dn0^Vb1*v_) z5^jR>#w^!F#$R4bsoE~6k`>VDL}}Y1!}*NB_KWg{nI_Id$x+Q;RrBdNo%@7&V;r6JA(FOBpi-pR8ynxXiT&-(rw)qlD+6ZwjXB5@ zATYJbRvE8u-C<;rWy~m8dy^KELsJJO{}*o~lxbj;5jEOnth9f1|2-+1buMgK%^ohl z4QtjOJP`vcWq(|ji)|xFI4f+2Ts<%^HMpZ*+xsFhG&3j| zbH52~z#`N0Nxnf|v~#=M^#SAY@nM5B+@{u9Ig;xj%aRSLmJEPdmIQ0SSY7!maP$4p z?H$;*r{a?FI4lf3rt^s>8)u?1mDHgFFoCeHpem>s!H;m^jx5B>=|XIVD=K7$AjAnQ zINO6#uL3EpGN=l8;H9}C;Cz^gDZ=EWvXyW1!^xJdGH^rOdU-LrH!#21K?m%a=aw3a)uoWog47Q)#TS$fXcYtdc!x~YOHtj#yD)vOZrN=~bi1WeVSRa0w-0+b5k1KzGUX+_lyjBb( zHzeN703llpeg`H!Ko>a!C)B_?XBu!U+1F#BZG7?;*o(-Mw^T%cU_L?0i6-KYpW{4h zO*x5~b2O;0oZsqr0gGY|xPnHGASTGbT|$wj3L~;B2vFyrD`DIhBzm-z0WrMIEBrx^ zg-+l|h5R3YRTq~Le1_OP1b>(c-OMI~{Kve}O7|Iz5nRlqC~085$JpX2nLv6@;UJc4 z1dw2;Zq9KxcQ9^Bt{q&uqgy+hmtegnqqIDB*$GNCha#WTX$p}!4?Z`jsbswR1a+_; zG7{c9yO30O+z=&aez&J{!M{8kTkt(8wnf_;yr&Aing3Z1(T010v`zDZc+KEibv;5TVhOAku)zOaTk1&iA5o^g|U9 zbUF#$<@?NzR?)Gb$hD^)jO;d93`n}?t3%Dp@83W-U=Q|yh~vGKnyyJg3idVM5yH#2 zw6fL859fK}1xgP&9b9N=={8icnRA73io;6`gZQ2Q>nc>*T16cTMa#WhW9`0~-T5tH zamd0g$$6AnJ(>N|7bW$(kvbaf?E7Cr-|GBP+|%A0Z;cL({3jm;|4%-;EhwKubyBUy zc^f5;D5;(FEG_o{ctfOio*?50K?lnl9p#7;fo31bECLbYf<=$16zxw}KiqPb-QV(t zK1ZZt8EsQyY7NQ=Do4@N*F~krH%64_y(@Ia@rG=M^i?4;Rdvw${xM2#34P`_E!1`@iohq z$aT=f@lPiMSMy^&NKQ48a{tQP_v^(Mg8}tad-E|6{k-ixaba!J2Ri2Y-jA4z#7<`# zEOUWCNCr$d%kTQJ_xQH_FV-~`D>*C_4e7KSCv7{ zIBcr9hvoORNBf_yYHdp3+1o`9L!fhQZH3w_WKp~04T#vX7mg@7WG{6fCuB#|PR3H} zgX5A{4)d`tX)*rLXA6By?r;i7kd4gDQL;Fu+7U^Uo%vfB>eL1Yh-(`m>T$SSjAD4E z)MDKg$gVRVMrJ_1#k32ZwdKnPm>1J)Ml0zL4z!ZGHd=yMy`0cdRC|5q=G*WyQdc1Nl{Dd`Z^;6gP`!~VX6@O?>O(b|% z=T`tAlH><}a_f+BuCRBv8ELyif;=Ir+6yTGeX2{{;lK3uuc{sdpn1JrHFEx{tjNot zdYaX!crnRM&w;3^Zi5zP3%hD3j9nGRyMMR}BsuxjpSgOUZjv~jxh@~Qt}F+3YQ{ zN^Fs?&?}awOsXLN;_9iBUgf(~i9xi`~!HO0d36zd616$KSTtN7*8~ z<$>G-%TLxEOhh{P$-xIvc*t_&SOSW24;zWojW?$3d_|9P#7c)cBBZ{qSR zTGq>{NNrV&O^&f)(RPzzgdkFLWu$J5*u=Kn@mA|z&ZPb-jAgV zu{S{e4kaGcOX{t1Yd;+dzP7g3@4=cI^*#y@h|D#H5)K;giZI1a{2c%8zdKkWUjEZM zvpm|1KZXsCe&MicOB}uK@oFX!Z+F^rDbGM&Lm*VL$)FA$jM$TAjR6pn*P42`uQ(h3 zyW574fy>y1SFlfCNqV~2eUVfM=pPC>5e0!Jb+gPt6M!`~T?P23xL(dYrCX2g1&kcde9~T1*`$oa>t~Fml`vt%TuTC%CU>^=5 zwO`8+iEs~#7!5bjtI81-yFA@rV~NF@Lx>ZS^hsI`vAuvkHR20t+`OPPyv5NR?4U zm|7YWk=9nCR+Y98H?@HNej@ZfO77($@>vk7Ld~!L$`XfEc0+n^vVre~YeX0rIN>d( zSfB@d^Dxen8b`eDTQq*8Jhu}$Jw0;dbb2&`4sZbbdAArCa`TS=yHD5Zrh?v65Q;Rk z$ULytJSK%NJg83+%Bl?pn$F*61OhJ=p6=~S3Ndd+2#1R*6WVxxB0&A{O;S043XHaz~ z2e6oHQ9VbU#KpEPb+n1o)x21M0XWKmk)!#o8jcw7Gm7$1p>5o0LWjfhHUQa0fWLAt z$iazo=%2fW)_^;x}mjyl8xwHo%uXdK;LE zw$}o4{z)e6a@nFsR!FHuNvvMUty^+$3=wSX(SNG<%$c~a!MjD-7-xD4f^j3|i4 zdO_=Rz2qpFc5F*oULA<~4r9-swbQmAgJ4$j&M{|(3h(B6acj9?tKX&qpmg=QJmbeO zGLa9jf+}!uX=kVsNVQ`jBxoNYaZ89WRPYWN`!a&2Gm2Cb;v);SXj?K!R(H_`E9sgC zo89(Gd!+D4(rEXLGvlzf;48Yp)C-L(1Ay6o!ZhU4;Es|Pdt6$c1xbJI*0zi&^dw&g z2?|3)2$M5cEgCm-nmXLw^YC+0Co=7nBT-NtBJoQOD9-6<<{wG zT6CTS*#L4kw-4%SXCjsm$m)Q)^?X2&_60CVL;}7{*L|zdw|=Zb;~!{<`Ow1DIkfkh z#Tj;76-^9ay$PW6V{6vG3fms4pl*u=jb<))p9 zws02BYJn=5HeZ798kCuKadK*Ai6Ns=13>Y!d|~}Te&`r3@{Se2x(0w1z53n$;?fOr z$l#3vMA5|V&k6%QKxw%RD#R1)v()KRV6EJ#q0YR|?)*?!F2n-qO$FSuYw~qy5#dB{ITeRU%~`0`P;Xc zkXtaD8w0u28Ex|y&deZM$0gU(>H2!aam(jJ9pcSeUmq9Q7@bq6~nuht~%Mrmjcn} zlMWOLu3;WHa=~on@`T>0r4XF}z@ladPf#gtIAVq}BB#b>fG}bV+}8Wmd@!h;Bg31a z<`D$}2;FB!^K5ZUbM zf(Hk3JgSU<;%OI}fVb6uY$?0q?++N}y2z;$iG!1yao9v`OP@LFkt0KW@{PZ%pGy_J zhZHarUGsdC@KHvGuM&ziMa%~lvs4y#3D>U{nZjlaY$R|R6*T*)`Ho_tt zy)XPUsmyI(J7!^Kq~gUuhSVN)|D=TTE2JGn>$Q9217iG!>EfRv2my!XvN<}aE0_E7jsU2Rv{#uzE<}B*y;CC zaKTKbRl`JvYrmUF40-D}r>pCd`tm}T>NP$+z+!zDdc-3Z!pI@S{_i4Nhm+*FALZni zVJiWtyk(73>H_=yOZuXd$v))N;H-<`1~}H*@nmfNMdN-dP|y3JZ5VN}yZjDfHPxE9 zcLJ&;X>;cap5}RRmKiyI`_2&iJ?nd4L2BaR#s^jVpz5V{*Q_jmZo1 z3ImR#G+?FP>hFk&(-j$1@FJ-*VW7!#5I`xHlqX?-!c2h_I5+8MwWuG(P7^W+ z0p~@VVs}>tCH;@teI{+`t=IaN7M8YuW!{zk?#t`D)6r!lCA$4jjsb*r>wTPf0Dk%T zocvXfXj2weGA9ge<=bg-$4t!(h`ccDGkDLY&j*>zdABS4N>?&97m9g4%6Hp4Esz-} z{!evS--ihT5$Ua+JBQ_h+GmeK&#cc5dSwbUCdI`yt=ww|O5YTo zTJNBOU1WwZCag55TFp?-Jmk3QifOhNw2UxC_|FlIJAv(eS!p7*saze#kFOE-SbIvY z$XO(?zoAlOr2QbqR&!-Q@cw}udpsrW9C}W|uMcjC9d^$Z>?}=16s31c>JQIrs~a!> zKM51(*qyDYt*4gBHY{u2?57>V4T;M#Fugm?^eU?UW>UK6LSco324fsJ_zmjz?O`Xg z&iXOtf9kNj{@vaGC}&%m;4}7;vZ*z%6{SY7UnOb7RqFo+w6E{R6p1+fJ}xk8q9w*| z!M0(K9VNn`uRVv^(|H)dc#bEZ?4P?wmI>}_QN=)^5OYYQ0W|wuA^S4W0)UvJ7)udu zRNFN%RRwHT+G|CGDjhlJ@lQI&;I{0+lMX^k`w!Xh0jI?rQIUo9K=0@2o4Eyn+IHVC z8*mq;SC?UyDP7c_v5SGMbTWv7o^}F|rJzOyOUy~Y?~VhcZwCNH4(9)(M8}8OVGz)L z^Qp%4^$^1sRG9`$Z2L~o8FwVMs|Gbl{e(!ax)N&M$ul0fTyH!H%N}j3!3`zc%m_4* zg(wjdGm1T5#hE1F#Tvs*MS8nymJ;yuy+9bu=pLAyW4ltHqqyu3y^&o5yiDC!U#-VN ztEp|V!l!6*Sx_RcK)CW5y}8Bp{a6XbF&IIO$^~lmvMUTa2D|kJ=Q;WgfCk|D?ykKF zUnIp(3^0Ah@?uA48n0miQOjb6qEZRXr9s>U2LtRQdO?5v^gc)NXI1XQoD2$71@JA% zj`yVFZqN7)SPXlj^qlT=fo+CM!~(|i`LND*3mbXoD`&qHK++B^r}9* zJ6Pv8?6Hf46U)HYH=HsO@rQXD@|>}hT@1q65Pc=fdEl{GBdp-wqcn!dg>b;pKMh(x zm@#9EgqNDd9-3PJeOAf_iC&rw`wC_0_#=11s+z{jGK17f6A*x9qARzIWk)c?YRB?n`!VE3)ie`8ha0e#GPbyFWU=D<`xkc-#a^zP}jo5QW>P@ z7(QX;HFc$4%<4VVXG2!|R+2*aF~57(zdPEyiL?eMYNe`Am(DC09WTO8f+ik0WL2&; z0%k$!&e0itO4f(1L_E_oIzb(l248Iuax1-AEmarw#OShMe`Sq8KQ#@<0ln&`q-$)T z<*8Y5423?7ozNj?v3PmS&c#;9j~GS)NgX|zu&+8g+1w(IB(CjmeE8{GS371lExx)v zN+b!3V0^1O9)e8d*T<$-uipnv#$IKX>Fr?b=Q+q)!ank0Bi%xnA7JT5#Dz8ktW-1{gp{lGawl0}xQ!IBN z{O#)CSJgv4@XS4P$z8q8>+;wJz2)6gXVhC2A9hvl@%W#FW4*Q|2@S(9cpfIz8XN<_ zE0{XvW7Vh_^}}MD{-~5QPk6EhBe&?gTG1O2TjDZ{=z*)`hl4s> zx8Vl!z7j*>NBDa7l0d|3{<9rL=dAm6RfB0&TJnvxSXKJeF3waD4M~UD63^)ef*@c*5LK`ForDBrD*S_ zv7k=nxzi%TJM;_Y=w+ZF6hU>3HU&Y(+U9o#mS)@C-!~EbJv%eg#X^)ig$>`-TB%upw=~(Pc!V_6R9N?Nd!#}TvxOjSgb zD*#VAX8dON)YI)Cu*4}j{qh>A5soKCVsz;WW%oF3Jx%I12bcFm!wu(hHYM1q4OY?7 z+25AXS6KC}^_*A)j;t6ufASxlX_Iioc+7}nm*wzVkBVcjaMAM0nFBpZLoLspx&ayW z+7D|bzIE-kTni2mk9@XRua6yX(CU852(VP{QG`W#(`Y8g zFl2b<<^@`V}9yJ=ZwV;BrARFd?^oRf$pWIZE-+!o-juHv8sQ+ z0P`d@SyNIQl^c+C+%*^~scrPmQVY1a_P|V#;^rJd;t6*6zGQuQp>0N8b-&;ZO+KP`=eKo}XKw$M+{dA|{Gm-;q-DVu)}zmIk!mwcwh7 zclBMJ#oMcwgIkO+{2z#6?Xv>jo6M6-%4rorZS_;WPKl+NtLcP!fvdZn=qIDJgZCCM?kONBF%lnFC5 z7d^_&q4;X^>OA}YNZqFtocIUuBRI>=pxmalN>b#`&RS_vgWVY(SaTNRZ=44jY>@P) zIW|!;r~|i=u^Rm1ogOtHR=Fv_nAuPFWO7mt5{2SHq_W0wFTJB#FDL9?TM_`06^9GF z4?K zL#9Y!LJ@cWLP@D$jBxJcHLdmYSN9!kiRab8_30Sl!QA8^hfp(^!=U$7eWklZwdkC9 zVyT3d{GnXTh3H`gJ#ZsY>aL}bZ1!IUo94*-Q4^uJNd#>iar}@*56Yz_S z{fe_DHyWGjbFF3_9W}b<58$n7Q)GwJi1`v@H^3c=-(2zkTUCP-w-yX#x<3>U8Q#vj zwNMN7;P}D@2{h3A`}v~4TpK+aw#3m!j3|%7YEh}$1cOMoIb6^Ky5UcNYEz;@ZHGy* zg=lWIL4@gVxu4@RreDlZ=?)kTHv|Ih^Sul4eJ*46i7j2z-v+?Yrvnr8@l{3ORo{KK z4{@}Q=86B-XeaCz$TPj2Oj%~C4GRO2{YxY)G719#E`=R&A_mV^p$R}vwR!MVH zw!CSrSpg^A1n;$=bNoMhXjWZqnP4qMNGR_zwSIG)^x*RAWP0#!*#Mu%%ckMCYX*qz z&>ilI=p|TN$bFYKEoB^>#Ww>SX!%S9F&eIdurrc!Fx~ExEnkS4bu0JkjRD!M{#noZ z_*X4la5R6wQ;OX+c7xA6>#51KtdQIM3nQZ(1MSe_*HI{W91kW>6bQQm$&2ytBl*Mm zC+b@&RN`R~CtjM3MOVa^E+vo2QE~Ju+dvDVIREap+;NSKqZF?vJGpLApRdXLqFy0F zl4-8$<8QcapBjH{5g7=nUJSNV{q07`x+7;&T2?V(CEwIII5ktBI_TXsTH578_-(c_ z>*Fp~*Ew*+`g8QM#_j${wCne1z_OI>;>v>~u)+{bkXlJJ&_AL4U9x4d-@Vk1)9Yd5 zhxD??Os8uKwESOwEGs#p+8-WXtU{moU=3aZiF++iU6}x4)X;~H6U%66o_OgQOvWA! zAFUs{+1YL2JSw(jvIEFvCd?O%`G*{-hW9em8S=%Y|%@Z#%38Aa}|T-kqCdm$Qkk zNo%iPI|=tsvSP^={kFHS1wCa2Y!cgxcuKcqwK!RUMSY5uT{YZb19ot8KpMJ0Nqi)P zmK)Ch8l4<_g^IPxt-}TSx*$wL3Uv(v-}gsZNVza$4L0>}B?1LJ#XitCxMtyltzWO= z-_cTuKNQoVPi)AVTeTfG7d)D<{8sApRyg)+z}~_ zx=TQ(h^5XLlpWsuo57HWuG_%=R9{JPibb8|Qq>u)OjVZnEZ2#8;90V`21$ zbHO`r2549Y-PV<9wQ()*$~U&FWuEDyuNUkl-s)0)b!wx;{5078RTZV2UcBx8J{`&o zFK1YhITNoW-B-O!3^{WjC>DhnG1aqXjoSuR>l8`)8Ocx0xo@j*Ms~w1acNT>Ber9< zvO9m;Iv3*8zAAl#aTmX(6!Wc2_Ps?Y4BU*k6mF0LyGC)`yE%}$ut+1PqO$XCLr<*} zs~OFYNs3-p(!(+^*-DPf3B5M6- z3PAVoGSwftR)318PZ`e<=;<6Pzw|xuZXpOcCg6EB;wjH9S2(7x2J@!AVA<5I^p0os znESrCdqqp{PAM% zr=;)Lw5%HLx>fe=lxFJu8cvC&WCB1s#y_@YT{;^g`6}yTNx4$Y+ats7t6^%&S9awj ze*6$AyYKv4Pg$(AT2&<$>5&C1Z%ln*s6$Mh&`lC5b#JTsZ1&e>eGaN zj2JB{<%x2*0le8l8Y;#S#Q^+oX=k*uhr3L$)6q(tb`qf%;hPr)w%x>jFIh^|BVc+&3D%2IhtOR2zJs_n~9*YAH1r* zsgeCn0}!y9u_$DJ!y?R<^ywQazOUcleXLGa%+v{Ng%pUZ+BOg|#&LKxWc-?ab3pwKn>JB%HKZw*-Lf!G8rFNr$~74H?U zKQlJ@n=FPSCQ#e;-Tr;^!9V`X@Xz7fR&R*m$^qyyQ&eu^m|>J!Y+oW~i5(q*DRxwDbyIhHWX1Z_#E#dSdy_`O@xP zef~ec{hYcHF*)X{8ck&QHgpzJft?+U_(6|=ptk_A2&?+6at|Kq|z859)YhYb2lVF$|_`5MY5z#NU?|H*~V_h1$Tl#V`FzuOkXD zlVE?}Bzjbm&a<$DMA3WP(|T#IVTrC{*MdT+NAPX`so=l3(?GK&(2G2&5HYD|I)gE7 z$!FzAXlKEaz(2SE z(TDiP{9ECFjM8w*pFThOU5pdVn)h`6_I5t#`N0Va%)pP(?fjw%bf8&C13J;IK7vF; zy9nr>mYqiyV%Nz(s}G<#vgXRw3 z8`*JD%a~W5z)Lq>?ktZx$ocQTe%wm~cb{Snh+MmhlZ&{eW+|`)rcFiSr&)$nw!omquv&*aXXk_udwX_R^ zyUPt`8CmYjOSq*r%CY!I%pY{%ItM%LivO!hl7i1h;)l~)F&Pb3;(!~yt8p#J8fHg) zy0=biJ8)snSo8Dnby((dx&@Wk%8LK4Qs=$*sNG1YZ{bDvVi5v*<)T1=W@HzT+?_lI zL_Ia+t1b@jXaLGs;1A&Q(o17oIB>+En08Itz%exrwvRACZ^>O31!_>8vCW@WaOI;wudJ9~Gs+zicJBxOJF_8fTLEp0 zC3YQ4bBm(i;myW`{pZIGJ+D7iFxcy~zjJNra#9To>(#ot`9?pP>+@xikyX87d>vKl zuTqm3&fk75yP+t4v|!%C;@K&uDE`=SLH5ns%eDVT9Ww9C|CXBAj44{go_r#aTD4g} zPMz!if zd6yqll9o%nfU?`81Z&d{czE(>9OE1l4o^{q!i}Fz<|&0#h-Z%fc}P^Ge0>`m_p*i6 zvN$0)I=P8t(pu;Uh3I@OrE$@(47mjaB=i)0^zF5-id6{!hrgdKobm`c<{srboKMfJ zRn3LcqSM^v0 z+rJMk0O~5fn=Vuhi-n^zR@mS!ZD}{8c=&W&{xj_A>`~Q|3zI!>Ju~Y zo4Mo`wP+XU%*(E#FBnZWg=}`RXa&nK=A1&A$2?tkO?%&h{|YdjfnKLX>z;w#JnX7P z$I@R8k92({p+mr>nk~qzc*HQUTu+}da#=Z$k7A5zj~uP@SOuCCaB1Za&nYF-G)#Qn zm7eSA;u;tR41xD*ddpcDFzz>C-0rHwN>bN$-V-_*>Isf5v)kKqJ&Ub2YMy!8z4l z&J4(P7Gz(c(n-+>{y08n(I+X2)Fr{j$qmWuE?O!Ur{howcD|}**{GUM|E2W$(|n!Q_whCT zcW>_&|MCZRo%cGpiF8AHu3(O=!lD|o$=fr*i%DhW)S(`~?VfhN=Qa)ucy|&yx0m{} zn+Ky`Q^IC8xv8mF9w$PO`W$_Hg;r4A*_QzbrZ5(61KH-f{zOk5kzL+JzA%$tLA5G# z%d32zTmEako;KdP(CpV7UV=}vR`Vs${3n*u`YWt;Ebr^PEosyX z=7_I;yV7E`)*6g|KR_gZ>3zA_`6Y@U$?zZeYZ9?RN!sG^fn0RV#POQX*z>G zlw<$BYl4s<@ru{dB&#Q#s4P_do5q&%=t#znb+FkDh`)aQ!JF%8BGu1?-JwLY8kWj@W*$HX@H+>&M%;*%vA}qm)I@ zjR$a-+!(OdbW7pG!(+AABRoJW;k!4Q?GSmGk1-7DVGqagnjZj6$WYsR{Epdu1ma?&kG%yp{Ot}w7abJXAg7O@_v&F0vnLEfW! zW&Nj!`6lG2p7`XTp1_`lwKMgCAigEnz!5dFWWRT+95x@s`;7jpCgZj=^TF(m3*^#4 zE{>Hs^Qwa@F}U&lYrW}621f0L68{lsN$&#*ezD4lK>4v^<20l&g1h%%Mv&8n>?xWR`LiI9~GD2fo^>q#VrO$NXK;2?< zht2AfPi#Oll!w@9o=l|P7vSas-aKkO?R?|{mJ4J=U!85KC4Tpfys5pX}Rw`j!&z>Dm4E-0l04d zN)iv7HradG9@Kda+(R>F=D8%|sajZf&7{p4&e(;P#)^V8_h|&xy&Yq(KWu2rR-B&U z3s+`T?Jw_CS>g4T)ohW3^+Nhk7oI~8zF%BggcUx-Sov_OF36gCmix^1apX))exIkD zSeIox2p5|7NLnH*``3iz>O4Z81QqL*R9#6K;KA650-TA~el@(?h|oRZW*`Z_bZ`>v zgyYeHZt4oY@=AE>sFVEp#1U0gYw!ATMRwSeY`52{qU%jhG%z5kTOf@m~l8=`cWRgD(Csr6KWashD_;24g3kZ#b*9BH>c8c z+%~)yV2nkNf%4;l4f!fIuNEsQW2Z*2-6$VsRWRXP#Kzi7X4lg!yh%xS?mFAX5S50O zn3ObR9DaAn$yMW~bOQE=iBo|_mlRtyjQ}_^0k8h}VD?FuNu~GLW{F2x=uDl_0GH{2 zkE>vs#x2nh2U%%Xj<{2^HN*(aP#Q=4T-B^v_qI}YZp!6jDbTfXSLJm~?<&*HJHvV1 z3r7NG{CWp7x?mdrhRn=jO_zk`DG-;h}_`S%8`cg3>;gp`1^&K z4pgpgl#M>|uK5|4EgLwHIlxgr9Rud^fTZ59Bms{r*aYfCRk|C;DIiGJv+1xw_pr6A zGa*qD+nhi*l3V(QDd|ZV7G{pX_gD!wkYGI+T@0?|LG=H z%f1Hfb0~dSD%1zaVXH-vUuBKbUI0pP<106U>O+orH!4&cP}ibp01LZMWoB?{3H?(v z^Ms8Fj_RU&{z2`489NPC9DG z3~<6S-=~DTcaw;P%`KuEFem*GWZn~L2&1Si5m@(Mb8Yq|Rds>KF9FYBo^o+1N}#Vw zI^J#LyCTlhvsXeU6V=t$(`SPO9QZHkEF?;J zrPpZgP$OGh+t*G56ZC%o4MFn0&m|E(l^2n__aNe?M4yVhEkfeLn_CYD_ZlSidr*t~ zj?fLv`l`iPsl*`L%=Hq;AivO#KeE*_!xACCKC}3 zx|fPFE)4yu4kt@E4*lYkIhQ(W0Q4(&Q}-8i$nOX_T)#>N`AsH+{Jys9h;G`d2#0%K z$z`}63#t8QMabdQXEc0(o@{jl%FX`xCj@fsdrK$P52>B zSX?~vJF+s!Z!#I=cLWgKv{mt9?=fXS0JP$5dKivgz%=Vx^l+ATNCVkh=zbCub6(_H z5^qwcXf>615v5P{Qw48Fn~GV(bL`OS{7!xx-7mtQISB_84*6Q*%NI~zRBzGS48b#Q z67krHoUO}32B*orr=?VJcZ194>s!9M{Y#_Fjklj;>4%H!LwLU(D1pdmZ`Z z#x@dy~{>d2Kn7#F(!-zm{c><|A7&~y% z+$H`s1Bh;UoQy4u{7(4dZa^Fy>NkzUp-0sO2x4I?o%3*Lb+Oi4Dy@qBb!Ynn_VhdTui62j=hg~WE-*R!Pb%Ar>T7Fad4B7poq+$!3yWOa0L7Ad+fk-ue|cAo2fSHqJ_$p%iDCg%j2uK2;x^g z^gE&4hyyQz6H2%+;xTcUHK2$~6no!vYRvkzQe&PLTI;#WX0x07Vx>HP?4#o|t$cam zy}1D%*&1%aNvNv9MYdyXc1`ohmQ>ZzM}9{Z(S_1Pe#zlzjEZGcwMF$5>``?Ysa)LH zo`3F@S6>b6H8otfSs;gdZm=+DU!kEY46grz($&QL29Ly{h@-Fub)1SKPJM}cMhnDM zvelr&F;{#O+L&jo(JizV%S-PSPp{>yx;4=BAgo->m*X0T+sgESN;Wb!iwz(<$2F3L z^XMbGJK%5`M>re}7{piqh(TIhe?qEiay?_S`yZ;R^`65$kw#!D<_+Pz1aI|eC)mDqCw;;rfzJ>Yq)}CZeS)r zbh%in8X4r*vLvD_bSW8IxQ0Wr7CKZksPs`hj*QKFWUJeKB7Y02=GSmJ?r{54Ib^OM zrd|=O;Q)-Nc{H~QU8gRmdJabxhnKkEVnZ_I*RmVcqO}b~w^*#Hk8EMba&i%WQu~{e zD_G_RW;~*smi)ihh#fnL+#&w|K+D2FMR*)g+D^bZ}P%G^2scnw@`5PJAn>}6F67oA1QB`|DlzKmbRfe!$ zdhEafVQfW3w(oG!Mo0dpHmbF{a5%?vxW`h+g@)wJ1Jedr9FAamw>`0&>YXwN68E4i z;_j9fM@5|QxS++wAitITponfn$%Y@`Y09r_YB>4;`Mo4g`DmvH=^NVx7#sBMA4!m3 zgXj)bRnOrbmHjE2RQbr3DG3$%S1IUj;+sawfB`?&L=9H}5H|=DN34i;`#v0+>Y1FN zVU*QWvABixd%uQL`c(fZE|}QBWA9^;-{gociZ=Y*XHki2IQ&w74OhE_kY5d=o6_dU zZ@>N|O13v+^zEOx$QMR_4WheCa(T~H%sz$@>b^&N#+ENhdHyE8q)@-}JuHw}+A6G!R|=Q2S39+ZOa|2>CUL?(Qq+H1oFY85<;wjlM(Nh$m3g3$fHY4|u?{i07M~ zkjx=w6PwLR6L#6n9=fhwrAXZhLTRC;h)~v2XhpmbycI%n5y-Vyxiky1?9KiQe&4+F z<(Y3XUpl+Vq{%u@CewOCMg936p7(u6Ut|lew#V`tS#)om41*<}3}(97D`K131GkKn zrLtFF!m&3AX{s>9&AvJ?;nao^uOsg6ZV-3G7J3cGT&;!&yFnZcHc+#H6OJ#6!#?wO zZOd<^zvUMyT1x$h9Ch>b^^Lj0;;+}PZZ0jK`}Um6(1vH9F&ur9nZt7SJ{*fKifkNg zjnaT>X+ma#X?`dc-Kfa6U$Akm*)sbJh?6kaw-5)wVX6UHs<4a1UkZhAav10JiyU!8 zfOC!tB28t65kWW{hej#xS?V=#!dd&+@*A^A*=p8RY^&xu)~;BISu1hr>J{tP)s5<6 z;hX$-)wT7-&2P~lRRip1WJ{NeMvDD&p7gW#PLZt`R*|iRTyzMkdn~_^MR!Ev0hf(6 z$mAdPsDws?NpzFbRN|WgaZ)c8KT?)zal*wv9vERzh-2yx5rJjHPF=ZA5Q#(vbzRA*%X@r{Bw8*;dP?-b%(qg(PKOeSZaIVSL5 zNYmN-V3yyM#nq;N{hhS*nswsqOD8YQt(>`9SUR(G@~l<;#jL@DqCikMaS1t9~`;6w5 zX>sxTPP0>L6XCvPL@S(L+W6|Hle24uuP<&StR>6(>D>1ltTu=0+s$G)0vkEMCjIPv zc-;eygRO(^!SK{Q&}kD8w<*W$1^2xZ_la6X)+?7{PohN1Sc>%?7pn>K0SA)~MHK z>!lVEZuvxht-5jH;$nU!d!w*?^}B*4>1NGR{{MRv*#b%*$7X6+7G20}0zv~y5VBFH z9~s_SEiX2=J(l0dqC1>ilm}ye7wizj`O%GPags9h%FtUj{kl7;D8(rx+kFwkQI=YL z2{GId8xDXR-w|*pf;g#}$}ZCM?!lxOPB2cg#5vPjq7wJY{?6Vfxf!HZpQ$!Wf^a`C zFQ2LY@tb9XZR4x!r>btcDVn9|9xxJ2_)%zuMK=t!N<-6jgHdhU4#sPxX0z1j)Xm+N z(()Tiu>X~(8Q9`c_h68&sKzy|C_D(xfpik#R52VJzoZoS#(!>PFn6y`RRgxs5cu|f$OMi}DAG4ceeqKec-?igsZb(G+8-yr?j)vPSqQGwbqWG zIW7lp?g*~0AI{xj zm`}uKPJH?KXPXz&n!KOGW#r3oNS}MzL zmQ8KXSdel=m5OWt+i(=z@;eb0-F>KeP{Or%!HsQv+io`Y+S@Ezt95use7C^~*J`t; zw%2Iva`@Hi^=9kezaxw80AQPzrHRL;9DUhafjBNrWfxI*8pK2@a#ex0{$pD_WSgDI ze=+yDtkU{$hI#&8k)_IvgCmY7snXFc8c%&tc8pLAhh_ujQ=g81mG~cMV1jD?lGp9l$KwGAx|K3s9+FqT7Xe=M?Y`21z3X3VGV=$^LE9`LZSQ32?G8^wJFWU&X}8m42A>WiTch5n?{=(gtMzw2vgjU2 z2BV1zY--FoU_-aTBX=)V;rLZ&slxH=h8%q;gv+H_GFqKEaiYK^@aDs}58r+5Jyp*J-Y?p?`sK_dgCvXYh^5C4U6Cya;c7je zT=o~gv*(0yO`CwM$hOmJq-u>l{wucYJTcuZwL6Vwtsd(%tBGti>K+^%Z2R1F%&CT4 zRZtEYE2IlaNoruo$%vy@iW?WLEF)X~1j&zYz#H?0+zu9nz&3hGZaDNBOR42$y4EgGKk&NiUD}sj85)>m^*1{av(6dz8IW z=2_xJw(Z((jXe#s)M?jQWS4BU*icnu(H*EQdT=36wb6z^PMNA|{G;kK=(>8Wf#??F z)4?nmoyn8@_~ykMd?6tTb~qqTN>qLJi_juWCb)jZ0KV)P@mfZ3wBemVmS0FW#{&fA z!NA6gY!d^vt}+d8lXQTrr%& z5~pdZI-J@qLWnbBX4M|{}Vv#wz4G7`rSi_V()^NmQiX*^D z1NdFUJ;4yi2}e=f8+|Rm~;h4RAQXu8Nm^JX8Cw*2E!C1>wA+lG)KAy7TtUlko{C*w`DSkQ$3HS!C^F1R zu;>n3ZI_d`Vb2~!MYdZ~WZSn>#D3xJ!?*r(_qv`k%4sUHDYyo*fo7pyWx|CQp zH7#`|A6wX1&#v4!bL!H?i>uky!nNx+Qd%f7GCrrOa-3|35Ns~XuMM_;B8%?0gN>y5 zp^ntcI5D&(58^Ff^A0PngVS~-y7c-;^2&sLkViQgu5Mlg8;amJk>e5)w9i9 z+M`6rQ#sd$~+`Pq9aem?Js*ze;IbA(}_O$VpwRmOakM$q&vnS7e zeLl5({%0*5WsE~tWK)Y4eDF3|WYHa`B3n?(0dDB0`T`DUGc&~s^XU43O&ziUZD1Sd z2E4rq+nZn;0Vq@fsL&m;`JH2u$aVf5aBfWg+ifle&V9TYW1s|bV zbiQI2v{r3Bcr>QuBp*nl>$4Rn)yA?R*mds~QN90gEi%E@6I3?s0N9ChCnl=mo+ z^OxtTT(T?fA)0M?Iq2DYNKaZZO;0;PTxF4g@M>Y>`h~eu=T`Cy*~=%-l`SoIaqFiQ zEj*MozXtx(DNRa1t?`ZxCl16}~ljrccOrLXr|}AREDrgnc1CD085L z9d4nBKuf4G2C%m@~${xi+l7*+wvRtCtOa|F|`=f1mUz_ zR)78D=btK@Kdu+7`C_*`XKa0MYT>D@9Xjb5`|d$>s%kQdY{BQYFn{l9qnfE)8AY~$ z?E`>KO*XI%1NGb&LNK$T=1Eh@G7^%2&R)K;$1Shx7nL~gqBtzWJWhG4U5i60PSoPw zunAZ6wfw3mGH)eQF^)M~sk2*bxK|so#@VblSdlHV=uEL-u&dF=bk65Nrrj_HLo zv;11Bv-e{w!_X!{RAiI72T_sjaD&Y>*dTn^MK*v9dh;zCY)DBZ5n#|{5Ol*Yg*npz zzA3U)l_!owqg!N(1L0Vj${qx94&!h^mF^3^+Q;%c#+jfQ)5chORAif2i|&DQ(HL^k zAj#DSmY;xG{BAcM(DwB2?ugSHpOxg_El^ck!H7K&j_Zsx|R_> zzJ(!tD`l|Gi=agLM5$u1IEbPt_H z_a@R)Fg&eet7$mJDD8fmOU&T#9qMT0@hx5?HH#vqQ z!tq3vHYs^ds6O#}AItAp#!2HtxX7aW|0}Y^{EBRO{vKxVvDB>)nNkvox#_N0m5Y9E~|zGgOKr#1Z7wmfu8toCgQ? z=T?ao)W%oEz=Vq|x?`ckS!9z`7g){FBe%x*bqL6oxPt#%Wn!+mdK(zHkwGV8NS_vaNDA==)U;Agf&4o_7d{)?RElO ze}O5;N3pLw^w>?!IrWM-#RUGHxGV@q9Qvup?&l#KJR^*6&(QRiNK{|743*_~HlvNL z%vfa6z2zzv51mDK=D?eapkPI|JAHMRhi?2M3khl)biI0+WM)rb*)!pumdx)=pHa+ zE9NTYayiX4pX7v_o)GDVuSGXM;O3&>7Tx#nc62BIFv*Ogz)gztk!X^W1e~J}KR8;irQ1Y?FO_5)OecXs6$cZ$Si;TZr)5z<6&fe$tg$-TH%>8Em zaqW-I%Z0VM(}kt)^>4M%ls=e6w;Wk?4+OSKnX#?Hm8%ft=989Ho*r-MW^ojA@M??h z`%{jcs|z?tYAT4Yn{htf1$x_GwSYK~MR2dnwoy)P-7a5gAk=|x{^y|qIEQfrIHr3z z;ea?^ieo|y$Me(&_=k#dZ^X^neV@HAO5n(*E?&{UU%U48*~_aZ3ya$M3s-;8wl3T_ zr-ewkoF1L33QqtV7v~yR(GmM;rOXi*n;uJX@8bL|or7m(BmRJFcI2kYO}RifL~dXk zlb`{rS4s&>h{td1y*x9bG8~u+#;GkMPHGOop_|Hegd{PJ(o`ng!GGof8*p|@l`OxN zn^}JKaQumltM#{XyTndaeA8R$aQZuARAY<%YH# zp5dfuMiyOY=(%Xc12*cElKAgLg*an+oOB?IE@?<$W3?hyFuEILn?h=;D!j2r%1`~< z3vl@ci{8|?U~stqQ%I#(supG(w{RemE&jrFReEAR-(EaWtPwfa@aeUKYmjyA2PD zW&=-D9X%BAp)fsIm8kj)J?K{nkMM_eXluo#XJ&YIsZi^j0XHn;DP4Hn%aLQ_oa zg@T(4b85Lgy|9L`$J=0l`hyNlRHYlue&~f{b~M5U0RJ@A3poDxB&xifDoSw(;>77K zdHM9I^Kfa-Q?=p(WY{Qp+0P994c<=;WA;lUc32>Odr+vRidb$lfiUZ-8x`=yCb@sjp zL!8M7N0{SEEn5GvcP_uO4N({mm%c4+tf&}CkP3GM1+_L zVgp@`q!(vYM+5|N{NKr!-?(U`UGko^u@R~CRpQ#qI?bV{|ifl8d5HtkB zDT0{JrpipkwwA<{X{5&$v!9#G__9ajbK+dv$Idyr+n zVX#5%RQG-;io-M@E;)Z+x$}01<(E!_(^=%5PgdXg>EADY|NV#WJL9&~7TqJLy?amq z8))<5%X*vPGXd^ukPwa{8y4MToT_@&a}Bo1iWMe@6i7^qahsm?)N~^|t*a%1gBgq} zM3&KENX&z7`gxET4weGr4z~y@^cqqeH_4CSvr6evU5SQqUN3Z5elORiq(0Ahu}L0piw#l6vJ`Q2S7i|&x3d*I%Hja;-8u~B4$YXTc&^IAGZD_k`D?$zx( zKc~oc$hIg&Hb3o{*ycV{Wu&RtCdesv6WXR5!CP0))K=Z@*XJ6%>DeGU2e^4L*%Cly zu8{+aK0^s^d77H)k|s~}AN{LvkC_N3#A$lVDa%t;i__WrqUHBcJ20{6-aT~>_Gi%r zY(W-m^T;=pg=~;bQh(j^EdcIpXp3$;*|q|k=HOMa*07_0>Prd}1UO-B3e)(;5WSu? zzJWKSSf!kuGOg(!-6QrnvVfX?Gp9;TJN3|rOD2`lRp|QVQ~;Muaat2MWwIe{{B<8s zIR8n;-{=~7HCTSHDwf~6PFr-3C_7bE0^2eN8{wLNw1jMu1!?5lyE~<*TJS-UZGVQ3 z585ocx5Eye4Y*A`-6G;*u;3>YJlp^rg9-qyL#mi(!t_=HT!`ZQt0jN~YE6G&%4^DircyZx2%v=YKJmmpDJ_tl>M<{ zxDLMEpFYK9zh%a$o9cscig6FGN^$<<2p19eT9@Vbv_K?R%?cIyd7mKB~ls8#)Tej%#zHIWnnnm|^kixc!poaNF z(Oa~Cz&TOPKf#-@rguBo_(}X`B9kKC^aY-EZzqo`f{WQL2#h*lN8rt@88U_%pjuq9+rpoZ_L$(K4)62+0dRJvDZZNLOMqd=L6{>9d^lpnfWnfE1HV?M* z`A!zyPljF(PLWMun+24GkEfcx2y6Z|)$jGQR?14{0|}i30j;R(<2B1SiLmXuQ!*B3 z`qF6awd3o+g3xY#m&XXIZ+4J^&If=)<=I zQ}gir(uJuk!^)z_niVUj7Z~YNRKXNTq^#X09=qjuWF7tPwAKhsx)2T(q0XR%? zbQ>tfIlZd0_eIO^N2fXXz3Xhbem40tM>r2S&$p0~!ZtfuRm(+p=pMA^qRYi)(V~k6 z=^kLwy{+kk89>uJNoBRL^CJvh#e_6{iJgQR+2n6)P}&T#z68l^uk_zYO*<*qM}Um8 z2h3@ap7Jd`MuEEfOcR8Kzimt6@cpvVT%y?k;~uh^>Z37^d-~#Y&fe#i-;+gm*n9Bj zKJxW|TPzIU=A@%Nz}B6Ms>rt7XOYck` zWOukzY#bm9wUC_>uQ5;BEH=B@lg*O3tWXnErs36JtL3~|C4z#kdkIQo_UMI$Iz zxD8z{#c4g?l!mH};;e#QCr|jwzg@!7aLM(QTfC-+kF67TuS;MK}1;1v54nt+x@VxWKkaI7sSu zwdIZvy96Yec%fU1QWGzc3?-iyG23;5X+=)RMN1m4SHgC}#%J9)X z2R{q>sJKQmRBZE0C!@n79eu& zPPQIkJ3B)60E_OE0c^@Cl1mS?*>sCvgtJ>~jDvDg)=}klL+Cg>7S7(EQqcj5 zY;C%`vc$;~XTVmC*c{jfD6$RJJ$QV*Eeg;A-Eit^;$WsFZLyaSUUoa%G`}css%*X5 z2deQf{#eXM3E=9oK2|4;IUPT66~j8x~zj*{DFK zsZsaR(;L}5&Wwpov{N5ck3yv_{@31LI6hQ!OTQ&n-BPTkS2x&**MM4V3gUo3)oZ8K zZv;Jha(0xYG~sSkwYMnvTsY=h=rkC{DT*eFqg6x@H^uvF5%*9+T(tb2)NRonP>Z+1 z?ABF4nWNf37Tr(we)ynf(bc%9f2hkPy5Z3Ub50@~>xTFs<&vhCxF6aqaeov|u*@oBrA~4QX)tmqu7TsM=Redt}+2q&b9DH)J#kr{oqS{7J zG~kWZlhMUV57a;nw86A)ph=hw_)%nfWCL6}g>Mk|aAaJx{GMbix(C+z-FF=1g0`50zr3Qzh7PrC zGR_{*HOP3z_qf>0q@scuP%9|?7Y*6k6q&{^%*m@nG$OTYPnfb1gl|f%`nM9Wh(WqM zZ>5=P@6K3G@FyB$o{Hm_zixgY69F5Q826y{#i}3#E+! z*3d2T(Qd&u|K?1J*v7)~zJgzN7f2G*3dD0vaTces;Khhbap`X|3o)U(?KIf)(D8O^ zk#SNwg`du-PG%Ny&)=E<-z>kW$aaH-Mfc#{11!3;l#MbDPxeWuCOL>Xm<6bbX!SHS zk}c&PEgb}sPj>A3XX02&LiH89hx^i?mCr*^i`t^>hR7qCkf-!cC4T*nr?Jqn*B6bG zaHP`V0o^)J(;y8u6bMJJ!5HHhQcaM1%ALLce^`ERB-m;eT{NskcNW3se@K2|3VCRl(mWtv&C;0SzzN-)!~;-V$s#HDtobt zAp|r{pQsSEp~+_BfUo`dCW;ZO8H+AM2>FWL&I`=>EP~hu=XM7iDnvGRlwRLB^-SzG z<~C_l5w5Vc4uT_0aflrw^*GMRMsbh3v-kI;<#z<#gO!eM(Y<~SK7cLw*L0^!QQHKd ztwOi_CZF7FK3WUviq^nc(;2HQIT6^(x}P8GHJlECHA-%;jb;J14?a6 zZ9ku|&6q-U>n}P?oh1{Q$Wv)K%+|xWCob;Z19p)c zS!CPK7Tx}BQBGBO^VGE+L#&1EYKC^TyFfu%8edG z>fhtQgmKTkb9RFXcecD~V8fz2!0bWe7G0&HN1B4B%-9D@U2N3O;PM^JF4XFGRNne_HV2=T@%_yxbkgFQFx>hw;|YuIG<4 zmn@mLR|=Njj}|veH!l~hT6C*m+jsXsE)SV> zwpn*Bh%*CL(L2yn3JEyOxmBQ67lWiFuw_@V+9m+* z`bg6v*7Ry+nf(>7D9C1}MSIJxGy-fH)P~VjcG{`OMWAi->#v&iU9azh^RhK(={!`S)09=jY(NExI2LzC{-w z8+3zd>xcO4T$Ng)dPKKJPB;ePdnTkDRpwD5o8T57*Lrnxhk=4Nb5*{l;F3u|O-I18 z&nbtTKIwVAYW>#H+Iiarq%r<+4zW~+Qzr#0J`Z3UEz*h>T?E($EV5~fu4-)ZvrQ7;VjCgJ3{+{%D&?Xwi8D}TsHPXU_P+ZTsS z!@)^47#T;#XZdy43Ov|?-V7hTQ~ajqnBq6K=;k^|_kd-SaiWTIE`V(Y%VIMWJ6jmA zn%bK#=F6=qE5cEan5~eA+Wshwg9fLrNiA8at8e>p>k@ZaD;tqq>J0EpJrtIPe^GyDI^zv?W%qu7CYwp7T5-t=TkNhv?!+mSE_ z*e(}Ei|+7FHn!->@v%~NKv^e7-IK6QaeN5W!lR^VYKrz^oC?@ z6Y*l2C7Chq?$k}dyJ6;xHZm<^7s+Eex`o@8lu4>U8y2yq_m&WKM#%n9WS+_^zo20vI zo@o&d-MRX8MbGqTYMQwVwl-&#mWxx=SxF5%^s-``7QYg<=@vR)|5*%Y>Ar8R|a&ABK` zbQ!GT=D`4=n;>OormFK%RMiBztlCCwX9{;G$k`AMy9mSh6(f!+t2yKNZ?3r9e44^I z9XWD(=A-uqHpEe6OPWKtYhpP6qWq>;n}(l*A0idCr>YXvCUNj!eY;2O7EN*HYBRWo zsiHv$uJMIA_^rGi)}o6`H(jkt5gU}d_TEA1yh{*eHK=y~UdIkV9AN5ZQUaP_NMcKN zn_xH2>wZVo`Vs+Cs;nW-slgP7|CdA+Yuqb?oxP{Zh634Yh@*K*9gaW_w86Ul<4$0E zx(jTd3_1tzxu$pxqwXy6j2U9iI78f9Y>3+<*wEa@aQ>lZNj8R)OuS|wH{aQ!J1E#t&7vzyu0Dxq zdQ0BCsAj0XDOy7uInG+s0nA25rLX;tz(tziN&gye~? zYvrP7i;PmvslXd5d zShTNIt4g7#;+$xGZ6C(j8O_=poKK$+Y8VYRBO7ol@lC7fW(%s@aM~N=095!)1Ka84 zoj3R(MOSt3)<<_&QWd<=CQmed$x>rRmCUv6j1N}XY4K~C-h*jS&1J%1Xg)bz z*lt+^4ut{J z){8N2>P99TYGIuCmKHs?$5Jue*y?d)9sIM!noXjSb?`Tl0+TNl&Ad>nvRkLEO|7C= zj80VP<~+8^M?QqlK*nVYv#Xjj*(j#cR;!S=h>C><{=CgxQh>8Pavl?ItofG3SG$wi zAlK>>!j;HY%Zg*lpj57Ju(HFby`zg0+|)s^k)r!- zeuIx;ve8AC?jG_&t+6CERhGpjN7QEVX?up?xe_pV;E0p zT0dPxX_TW3r1Sh@&xRxKDeBJXBvH|T^e}i7cT=m!1aHHe-EuQdhMc*n$LB|irWU~_ zXEaA!{IXqGgu1#72ik(t4izDX4X4$8-I}6%Hk_hc0Nc_jy1V+Ad>7qR8mtO<%Bv8* zXC?-HPb;OeTeH)ThTsN5=(U6tdMNvZZBMVw?eQNZ4>b>9>3kNA%$B z9I}z3Yl`6m!&+D)&M@GFH=w3j2MKHPMD3I5dR#J75VW*WO=6~~hg$sPC>z$QbfbK9 z@~i_k10BKZPN97hh`FTs%t;EHdp!N%zRCRr$_J1Qhr@TUPh{SkdcGFO$MiG$ki`AaSS7UJ-B()!Jdr)MW;7x9=aku6*aaJNAYSM^rD0XaR$ zSFK7c^9A_+9PZci%X+g(X;LOzd-OnlsHNyCI=Z&tsI5_iMq|^W)Q>H#L_)>3L^XRc zK7q{@z;i;uD0psEjHca)8hBFHY)eJFZnMkZxv{eabTzigHL#Jh<|-WS>G*^DDI(lH z^^l)rRpF!}E1*eut7QLvyZ`u`pMLuJ=kselO^etTcS8bgX&H*8ifFi}VIlFi3^+Cl zF<{4?A6~sWK5DOnhinUts9x6{x=~G)ZffsgW}7aKL`6&3O*^4@n)MA6Has^Uu4jR;}O0UWoK_vu-JY*yey{Z z-r1-sJW-)-EvQMf(G5}=e54y?RhoBJme>Z?+Tms%DNmkm32;0z`=GhShimkaU5)UX zG)WomF^jKZ=%|~@$&n94+e$X=CvkTy;zU~f51Lh7h4L-ojZ)Qvy$=G|e){?4CUPxZ z5pjCRAQ$M?L^#`sbKyQ5Oa*La%MI(-7#4jpruJaL6y2{rR~@`1=%z(5^V+q$QH`om zTIkvy;fane8kJQpy78(w8=jnx5d#pQoKybTGUH&7cj61TBJqqu<1*I3p$G>4vOi|M zj`S`?B{1f*J=y_k32mk9xSRT|y=z}I5^bYPk^vV#r#EmD*xEu3MmPM`+vpaTmTZxy zrf5jH?;1x0D4TdNi&d@v?Qm!ma1a06##=UR+( zLWGo%sMbu4m~#OF+J5Il;p@=D&(uSs$a5>Tp?vV;UjEa<6ZMfFZ_w4{+r>q&eH_5H zNfOeui5HSR=UvrEy7ovaHXN!_PHPQp7gr}wj^55Zc%lb3IXOZMoQZALb?TiV277sts>@|F*jm zmRkDwpL`sR4p*q^#N=Q<9OLmA*C(Z<}*DsNjuYL%g?}a5{;*J(e0iEH?^s z4=zf>Mep#CO~Vz{8{G0ev?PJDwb4vvRvfeuJD?t(tPggWY*mm|v^Ae<>!D()@g}bh z4e~)z9%fC1_Xk`UVZcUj@fWl6Hs0CGiM0l$znJ9T?xWtd+@K%(I0V+D=)q4vUu~RD z*`11XD1gm2QDG&(U^yeV8F9j!==QYDhNE)p{4#*8p#gmpg6#HSXyxs5u?^nmqtb^jEgtRU9+to1_9^823iWU_q4O&Rqb{gBy*8aG&`>i`zY*l zroa}AI@?QnE_Ur`uYIh<@p@R{Z3&Y3p%{8}MXO%X7X@sCI@JfAYtaWIE|E?5MpxAk z7d}-Rj@$X+%Md*{8lJ(oJlIIlwS)?}N@r`LTUgCp z(~Zc1l^rNSR}TSS&KoT%!Hl(OP_iH90?b6ZJcLkyTT4ea8VJI-d3m{IHB8RqyJWh- zF;_Dh)l=0Tq^PCCaJznekihoI&#}BJUYmG{axD_#FystzT9K@{_-V7>bT9jr?H#QGml#|!Frh{%JD(c2nO;gS@W+#hbdOa?B0Mz2j4b5z( z>uV3B3eWO)xF=Y0{^)ttE8-uja8ARQ?smxLdblOJp~Di;oJbWZd8s(6)NHlNw-!ma zkni#GghRHwuRWl_$8Hc%%Thh!P#2TDQ8g_J8*)8tqlGDaVP~7MqPWVVjoG4F(woL~ z4_dJ02W|6D*0O^WH=}CQI2Xeho{7uQB~hf;8ayB>XR-+?fnSCZ8le-sA_yB+kCK*qMMW&d@z7d zJBbb-yH-_|(3Z;JN%z13Wn+U}n$bp7RCiG38|#qZiMgyiT}HBpP=EUGwRiEI2Qm)3g!Xv<9`* z-fyahZ0Jp<=}qLBbLD#Q2hUF+!{I-`jY#iN2?IGhL-%C5;1rl&ADPG}X|;loQVS7?+1D->CGl2zPuU8&1ZXR_(Of zVO(3bh&if(Y@74kl=F})hEh)h*pA;;9ee`Y?k>k`uuO2=AVqDa>%`cp-fir(xW->tOKJnz470&fjxQ8eA z`@Mhk;zcCftFyD0*l-(m8$A(WI2=ZtE?;*K-r`k~>QgrtXRk=no!o44Mv5+7id;7~ z1kl7dJ*3jA%tnikgblK?s%hm^)WRoWufaBV)Ycdg)nZlf3+1vwB)FDmDV1zDUp!@; z5f*x;bXuOEz3^Q_s$>E-tqMsZdUc(|t(f`febcL^L%n)ZT}o$*+z_ z*TK&R8!5Uefz2uow0Wc{RXN*C@;q(O9+jeq*r+uvB1>1>=0x4fDVrjqBdRMoczFx_2Ru7aOhwJ=;Xy%x;mi)M#w+ZNlScOSKvR0(c)p4zd}f-NzP zf&=Xv@V3~nJzjX#XL<61WK0MqWt|EIM}0b$y2k255$>3Dk&Ugo`&h$a9d270zhWHC zFICX6IgI%pZJBJ+(kZ%Y1ll6c!eV_TU3zXBCmP8{Kve4%+Dvo^7vkF8QLvUt@1ff* zX&+47(+9)N%{-Xlgbr)aMgm#*h`tjN=A4nPR~~dt(kG;gWL~P=Qjmuo*HYE6$!qTq zF96#jJ=xl$2RdYX1kbc}4}#Y&vdR_a~t+?d;OZIsEo8Miwz&N;EwtMd~8rl4B%^tf*Pwf zGx=tSDgv&BLx79wDP7vbw;t>4n%P|B@r@04c5&hOhT6M|1dENLs)l@zMMq3$QJ8XvdHQ77$anI32B2=30 zJ?PoBoQ!ZW$;aIG01iEtCAB)j3c4Fi<5BA@PopBXeV4_6wP$xz2Oq%pEuC%`m7<{RBjK8}JxI-Uh{IYF!qUb%g^wty;zbM3 zc7}U^^E7zIMv59_^vt6((X7VPyy>&5y5>tMzX5T9aIdayxJ{ubH7%HN7gxv0ViUEX z+1%(sdx|bZsp5@q41S*>$s>pEL9cuWpT6{58U#-$0lGDS4Z$vMoVqm&Y8mX9kLMvr zeG@i}<@vH2rA_Q1{`t~D#S;`%J*F0@$6?{T3pH4cM+ z_S>HcNroqY1?EtZY*QM1=#VWDL-d%g_qov60q}_24s0~N^nn5!2tRHLJWpa-T{9pD z!ccmP2p2F?-cQ)&`^)F4tvcGe7F6B?^XygdMF0333>lZeTty3|=pJkpD>=-tUtI6C zCb1C@%_s2XOyjB2F?Z$P)hxFb;a|3t#MedF50?XCSj2csJBjy}5`JM?@fH`x8?Gn+G;K5OxMk9`WDeTm#k&b+ z8-Vik8M!W8hR$Gt!oDMtl zV=JazB}I2{&-q_`cG#z<7UI!NNTR9}Sv)`_OABDbAwd}7oWh0_0lGd&2l8Wo{PK(MGmFil2djzz zoF3v|K-Gx^>3)wQbjmQuyg2DTDMhxJ8y4?xP#lXG;`}l2&aq zJmM_}S0ui5gjdtAh3rJdn zV!BTeT-JY;c5~Bl+2x~L2#6-}f@6Qe1oq0iv!y619?f@awnHDH_~ zG0k>CMv4jYF03B)#JBjyO!@3J?Uj8;E>+CqF+LKzV|JAK>J!wtTJXc9f)5?81;8v`~h$vfLjR}DA4dRrZAUqIIV3SWN zbrqw$nKg>f+i;z5UH`Iow>xgaKp2Jb{oizdjI2lR6e+HVDpotE2^jN{W~)7&!8ZQU z=>JrIKBXon!3jVJdc2Z~$7`qrD9$qL&T(tdGd(=}Tf6AfTWo+q17}HOy-jYAgGWtY zH7Xhg#Leb@Bd*BO0y0|yB7oZ)+a`UcD7O19b#*V1nw+yLzKifHvrBdZG%w^hgNW$r z&3tQykPgEQOM~6zw=mE{$}d(^n@9pc#zhjr44`UBPB=&70B!oDTJPl^U!`E*qo57& z!#s-Kaw_;OJn#H(Y0Y%X!_b<_{Px78mf7J`{LupvyQ=uvaTcgJiUV6d31svK=&QEh znmuAwkl$DE<53~-I*6||aMl*_tzFDLYr5tRLnfecKS)a|1Il9Q4JFAQ(mg|LSIRMl zy~<%d8E3gm=q5HSno+Ypqse1@V`TbnSuoMj|32)sx6PZJG4(udZ5R>}+DhqdqY<7S z^z8{{QdEK9GXzv>Ad8Ker%G+4xD6P&@rRFrdQd!oD@Rr#=6yeHWUyNL^!V5Ek2s@8 zhv6?G)sc^`HfOIuO_u82#x6?DzKE34Ce-D&@C>j+2{;W(wUAoH+RZbXI?PiYy=6AN zIEDd0w38wyWYTR*3{n_KB-(KPcj2KESACoMLOYCQ_I7}Jfov4wF2Diqg+}Nm=2o$0 z#4w?Cshv5Uu zQd(*dB;83y_s*Qa-1*7%^F(L703t(_&^v?NbqLYP#)-!e*V0Nl1FMGNA6UI8Ep>a_ z)3IyQYw;tV$>s6!m(r6DU?gfItu(!#lZ4eJYV%k|aNxLY7`}pxhY+Ge$Sx8<=c7Ze z)B0moJ#`vzJ7zO039rZu-II@s$s~v26~veYS(cSNtfB8|$Jzgn>$Dc~?*o=qsZ5JZ ze%uT;7>3s%0TawsiK9e&MXi-@ZNiz#eFbHQ*Ami0nKbeP(c1%PXvN?C0|po5`Ap|; zG=dw3H?Z7DyL-}kz33#}CUw1W)zKE20rmin0b9iq9eOGh68jA5SXo=IYlcz{!)wuy zuwQ+hdaLbYFL7qxtjhgp8+q2fuk3`6vp0j)<0LhYUN&fN%}g%C@GkbPG!2LSp+3EH zzC!QjdYhis$@VB^0XFjxhPsibBW$a(rwWvQh#$k!XD%OCFwc=3hS%X)NEcM-0c=T( zOhYKQF&c{+{k3vWnkXenTPj=)rlIIMi>ng)p^AL}p2+uy7tD+Y!|+047?uhtm~s)% z2GXBDT*oJtRhx%9)RBaj9rRqGhj3kq_E4*8$~s3sxtkTM&BV&X@Iw4vCabG0wb$px zi)4{{Vq~kM2~Cz<@KP6;S|8Q4XA?Uq)jIqHmoRJ9bjuxvm$Q_V0b|PrXH7d*RzXgV zDD9-Flw`axPWkbkb zJH^GB(o1&BeO2k}HdF#LE!8mm3f4tJ*7{*E7CEgda*dN!+r?%K=y)27!BRbHQ{dv6 zrfL|51D385H}l<5jO?__&xEatOsBOAFCo>jr$#hnKpj!BA43ZMf!u`$TozfEWh#5#Z0$4)xsSi2aM zf#vS{+;_sdYQVK~VWDx~tay31c6BaeyIbdixMLTB5o%p^E(B0a&Td6pUG^DUxByq0 zn^x)rgw8d8T=nf(wlym2P*`<;ZkLP80<+;?7oLgt9uUem;ID$FOes$GPhASW-70-b zpx*A6ie*-Zce9Jv=+F0$&8N!$!q1VFoLnL+s(Oyi+ls_ zY2WpA5qsS#`^>2`n!W|t*`*QzAFnP)229_Mq5J>r1}ilMAW&p - - - - - - diff --git a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg b/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg index d80426bf7899..1b8d4d67d5ef 100644 --- a/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg +++ b/learning/tour-of-beam/frontend/assets/svg/welcome-progress-0.svg @@ -15,5 +15,5 @@ See the License for the specific language governing permissions and limitations under the License. --> - + diff --git a/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart index e3811d53a7d2..34612cb04806 100644 --- a/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart +++ b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; class ExpansionTileWrapper extends StatelessWidget { final ExpansionTile expansionTile; - const ExpansionTileWrapper({required this.expansionTile}); + const ExpansionTileWrapper(this.expansionTile); @override Widget build(BuildContext context) { diff --git a/learning/tour-of-beam/frontend/lib/components/filler_text.dart b/learning/tour-of-beam/frontend/lib/components/filler_text.dart index a40ed8788424..0bd60b5b987f 100644 --- a/learning/tour-of-beam/frontend/lib/components/filler_text.dart +++ b/learning/tour-of-beam/frontend/lib/components/filler_text.dart @@ -24,6 +24,6 @@ class FillerText extends StatelessWidget { @override Widget build(BuildContext context) { - return Text(''.padRight(width, 'Just a filler text ')); + return Text(''.padRight(width, 'Just a filler text. ')); } } diff --git a/learning/tour-of-beam/frontend/lib/components/footer.dart b/learning/tour-of-beam/frontend/lib/components/footer.dart index 930034dfeffe..fc7af8cd4691 100644 --- a/learning/tour-of-beam/frontend/lib/components/footer.dart +++ b/learning/tour-of-beam/frontend/lib/components/footer.dart @@ -44,7 +44,7 @@ class Footer extends StatelessWidget { ), // TODO(nausharipov): get version Text( - '${'ui.builtWith'.tr()} 2.40.0', + '${'ui.builtWith'.tr()} (TODO: Version)', style: const TextStyle( color: BeamColors.grey3, ), diff --git a/learning/tour-of-beam/frontend/lib/components/scaffold.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart index 921b6f4532ea..fd3291b9dd77 100644 --- a/learning/tour-of-beam/frontend/lib/components/scaffold.dart +++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart @@ -38,9 +38,11 @@ class TobScaffold extends StatelessWidget { appBar: AppBar( title: const Logo(), actions: const [ - SdkDropdown(), - ToggleThemeButton(), - SignInButton(), + _ActionVerticalPadding(child: SdkDropdown()), + SizedBox(width: BeamSizes.size12), + _ActionVerticalPadding(child: ToggleThemeButton()), + SizedBox(width: BeamSizes.size6), + _ActionVerticalPadding(child: SignInButton()), SizedBox(width: BeamSizes.size16), ], ), @@ -53,3 +55,16 @@ class TobScaffold extends StatelessWidget { ); } } + +class _ActionVerticalPadding extends StatelessWidget { + final Widget child; + const _ActionVerticalPadding({required this.child}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: BeamSizes.size10), + child: child, + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart index 28ba218ff650..57746b647da1 100644 --- a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart +++ b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart @@ -37,7 +37,7 @@ class SdkDropdown extends StatelessWidget { child: Text(sdk), ), ) - .toList(), + .toList(growable: false), isDense: true, alignment: Alignment.center, focusColor: Colors.transparent, @@ -53,9 +53,7 @@ class _DropdownWrapper extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.all(BeamSizes.size10), - padding: const EdgeInsets.all(BeamSizes.size2), + return DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).hoverColor, borderRadius: BorderRadius.circular(BeamSizes.size6), diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart index efe8fd39bffc..a74b087d4dbc 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -113,7 +113,7 @@ class _ContentTree extends StatelessWidget { ...[ 'Core Transforms', 'Common Transforms', - ].map((e) => _Module(module: e)).toList(), + ].map((e) => _Module(module: e)).toList(growable: false), const SizedBox(height: BeamSizes.size12), ], ), @@ -131,7 +131,9 @@ class _Module extends StatelessWidget { return Column( children: [ _ModuleTitle(title: module), - ...['Map', 'Combine'].map((e) => _Groups(group: e)).toList(), + ...['Map', 'Combine'] + .map((group) => _Group(group: group)) + .toList(growable: false), const BeamDivider( margin: EdgeInsets.symmetric(vertical: BeamSizes.size10), ), @@ -185,14 +187,14 @@ class _ModuleTitle extends StatelessWidget { } } -class _Groups extends StatelessWidget { +class _Group extends StatelessWidget { final String group; - const _Groups({required this.group}); + const _Group({required this.group}); @override Widget build(BuildContext context) { return ExpansionTileWrapper( - expansionTile: ExpansionTile( + ExpansionTile( tilePadding: EdgeInsets.zero, title: _GroupTitle(title: group), childrenPadding: const EdgeInsets.only( @@ -213,7 +215,7 @@ class _Units extends StatelessWidget { return Column( children: ['ParDo one-to-one', 'ParDo one-to-many'] .map((e) => _Unit(title: e)) - .toList(), + .toList(growable: false), ); } } @@ -325,7 +327,7 @@ class _ContentFooter extends StatelessWidget { Flexible( child: OutlinedButton( style: OutlinedButton.styleFrom( - primary: ThemeColors.of(context).primary, + foregroundColor: ThemeColors.of(context).primary, side: BorderSide(color: ThemeColors.of(context).primary), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart index c8c212502c6a..fdcf63e13591 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart @@ -80,6 +80,8 @@ class _NarrowWelcome extends StatelessWidget { class _SdkSelection extends StatelessWidget { const _SdkSelection(); + static const double _minimalHeight = 900; + @override Widget build(BuildContext context) { return Container( @@ -99,7 +101,7 @@ class _SdkSelection extends StatelessWidget { ? Image.asset(Assets.png.laptopDark.path) : Image.asset(Assets.png.laptopLight.path), ), - const SizedBox(height: 900), + const SizedBox(height: _minimalHeight), Padding( padding: const EdgeInsets.fromLTRB(50, 60, 50, 20), child: Column( @@ -135,7 +137,7 @@ class _TourSummary extends StatelessWidget { isLast: module == _modules.last, ), ) - .toList(), + .toList(growable: false), ), ); } @@ -152,6 +154,8 @@ class _TourSummary extends StatelessWidget { class _IntroText extends StatelessWidget { const _IntroText(); + static const double _dividerMaxWidth = 150; + @override Widget build(BuildContext context) { return Column( @@ -163,9 +167,9 @@ class _IntroText extends StatelessWidget { ).tr(), Container( margin: const EdgeInsets.symmetric(vertical: 32), - height: 2, + height: BeamSizes.size2, color: BeamColors.grey2, - constraints: const BoxConstraints(maxWidth: 150), + constraints: const BoxConstraints(maxWidth: _dividerMaxWidth), ), RichText( text: TextSpan( @@ -214,7 +218,7 @@ class _Buttons extends StatelessWidget { onChanged: _onSdkChanged, ), ) - .toList(), + .toList(growable: false), ), ElevatedButton( onPressed: () { @@ -296,7 +300,7 @@ class _ModuleHeader extends StatelessWidget { padding: const EdgeInsets.all(BeamSizes.size4), child: SvgPicture.asset( Assets.svg.welcomeProgress0, - color: ThemeColors.of(context).progressBackgroundColor, + color: BeamColors.grey4, ), ), const SizedBox(width: BeamSizes.size16), diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index c8d14482c60a..875519b1c369 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -21,7 +21,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.11" + version: "3.3.0" args: dependency: transitive description: @@ -35,7 +35,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -105,14 +105,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.2.1" checked_yaml: dependency: transitive description: @@ -126,7 +119,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -161,7 +154,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" csv: dependency: transitive description: @@ -210,7 +203,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: @@ -385,21 +378,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -427,7 +420,7 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: @@ -635,7 +628,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" split_view: dependency: transitive description: @@ -670,28 +663,28 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" sync_http: dependency: transitive description: name: sync_http url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" time: dependency: transitive description: @@ -719,7 +712,7 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" url_launcher: dependency: "direct main" description: @@ -796,7 +789,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "8.2.2" + version: "9.0.0" watcher: dependency: transitive description: diff --git a/learning/tour-of-beam/frontend/lib/components/test_screen_wrapper.dart b/learning/tour-of-beam/frontend/test/common/test_screen_wrapper.dart similarity index 100% rename from learning/tour-of-beam/frontend/lib/components/test_screen_wrapper.dart rename to learning/tour-of-beam/frontend/test/common/test_screen_wrapper.dart diff --git a/learning/tour-of-beam/frontend/test/overflow_test.dart b/learning/tour-of-beam/frontend/test/overflow_test.dart index 312edb8bd777..0596b63a61c2 100644 --- a/learning/tour-of-beam/frontend/test/overflow_test.dart +++ b/learning/tour-of-beam/frontend/test/overflow_test.dart @@ -18,8 +18,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:tour_of_beam/components/test_screen_wrapper.dart'; import 'package:tour_of_beam/pages/tour/screen.dart'; +import 'common/test_screen_wrapper.dart'; void main() { testWidgets('WelcomeScreen overflow', (tester) async { diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart index b59617a6b8f5..9a7cc82a3be6 100644 --- a/playground/frontend/playground_components/lib/playground_components.dart +++ b/playground/frontend/playground_components/lib/playground_components.dart @@ -16,8 +16,6 @@ * limitations under the License. */ -library playground_components; - export 'src/constants/colors.dart'; export 'src/constants/links.dart'; export 'src/constants/sizes.dart'; diff --git a/playground/frontend/playground_components/lib/src/constants/sizes.dart b/playground/frontend/playground_components/lib/src/constants/sizes.dart index dd4cd994fc9f..402f4351b3fe 100644 --- a/playground/frontend/playground_components/lib/src/constants/sizes.dart +++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart @@ -32,20 +32,16 @@ class BeamSizes { static const double size32 = 32; static const double size36 = 36; static const double size40 = 40; + static const double appBarHeight = 55; static const double splitViewSeparator = BeamSizes.size8; } class BeamBorderRadius { - static const double small = 4; - static const double medium = 6; - static const double large = 8; - static const double xl = 28; + static const double small = BeamSizes.size4; + static const double large = BeamSizes.size8; } class BeamIconSizes { - static const double xs = 8; - static const double small = 16; - static const double medium = 24; - static const double large = 32; + static const double large = BeamSizes.size32; } diff --git a/playground/frontend/playground_components/lib/src/theme/color_provider.dart b/playground/frontend/playground_components/lib/src/theme/color_provider.dart index e3001708ac48..c158229a53db 100644 --- a/playground/frontend/playground_components/lib/src/theme/color_provider.dart +++ b/playground/frontend/playground_components/lib/src/theme/color_provider.dart @@ -79,8 +79,4 @@ class ThemeColors { Color get textColor => isDark ? BeamDarkThemeColors.text : BeamLightThemeColors.text; - - Color get progressBackgroundColor => - // TODO(nausharipov): reuse these colors after discussion with Anna - isDark ? const Color(0xffFFFFFF) : const Color(0xff242639); } diff --git a/playground/frontend/playground_components/lib/src/widgets/divider.dart b/playground/frontend/playground_components/lib/src/widgets/divider.dart index e76b793e0909..a6ceef08bee6 100644 --- a/playground/frontend/playground_components/lib/src/widgets/divider.dart +++ b/playground/frontend/playground_components/lib/src/widgets/divider.dart @@ -18,6 +18,8 @@ import 'package:flutter/material.dart'; +/// Replaces Flutter's Divider, which is buggy with HTML renderer. +/// See https://github.com/flutter/flutter/issues/46339 class BeamDivider extends StatelessWidget { final EdgeInsets? margin; const BeamDivider({this.margin}); diff --git a/playground/frontend/playground_components/lib/src/widgets/logo.dart b/playground/frontend/playground_components/lib/src/widgets/logo.dart index febc64786fc3..61741807e3c6 100644 --- a/playground/frontend/playground_components/lib/src/widgets/logo.dart +++ b/playground/frontend/playground_components/lib/src/widgets/logo.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; +import '../constants/names.dart'; import '../constants/sizes.dart'; import '../generated/assets.gen.dart'; @@ -29,6 +30,7 @@ class BeamLogo extends StatelessWidget { return Image.asset( Assets.png.beamLogo.path, height: BeamIconSizes.large, + package: BeamNames.package, ); } } diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart b/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart index 33be8bb42464..4ef912b9b5c7 100644 --- a/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart +++ b/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart @@ -20,6 +20,10 @@ import 'package:flutter/material.dart'; class Pan { final Widget child; + + /// Fraction of a split view, from 0 to 1. + /// For example, 0.5 is half of a 2-column split view. + /// Null means no limit. final double? minWeight; final double? maxWeight; diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart b/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart index a9131b039d24..0c924bdb2331 100644 --- a/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart +++ b/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart @@ -49,14 +49,14 @@ class BeamSplitView extends StatelessWidget { controller: SplitViewController( limits: pans .map( - (e) => WeightLimit( - min: e.minWeight, - max: e.maxWeight, + (pan) => WeightLimit( + min: pan.minWeight, + max: pan.maxWeight, ), ) - .toList(), + .toList(growable: false), ), - children: pans.map((e) => e.child).toList(), + children: pans.map((e) => e.child).toList(growable: false), ); } } diff --git a/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart index 51c16f369e9c..53cf3c46bc3b 100644 --- a/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart @@ -35,18 +35,12 @@ class ToggleThemeButton extends StatelessWidget { final text = notifier.isDarkMode ? 'ui.lightMode'.tr() : 'ui.darkMode'.tr(); - return Padding( - padding: const EdgeInsets.symmetric( - vertical: BeamSizes.size4, - horizontal: BeamSizes.size8, - ), - child: TextButton.icon( - icon: SvgPicture.asset(Assets.svg.themeMode), - label: Text(text), - onPressed: () { - notifier.toggleTheme(); - }, - ), + return TextButton.icon( + icon: SvgPicture.asset(Assets.svg.themeMode), + label: Text(text), + onPressed: () { + notifier.toggleTheme(); + }, ); }, ); From 8222faed4f41ee51a587764355411a63e08b6775 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Wed, 7 Sep 2022 10:20:00 +0600 Subject: [PATCH 03/17] branded sign in buttons #22600 --- .../frontend/assets/svg/github-logo.svg | 3 ++ .../frontend/assets/svg/google-logo.svg | 6 ++++ .../components/expansion_tile_wrapper.dart | 9 ++--- .../frontend/lib/components/sdk_dropdown.dart | 2 +- .../sign_in/sign_in_overlay_content.dart | 35 ++++++++++++++++--- .../lib/src/constants/colors.dart | 9 +++-- 6 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 learning/tour-of-beam/frontend/assets/svg/github-logo.svg create mode 100644 learning/tour-of-beam/frontend/assets/svg/google-logo.svg diff --git a/learning/tour-of-beam/frontend/assets/svg/github-logo.svg b/learning/tour-of-beam/frontend/assets/svg/github-logo.svg new file mode 100644 index 000000000000..ff9dd2f259c6 --- /dev/null +++ b/learning/tour-of-beam/frontend/assets/svg/github-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/learning/tour-of-beam/frontend/assets/svg/google-logo.svg b/learning/tour-of-beam/frontend/assets/svg/google-logo.svg new file mode 100644 index 000000000000..a0eb5d38a184 --- /dev/null +++ b/learning/tour-of-beam/frontend/assets/svg/google-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart index 34612cb04806..b09e6177ef2f 100644 --- a/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart +++ b/learning/tour-of-beam/frontend/lib/components/expansion_tile_wrapper.dart @@ -17,6 +17,7 @@ */ import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; class ExpansionTileWrapper extends StatelessWidget { final ExpansionTile expansionTile; @@ -26,10 +27,10 @@ class ExpansionTileWrapper extends StatelessWidget { Widget build(BuildContext context) { return Theme( data: Theme.of(context).copyWith( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - dividerColor: Colors.transparent, + hoverColor: BeamColors.transparent, + splashColor: BeamColors.transparent, + highlightColor: BeamColors.transparent, + dividerColor: BeamColors.transparent, unselectedWidgetColor: Colors.grey, colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.grey), visualDensity: const VisualDensity(vertical: -4), diff --git a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart index 57746b647da1..47f1a728b8ea 100644 --- a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart +++ b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart @@ -40,7 +40,7 @@ class SdkDropdown extends StatelessWidget { .toList(growable: false), isDense: true, alignment: Alignment.center, - focusColor: Colors.transparent, + focusColor: BeamColors.transparent, borderRadius: BorderRadius.circular(BeamSizes.size6), ), ); diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart index bbbfa4e4a63d..e5bb67a8f189 100644 --- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart @@ -18,15 +18,37 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:playground_components/playground_components.dart'; import '../../constants/sizes.dart'; +import '../../generated/assets.gen.dart'; class SignInOverlayContent extends StatelessWidget { const SignInOverlayContent(); @override Widget build(BuildContext context) { + final isLightTheme = Theme.of(context).brightness == Brightness.light; + final brandedButtonTextStyle = MaterialStatePropertyAll( + Theme.of(context).textTheme.bodyMedium, + ); + final darkButtonStyle = ButtonStyle( + textStyle: brandedButtonTextStyle, + backgroundColor: const MaterialStatePropertyAll(BeamColors.darkGrey), + ); + final githubLightButtonStyle = ButtonStyle( + textStyle: brandedButtonTextStyle, + backgroundColor: const MaterialStatePropertyAll(BeamColors.darkBlue), + ); + final googleLightButtonStyle = ButtonStyle( + elevation: const MaterialStatePropertyAll(BeamSizes.size4), + foregroundColor: const MaterialStatePropertyAll(BeamColors.black), + textStyle: brandedButtonTextStyle, + overlayColor: MaterialStatePropertyAll(Theme.of(context).hoverColor), + backgroundColor: const MaterialStatePropertyAll(BeamColors.white), + ); + return _Body( child: Column( children: [ @@ -40,15 +62,18 @@ class SignInOverlayContent extends StatelessWidget { textAlign: TextAlign.center, ).tr(), const _Divider(), - // TODO(nausharipov): check branded buttons in firebase_auth - ElevatedButton( + ElevatedButton.icon( onPressed: () {}, - child: const Text('ui.continueGitHub').tr(), + style: isLightTheme ? githubLightButtonStyle : darkButtonStyle, + icon: SvgPicture.asset(Assets.svg.githubLogo), + label: const Text('ui.continueGitHub').tr(), ), const SizedBox(height: BeamSizes.size16), - ElevatedButton( + ElevatedButton.icon( onPressed: () {}, - child: const Text('ui.continueGoogle').tr(), + style: isLightTheme ? googleLightButtonStyle : darkButtonStyle, + icon: SvgPicture.asset(Assets.svg.googleLogo), + label: const Text('ui.continueGoogle').tr(), ), ], ), diff --git a/playground/frontend/playground_components/lib/src/constants/colors.dart b/playground/frontend/playground_components/lib/src/constants/colors.dart index e5dad6a705a5..43aa2a56d85d 100644 --- a/playground/frontend/playground_components/lib/src/constants/colors.dart +++ b/playground/frontend/playground_components/lib/src/constants/colors.dart @@ -19,12 +19,15 @@ import 'package:flutter/material.dart'; class BeamColors { + static const transparent = Colors.transparent; static const white = Colors.white; static const black = Colors.black; static const grey1 = Color(0xffDFE1E3); static const grey2 = Color(0xffCBCBCB); static const grey3 = Color(0xffA0A4AB); static const grey4 = Color(0x30808080); + static const darkGrey = Color(0xff2E2E34); + static const darkBlue = Color(0xff242639); static const green = Color(0xff37AC66); static const orange = Color(0xffEEAB00); @@ -32,16 +35,16 @@ class BeamColors { } class BeamLightThemeColors { - static const primaryBackground = Colors.white; + static const primaryBackground = BeamColors.white; static const secondaryBackground = Color(0xffFCFCFC); static const grey = Color(0xffE5E5E5); - static const text = Color(0xff242639); + static const text = BeamColors.darkBlue; static const primary = Color(0xffE74D1A); } class BeamDarkThemeColors { static const primaryBackground = Color(0xff18181B); - static const secondaryBackground = Color(0xff2E2E34); + static const secondaryBackground = BeamColors.darkGrey; static const grey = Color(0xff3F3F46); static const text = Color(0xffFFFFFF); static const primary = Color(0xffF26628); From dd585bc2400749f08117c6853c1d7cfbe30de909 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Wed, 7 Sep 2022 10:59:42 +0600 Subject: [PATCH 04/17] _BrandedSignInButtons #22600 --- .../sign_in/sign_in_overlay_content.dart | 94 ++++++++++++------- .../frontend/lib/constants/sizes.dart | 2 +- .../lib/src/theme/theme.dart | 13 +-- 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart index e5bb67a8f189..7c23afa3614e 100644 --- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart @@ -29,26 +29,6 @@ class SignInOverlayContent extends StatelessWidget { @override Widget build(BuildContext context) { - final isLightTheme = Theme.of(context).brightness == Brightness.light; - final brandedButtonTextStyle = MaterialStatePropertyAll( - Theme.of(context).textTheme.bodyMedium, - ); - final darkButtonStyle = ButtonStyle( - textStyle: brandedButtonTextStyle, - backgroundColor: const MaterialStatePropertyAll(BeamColors.darkGrey), - ); - final githubLightButtonStyle = ButtonStyle( - textStyle: brandedButtonTextStyle, - backgroundColor: const MaterialStatePropertyAll(BeamColors.darkBlue), - ); - final googleLightButtonStyle = ButtonStyle( - elevation: const MaterialStatePropertyAll(BeamSizes.size4), - foregroundColor: const MaterialStatePropertyAll(BeamColors.black), - textStyle: brandedButtonTextStyle, - overlayColor: MaterialStatePropertyAll(Theme.of(context).hoverColor), - backgroundColor: const MaterialStatePropertyAll(BeamColors.white), - ); - return _Body( child: Column( children: [ @@ -62,19 +42,7 @@ class SignInOverlayContent extends StatelessWidget { textAlign: TextAlign.center, ).tr(), const _Divider(), - ElevatedButton.icon( - onPressed: () {}, - style: isLightTheme ? githubLightButtonStyle : darkButtonStyle, - icon: SvgPicture.asset(Assets.svg.githubLogo), - label: const Text('ui.continueGitHub').tr(), - ), - const SizedBox(height: BeamSizes.size16), - ElevatedButton.icon( - onPressed: () {}, - style: isLightTheme ? googleLightButtonStyle : darkButtonStyle, - icon: SvgPicture.asset(Assets.svg.googleLogo), - label: const Text('ui.continueGoogle').tr(), - ), + const _BrandedSignInButtons(), ], ), ); @@ -105,10 +73,68 @@ class _Divider extends StatelessWidget { @override Widget build(BuildContext context) { return Container( + color: Theme.of(context).dividerColor, margin: const EdgeInsets.symmetric(vertical: 20), width: BeamSizes.size32, height: BeamSizes.size1, - color: BeamColors.grey3, + ); + } +} + +class _BrandedSignInButtons extends StatelessWidget { + const _BrandedSignInButtons(); + + @override + Widget build(BuildContext context) { + final isLightTheme = Theme.of(context).brightness == Brightness.light; + final textStyle = + MaterialStatePropertyAll(Theme.of(context).textTheme.bodyMedium); + const padding = MaterialStatePropertyAll( + EdgeInsets.symmetric( + vertical: BeamSizes.size20, + horizontal: BeamSizes.size24, + ), + ); + const minimumSize = MaterialStatePropertyAll(Size(double.infinity, 0)); + + final darkButtonStyle = ButtonStyle( + minimumSize: minimumSize, + padding: padding, + textStyle: textStyle, + backgroundColor: const MaterialStatePropertyAll(BeamColors.darkGrey), + ); + final githubLightButtonStyle = ButtonStyle( + minimumSize: minimumSize, + padding: padding, + textStyle: textStyle, + backgroundColor: const MaterialStatePropertyAll(BeamColors.darkBlue), + ); + final googleLightButtonStyle = ButtonStyle( + minimumSize: minimumSize, + padding: padding, + elevation: const MaterialStatePropertyAll(BeamSizes.size4), + foregroundColor: const MaterialStatePropertyAll(BeamColors.black), + textStyle: textStyle, + overlayColor: MaterialStatePropertyAll(Theme.of(context).hoverColor), + backgroundColor: const MaterialStatePropertyAll(BeamColors.white), + ); + + return Column( + children: [ + ElevatedButton.icon( + onPressed: () {}, + style: isLightTheme ? githubLightButtonStyle : darkButtonStyle, + icon: SvgPicture.asset(Assets.svg.githubLogo), + label: const Text('ui.continueGitHub').tr(), + ), + const SizedBox(height: BeamSizes.size16), + ElevatedButton.icon( + onPressed: () {}, + style: isLightTheme ? googleLightButtonStyle : darkButtonStyle, + icon: SvgPicture.asset(Assets.svg.googleLogo), + label: const Text('ui.continueGoogle').tr(), + ), + ], ); } } diff --git a/learning/tour-of-beam/frontend/lib/constants/sizes.dart b/learning/tour-of-beam/frontend/lib/constants/sizes.dart index f4677576a8a8..bb9a665c8a30 100644 --- a/learning/tour-of-beam/frontend/lib/constants/sizes.dart +++ b/learning/tour-of-beam/frontend/lib/constants/sizes.dart @@ -18,7 +18,7 @@ class TobSizes { static const double footerHeight = 35; - static const double authOverlayWidth = 300; + static const double authOverlayWidth = 260; } class ScreenSizes { diff --git a/playground/frontend/playground_components/lib/src/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart index 9544fbae167f..67d018c494b0 100644 --- a/playground/frontend/playground_components/lib/src/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -19,13 +19,13 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../constants/colors.dart'; -import '../constants/sizes.dart'; +import '../../playground_components.dart'; final kLightTheme = ThemeData( brightness: Brightness.light, primaryColor: BeamLightThemeColors.primary, canvasColor: BeamLightThemeColors.primaryBackground, + dividerColor: BeamLightThemeColors.grey, scaffoldBackgroundColor: BeamLightThemeColors.secondaryBackground, backgroundColor: BeamLightThemeColors.primaryBackground, textTheme: _getTextTheme(BeamLightThemeColors.text), @@ -42,6 +42,7 @@ final kDarkTheme = ThemeData( brightness: Brightness.dark, primaryColor: BeamDarkThemeColors.primary, canvasColor: BeamDarkThemeColors.primaryBackground, + dividerColor: BeamDarkThemeColors.grey, scaffoldBackgroundColor: BeamDarkThemeColors.secondaryBackground, backgroundColor: BeamDarkThemeColors.primaryBackground, textTheme: _getTextTheme(BeamDarkThemeColors.text), @@ -110,7 +111,7 @@ TextTheme _getTextTheme(Color textColor) { TextButtonThemeData _getTextButtonTheme(Color textColor) { return TextButtonThemeData( style: TextButton.styleFrom( - primary: textColor, + foregroundColor: textColor, shape: _getButtonBorder(BeamBorderRadius.large), ), ); @@ -122,7 +123,7 @@ OutlinedButtonThemeData _getOutlineButtonTheme( ) { return OutlinedButtonThemeData( style: OutlinedButton.styleFrom( - primary: textColor, + foregroundColor: textColor, side: BorderSide(color: outlineColor, width: 3), padding: _buttonPadding, shape: _getButtonBorder(BeamBorderRadius.small), @@ -133,8 +134,8 @@ OutlinedButtonThemeData _getOutlineButtonTheme( ElevatedButtonThemeData _getElevatedButtonTheme(Color color) { return ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - onPrimary: BeamColors.white, - primary: color, + foregroundColor: BeamColors.white, + backgroundColor: color, padding: _buttonPadding, elevation: BeamSizes.size0, ), From f9ed0cebf7139f30878f851f00eaa4d9a6da8c27 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Wed, 7 Sep 2022 11:01:38 +0600 Subject: [PATCH 05/17] _Divider color #22600 --- .../lib/components/sign_in/sign_in_overlay_content.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart index 7c23afa3614e..ebb66ed8bb12 100644 --- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart @@ -73,7 +73,7 @@ class _Divider extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - color: Theme.of(context).dividerColor, + color: BeamColors.grey3, margin: const EdgeInsets.symmetric(vertical: 20), width: BeamSizes.size32, height: BeamSizes.size1, From 999e916326835af548f43f5a35751a24fb169708 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Wed, 7 Sep 2022 15:03:16 +0600 Subject: [PATCH 06/17] profile #22600 --- .../frontend/assets/png/profile-website.png | Bin 0 -> 614 bytes .../frontend/assets/svg/github-logo.svg | 20 ++- .../frontend/assets/svg/google-logo.svg | 26 +++- .../frontend/assets/svg/profile-about.svg | 21 +++ .../frontend/assets/svg/profile-delete.svg | 19 +++ .../frontend/assets/svg/profile-logout.svg | 19 +++ .../frontend/assets/translations/en.yaml | 38 ++--- .../login_button.dart} | 19 ++- .../login_content.dart} | 10 +- .../lib/components/profile/avatar.dart | 56 ++++++++ .../components/profile/profile_content.dart | 135 ++++++++++++++++++ .../frontend/lib/components/scaffold.dart | 10 +- .../lib/src/theme/theme.dart | 5 +- .../lib/src/widgets/toggle_theme_button.dart | 1 - 14 files changed, 335 insertions(+), 44 deletions(-) create mode 100644 learning/tour-of-beam/frontend/assets/png/profile-website.png create mode 100644 learning/tour-of-beam/frontend/assets/svg/profile-about.svg create mode 100644 learning/tour-of-beam/frontend/assets/svg/profile-delete.svg create mode 100644 learning/tour-of-beam/frontend/assets/svg/profile-logout.svg rename learning/tour-of-beam/frontend/lib/components/{sign_in/sign_in_button.dart => login/login_button.dart} (81%) rename learning/tour-of-beam/frontend/lib/components/{sign_in/sign_in_overlay_content.dart => login/login_content.dart} (95%) create mode 100644 learning/tour-of-beam/frontend/lib/components/profile/avatar.dart create mode 100644 learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart diff --git a/learning/tour-of-beam/frontend/assets/png/profile-website.png b/learning/tour-of-beam/frontend/assets/png/profile-website.png new file mode 100644 index 0000000000000000000000000000000000000000..e0b25a42aafb3dad19d384e81f53b831633e5aea GIT binary patch literal 614 zcmV-s0-61ZP)FJM;kCBm)#>U2vkB^OwjgXL#d3kw|kdS0#Wb5ne>gwv`Jv^z`)Q<>lDe*vH4m#l^+x>FLSI$;ima-QC^Q)z$0k>(bKF&d$z< zhljbjxs8pDgoK3O-{0EW+QY-cy}iAsr>CBto|>APnVFfDm6iMZ`|t1X?d|Q$%gd~+ ztf;7{)YR0!zrVJ&wzajju&}VNudkAllIpl$5C8xGBy>_vQvezi`1ttYEcnyOwH*)( z`1ttu_3r1@%Eqmgf_ZCZU{+E*AbjNLNdN!=1W80eR4C8Q&c#;4Koo%CDHcivcZW&J z#64*|)bRIz9A+XcZf4Etzx~b?^dD_Bjre?o+s?0qE^oj2E8KR|Wo<&7WZC#FhiOEP zzZsv5xvXjSoFvqr0PoxL*-O={o?(0s_&~GmS;KjT9{?ZLx9NJ47I`KB-ZTkgLf%VJ zECvX&B~JZ#n3;?+DDLGL!oS_Y30{|cU84w zeWB*;Jm4)wcO8p + - - + + \ No newline at end of file diff --git a/learning/tour-of-beam/frontend/assets/svg/google-logo.svg b/learning/tour-of-beam/frontend/assets/svg/google-logo.svg index a0eb5d38a184..b704ef2cc7c4 100644 --- a/learning/tour-of-beam/frontend/assets/svg/google-logo.svg +++ b/learning/tour-of-beam/frontend/assets/svg/google-logo.svg @@ -1,6 +1,22 @@ + + - - - - - + + + + + \ No newline at end of file diff --git a/learning/tour-of-beam/frontend/assets/svg/profile-about.svg b/learning/tour-of-beam/frontend/assets/svg/profile-about.svg new file mode 100644 index 000000000000..9096d879ce6e --- /dev/null +++ b/learning/tour-of-beam/frontend/assets/svg/profile-about.svg @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/learning/tour-of-beam/frontend/assets/svg/profile-delete.svg b/learning/tour-of-beam/frontend/assets/svg/profile-delete.svg new file mode 100644 index 000000000000..2d801aba6e28 --- /dev/null +++ b/learning/tour-of-beam/frontend/assets/svg/profile-delete.svg @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/learning/tour-of-beam/frontend/assets/svg/profile-logout.svg b/learning/tour-of-beam/frontend/assets/svg/profile-logout.svg new file mode 100644 index 000000000000..9f4b5de09be7 --- /dev/null +++ b/learning/tour-of-beam/frontend/assets/svg/profile-logout.svg @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index 6bc0e751e195..ca8f929fdb73 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -16,28 +16,32 @@ # under the License. ui: - builtWith: 'Built with Apache Beam' - copyright: '© The Apache Software Foundation' - darkMode: 'Dark Mode' - lightMode: 'Light Mode' - privacyPolicy: 'Privacy Policy' - reportIssue: 'Report Issue in GitHub' - signIn: 'Sign in' - continueGitHub: 'Continue with GitHub' - continueGoogle: 'Continue with Google' + about: About Tour of Beam + builtWith: Built with Apache Beam + continueGitHub: Continue with GitHub + continueGoogle: Continue with Google + copyright: © The Apache Software Foundation + darkMode: Dark Mode + lightMode: Light Mode + privacyPolicy: Privacy Policy + reportIssue: Report Issue in GitHub + signIn: Sign in + signOut: Sign out + toWebsite: To Apache Beam website + deleteAccount: Delete my account pages: welcome: - title: 'Welcome to the Tour of Beam!' - ifSaveProgress: 'Your journey is broken down into learning modules. If you would like to save your progress and track completed modules, please' + title: Welcome to the Tour of Beam! + ifSaveProgress: Your journey is broken down into learning modules. If you would like to save your progress and track completed modules, please signIn: ' sign in.' selectLanguage: 'Please select the default language (you may change the language at any time):' - startLearning: 'Start learning' + startLearning: Start learning tour: - summaryTitle: 'Table of Contents' - completeUnit: 'Complete Unit' + summaryTitle: Table of Contents + completeUnit: Complete Unit dialogs: signInIf: If you would like to save your progress and track completed modules complexity: - basic: 'Basic level' - medium: 'Medium level' - advanced: 'Advanced level' + basic: Basic level + medium: Medium level + advanced: Advanced level diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart b/learning/tour-of-beam/frontend/lib/components/login/login_button.dart similarity index 81% rename from learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart rename to learning/tour-of-beam/frontend/lib/components/login/login_button.dart index 4f4fbdf2fff5..36d96faffe5a 100644 --- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_button.dart +++ b/learning/tour-of-beam/frontend/lib/components/login/login_button.dart @@ -20,25 +20,22 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:playground_components/playground_components.dart'; -import 'sign_in_overlay_content.dart'; +import 'login_content.dart'; -class SignInButton extends StatefulWidget { - const SignInButton(); +class LoginButton extends StatelessWidget { + const LoginButton(); - @override - State createState() => _SignInButtonState(); -} - -class _SignInButtonState extends State { @override Widget build(BuildContext context) { return TextButton( - onPressed: _openOverlay, + onPressed: () { + _openOverlay(context); + }, child: const Text('ui.signIn').tr(), ); } - void _openOverlay() { + void _openOverlay(BuildContext context) { OverlayEntry? overlay; overlay = OverlayEntry( builder: (context) => DismissibleOverlay( @@ -48,7 +45,7 @@ class _SignInButtonState extends State { child: const Positioned( right: BeamSizes.size10, top: BeamSizes.appBarHeight, - child: SignInOverlayContent(), + child: LoginContent(), ), ), ); diff --git a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart b/learning/tour-of-beam/frontend/lib/components/login/login_content.dart similarity index 95% rename from learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart rename to learning/tour-of-beam/frontend/lib/components/login/login_content.dart index ebb66ed8bb12..f0046184d0e0 100644 --- a/learning/tour-of-beam/frontend/lib/components/sign_in/sign_in_overlay_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/login/login_content.dart @@ -24,8 +24,8 @@ import 'package:playground_components/playground_components.dart'; import '../../constants/sizes.dart'; import '../../generated/assets.gen.dart'; -class SignInOverlayContent extends StatelessWidget { - const SignInOverlayContent(); +class LoginContent extends StatelessWidget { + const LoginContent(); @override Widget build(BuildContext context) { @@ -42,7 +42,7 @@ class SignInOverlayContent extends StatelessWidget { textAlign: TextAlign.center, ).tr(), const _Divider(), - const _BrandedSignInButtons(), + const _BrandedLoginButtons(), ], ), ); @@ -81,8 +81,8 @@ class _Divider extends StatelessWidget { } } -class _BrandedSignInButtons extends StatelessWidget { - const _BrandedSignInButtons(); +class _BrandedLoginButtons extends StatelessWidget { + const _BrandedLoginButtons(); @override Widget build(BuildContext context) { diff --git a/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart new file mode 100644 index 000000000000..70699a9e4cf3 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../../generated/assets.gen.dart'; +import 'profile_content.dart'; + +class Avatar extends StatelessWidget { + const Avatar(); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + _openOverlay(context); + }, + child: CircleAvatar( + foregroundImage: AssetImage(Assets.png.laptopLight.path), + ), + ); + } + + void _openOverlay(BuildContext context) { + OverlayEntry? overlay; + overlay = OverlayEntry( + builder: (context) => DismissibleOverlay( + close: () { + overlay?.remove(); + }, + child: const Positioned( + right: BeamSizes.size10, + top: BeamSizes.appBarHeight, + child: ProfileContent(), + ), + ), + ); + Overlay.of(context)?.insert(overlay); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart new file mode 100644 index 000000000000..69edd797db81 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../../constants/sizes.dart'; +import '../../generated/assets.gen.dart'; + +class ProfileContent extends StatelessWidget { + const ProfileContent(); + + @override + Widget build(BuildContext context) { + return _Body( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(BeamSizes.size16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Name Surname', + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + 'email@mail.com', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + const BeamDivider(), + _IconLabel( + isSvg: false, + onTap: () {}, + iconPath: Assets.png.profileWebsite.path, + label: 'ui.toWebsite'.tr(), + ), + _IconLabel( + onTap: () {}, + iconPath: Assets.svg.profileAbout, + label: 'ui.about'.tr(), + ), + const BeamDivider(), + _IconLabel( + onTap: () {}, + iconPath: Assets.svg.profileLogout, + label: 'ui.signOut'.tr(), + ), + const BeamDivider(), + _IconLabel( + onTap: () {}, + iconPath: Assets.svg.profileDelete, + label: 'ui.deleteAccount'.tr(), + ), + ], + ), + ); + } +} + +class _Body extends StatelessWidget { + final Widget child; + + const _Body({required this.child}); + + @override + Widget build(BuildContext context) { + return Material( + elevation: BeamSizes.size10, + borderRadius: BorderRadius.circular(10), + child: SizedBox( + width: TobSizes.authOverlayWidth, + child: child, + ), + ); + } +} + +class _IconLabel extends StatelessWidget { + final String iconPath; + final String label; + final void Function()? onTap; + final bool isSvg; + + const _IconLabel({ + required this.iconPath, + required this.label, + required this.onTap, + this.isSvg = true, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(BeamSizes.size12), + child: Row( + children: [ + if (isSvg) + SvgPicture.asset(iconPath) + else + Image.asset( + iconPath, + height: 20, + ), + const SizedBox(width: BeamSizes.size10), + Text(label), + ], + ), + ), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/scaffold.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart index fd3291b9dd77..754a6126abfa 100644 --- a/learning/tour-of-beam/frontend/lib/components/scaffold.dart +++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart @@ -20,9 +20,10 @@ import 'package:flutter/material.dart'; import 'package:playground_components/playground_components.dart'; import 'footer.dart'; +import 'login/login_button.dart'; import 'logo.dart'; +import 'profile/avatar.dart'; import 'sdk_dropdown.dart'; -import 'sign_in/sign_in_button.dart'; class TobScaffold extends StatelessWidget { final Widget child; @@ -32,6 +33,8 @@ class TobScaffold extends StatelessWidget { required this.child, }); + static const _isAuthorized = true; + @override Widget build(BuildContext context) { return Scaffold( @@ -42,7 +45,9 @@ class TobScaffold extends StatelessWidget { SizedBox(width: BeamSizes.size12), _ActionVerticalPadding(child: ToggleThemeButton()), SizedBox(width: BeamSizes.size6), - _ActionVerticalPadding(child: SignInButton()), + _ActionVerticalPadding( + child: _isAuthorized ? Avatar() : LoginButton(), + ), SizedBox(width: BeamSizes.size16), ], ), @@ -58,6 +63,7 @@ class TobScaffold extends StatelessWidget { class _ActionVerticalPadding extends StatelessWidget { final Widget child; + const _ActionVerticalPadding({required this.child}); @override diff --git a/playground/frontend/playground_components/lib/src/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart index 67d018c494b0..88c644a0604c 100644 --- a/playground/frontend/playground_components/lib/src/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -100,7 +100,10 @@ TextTheme _getTextTheme(Color textColor) { fontSize: 13, fontWeight: FontWeight.w400, ), - bodySmall: _emptyTextStyle, + bodySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + ), ).apply( bodyColor: textColor, displayColor: textColor, diff --git a/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart index 53cf3c46bc3b..abd0b82edd56 100644 --- a/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart @@ -21,7 +21,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; -import '../constants/sizes.dart'; import '../generated/assets.gen.dart'; import '../theme/switch_notifier.dart'; From 1d1ff9093304d945363d8fa14f708b27941f916c Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Wed, 7 Sep 2022 16:16:12 +0600 Subject: [PATCH 07/17] moved split_view from PGC into ToB #22600 --- .../lib/components/profile/avatar.dart | 1 + .../lib/components}/split_view/pan.dart | 0 .../lib/components}/split_view/widget.dart | 17 ++++----- .../frontend/lib/pages/tour/screen.dart | 4 ++- learning/tour-of-beam/frontend/pubspec.lock | 2 +- learning/tour-of-beam/frontend/pubspec.yaml | 1 + .../lib/playground_components.dart | 3 +- .../lib/src/widgets/drag_indicator.dart | 35 +++++++++++++++++++ .../playground_components/pubspec.yaml | 1 - 9 files changed, 48 insertions(+), 16 deletions(-) rename {playground/frontend/playground_components/lib/src/widgets => learning/tour-of-beam/frontend/lib/components}/split_view/pan.dart (100%) rename {playground/frontend/playground_components/lib/src/widgets => learning/tour-of-beam/frontend/lib/components}/split_view/widget.dart (80%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/drag_indicator.dart diff --git a/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart index 70699a9e4cf3..959c83876e3c 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart @@ -32,6 +32,7 @@ class Avatar extends StatelessWidget { _openOverlay(context); }, child: CircleAvatar( + backgroundColor: BeamColors.white, foregroundImage: AssetImage(Assets.png.laptopLight.path), ), ); diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart b/learning/tour-of-beam/frontend/lib/components/split_view/pan.dart similarity index 100% rename from playground/frontend/playground_components/lib/src/widgets/split_view/pan.dart rename to learning/tour-of-beam/frontend/lib/components/split_view/pan.dart diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart b/learning/tour-of-beam/frontend/lib/components/split_view/widget.dart similarity index 80% rename from playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart rename to learning/tour-of-beam/frontend/lib/components/split_view/widget.dart index 0c924bdb2331..fb28cb4f7ee1 100644 --- a/playground/frontend/playground_components/lib/src/widgets/split_view/widget.dart +++ b/learning/tour-of-beam/frontend/lib/components/split_view/widget.dart @@ -17,18 +17,16 @@ */ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:split_view/split_view.dart'; -import '../../../playground_components.dart'; -import '../../constants/names.dart'; -import '../../generated/assets.gen.dart'; +import 'pan.dart'; -class BeamSplitView extends StatelessWidget { +class TobSplitView extends StatelessWidget { final Axis direction; final List pans; - const BeamSplitView({ + const TobSplitView({ required this.direction, required this.pans, }); @@ -39,10 +37,7 @@ class BeamSplitView extends StatelessWidget { gripSize: BeamSizes.splitViewSeparator, gripColor: ThemeColors.of(context).divider, gripColorActive: ThemeColors.of(context).divider, - indicator: SvgPicture.asset( - Assets.svg.drag, - package: BeamNames.package, - ), + indicator: const DragIndicator(), viewMode: direction == Axis.horizontal ? SplitViewMode.Horizontal : SplitViewMode.Vertical, @@ -56,7 +51,7 @@ class BeamSplitView extends StatelessWidget { ) .toList(growable: false), ), - children: pans.map((e) => e.child).toList(growable: false), + children: pans.map((pan) => pan.child).toList(growable: false), ); } } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart index a74b087d4dbc..da6246f80afd 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -24,6 +24,8 @@ import 'package:playground_components/playground_components.dart'; import '../../components/expansion_tile_wrapper.dart'; import '../../components/filler_text.dart'; import '../../components/scaffold.dart'; +import '../../components/split_view/pan.dart'; +import '../../components/split_view/widget.dart'; import '../../constants/sizes.dart'; import '../../generated/assets.gen.dart'; @@ -50,7 +52,7 @@ class _WideTour extends StatelessWidget { children: const [ _ContentTree(), Expanded( - child: BeamSplitView( + child: TobSplitView( direction: Axis.horizontal, pans: [ Pan( diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index 875519b1c369..46d831721eca 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -630,7 +630,7 @@ packages: source: hosted version: "1.9.0" split_view: - dependency: transitive + dependency: "direct main" description: name: split_view url: "https://pub.dartlang.org" diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index e34b474d281e..c11c3f0be732 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: path: ../../../playground/frontend/playground_components provider: ^6.0.3 shared_preferences: ^2.0.15 + split_view: ^3.2.1 url_launcher: ^6.1.5 url_strategy: ^0.2.0 diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart index 9a7cc82a3be6..21338ebf9780 100644 --- a/playground/frontend/playground_components/lib/playground_components.dart +++ b/playground/frontend/playground_components/lib/playground_components.dart @@ -26,7 +26,6 @@ export 'src/theme/theme.dart'; export 'src/widgets/complexity.dart'; export 'src/widgets/dismissible_overlay.dart'; export 'src/widgets/divider.dart'; +export 'src/widgets/drag_indicator.dart'; export 'src/widgets/logo.dart'; -export 'src/widgets/split_view/pan.dart'; -export 'src/widgets/split_view/widget.dart'; export 'src/widgets/toggle_theme_button.dart'; diff --git a/playground/frontend/playground_components/lib/src/widgets/drag_indicator.dart b/playground/frontend/playground_components/lib/src/widgets/drag_indicator.dart new file mode 100644 index 000000000000..768b513b9e33 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/drag_indicator.dart @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../constants/names.dart'; +import '../generated/assets.gen.dart'; + +class DragIndicator extends StatelessWidget { + const DragIndicator(); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.drag, + package: BeamNames.package, + ); + } +} diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index 499b1c972237..d489229cb822 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -31,7 +31,6 @@ dependencies: google_fonts: ^3.0.1 provider: ^6.0.3 shared_preferences: ^2.0.15 - split_view: ^3.2.1 dev_dependencies: build_runner: ^2.2.0 From dfca997b9ac514a0f73ef31b3b7bd3203c3f1820 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Wed, 7 Sep 2022 16:18:01 +0600 Subject: [PATCH 08/17] indentation fix #22600 --- .../frontend/lib/components/filler_text.dart | 32 +++++++++---------- .../lib/src/widgets/divider.dart | 32 +++++++++---------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/learning/tour-of-beam/frontend/lib/components/filler_text.dart b/learning/tour-of-beam/frontend/lib/components/filler_text.dart index 0bd60b5b987f..ca6099e6d9de 100644 --- a/learning/tour-of-beam/frontend/lib/components/filler_text.dart +++ b/learning/tour-of-beam/frontend/lib/components/filler_text.dart @@ -1,20 +1,20 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import 'package:flutter/material.dart'; diff --git a/playground/frontend/playground_components/lib/src/widgets/divider.dart b/playground/frontend/playground_components/lib/src/widgets/divider.dart index a6ceef08bee6..fdd0b0ef6092 100644 --- a/playground/frontend/playground_components/lib/src/widgets/divider.dart +++ b/playground/frontend/playground_components/lib/src/widgets/divider.dart @@ -1,20 +1,20 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import 'package:flutter/material.dart'; From f8e462d9e127e31a8224956ca8d0dd41a13df6fe Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Wed, 7 Sep 2022 16:24:41 +0600 Subject: [PATCH 09/17] split ProfileContent into widgets #22600 --- .../components/profile/profile_content.dart | 104 +++++++++++------- .../frontend/lib/components/scaffold.dart | 1 + 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart index 69edd797db81..a036a37ad2a6 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart @@ -32,47 +32,10 @@ class ProfileContent extends StatelessWidget { return _Body( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(BeamSizes.size16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Name Surname', - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - 'email@mail.com', - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - const BeamDivider(), - _IconLabel( - isSvg: false, - onTap: () {}, - iconPath: Assets.png.profileWebsite.path, - label: 'ui.toWebsite'.tr(), - ), - _IconLabel( - onTap: () {}, - iconPath: Assets.svg.profileAbout, - label: 'ui.about'.tr(), - ), - const BeamDivider(), - _IconLabel( - onTap: () {}, - iconPath: Assets.svg.profileLogout, - label: 'ui.signOut'.tr(), - ), - const BeamDivider(), - _IconLabel( - onTap: () {}, - iconPath: Assets.svg.profileDelete, - label: 'ui.deleteAccount'.tr(), - ), + children: const [ + _Info(), + BeamDivider(), + _Buttons(), ], ), ); @@ -97,6 +60,65 @@ class _Body extends StatelessWidget { } } +class _Info extends StatelessWidget { + const _Info(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(BeamSizes.size16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Name Surname', + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + 'email@mail.com', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ); + } +} + +class _Buttons extends StatelessWidget { + const _Buttons(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _IconLabel( + isSvg: false, + onTap: () {}, + iconPath: Assets.png.profileWebsite.path, + label: 'ui.toWebsite'.tr(), + ), + _IconLabel( + onTap: () {}, + iconPath: Assets.svg.profileAbout, + label: 'ui.about'.tr(), + ), + const BeamDivider(), + _IconLabel( + onTap: () {}, + iconPath: Assets.svg.profileLogout, + label: 'ui.signOut'.tr(), + ), + const BeamDivider(), + _IconLabel( + onTap: () {}, + iconPath: Assets.svg.profileDelete, + label: 'ui.deleteAccount'.tr(), + ), + ], + ); + } +} + class _IconLabel extends StatelessWidget { final String iconPath; final String label; diff --git a/learning/tour-of-beam/frontend/lib/components/scaffold.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart index 754a6126abfa..f8352140436e 100644 --- a/learning/tour-of-beam/frontend/lib/components/scaffold.dart +++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart @@ -33,6 +33,7 @@ class TobScaffold extends StatelessWidget { required this.child, }); + // TODO(nausharipov): get state static const _isAuthorized = true; @override From 0ac651374a798b830162eda6497ff7cd2ad0b6ad Mon Sep 17 00:00:00 2001 From: Alexey Inkin Date: Thu, 15 Sep 2022 12:34:41 +0400 Subject: [PATCH 10/17] Extract playground components to a separate package (#22600) --- .../frontend/assets/translations/en.yaml | 5 +- .../frontend/lib/components/footer.dart | 9 +- .../lib/components/split_view/widget.dart | 57 --- learning/tour-of-beam/frontend/lib/main.dart | 6 +- .../lib/pages/tour/playground_demo.dart | 125 ++++++ .../frontend/lib/pages/tour/screen.dart | 38 +- .../frontend/lib/pages/welcome/screen.dart | 12 +- learning/tour-of-beam/frontend/pubspec.lock | 88 +++- learning/tour-of-beam/frontend/pubspec.yaml | 13 +- playground/buf.gen.yaml | 4 +- playground/frontend/assets/theme.svg | 27 -- .../frontend/assets/translations/en.yaml | 21 + .../components/banner/banner_description.dart | 7 +- .../dropdown_button/dropdown_button.dart | 8 +- .../playground_run_or_cancel_button.dart | 57 +++ .../toggle_theme_button.dart | 55 --- playground/frontend/lib/config/theme.dart | 305 -------------- playground/frontend/lib/configure_nonweb.dart | 21 - playground/frontend/lib/constants/sizes.dart | 14 - playground/frontend/lib/l10n/app_en.arb | 16 - playground/frontend/lib/l10n/l10n.dart | 6 +- playground/frontend/lib/main.dart | 31 +- .../components/new_example_action.dart | 10 +- .../actions/components/reset_action.dart | 30 +- .../modules/analytics/analytics_service.dart | 7 +- .../analytics/google_analytics_service.dart | 9 +- .../editor/components/editor_themes.dart | 64 --- .../pipeline_options_dropdown_body.dart | 2 +- .../pipeline_options_dropdown_separator.dart | 4 +- .../pipeline_options_form.dart | 5 +- .../pipeline_options_text_field.dart | 12 +- .../share_dropdown/link_text_field.dart | 8 +- .../share_dropdown/share_button.dart | 18 +- .../share_dropdown/share_dropdown_body.dart | 2 +- .../share_dropdown/share_tabs/share_tabs.dart | 12 +- .../snippet_save_and_share_tabs.dart | 12 +- .../share_dropdown/share_tabs_headers.dart | 28 +- .../code_client/grpc_code_client.dart | 234 ----------- .../description_popover.dart | 4 +- .../description_popover_button.dart | 9 +- .../category_expansion_panel.dart | 4 +- .../example_list/example_item_actions.dart | 7 +- .../components/example_list/example_list.dart | 6 +- .../example_list/expansion_panel_item.dart | 25 +- .../components/filter/category_bubble.dart | 55 +-- .../components/filter/type_filter.dart | 2 +- .../multifile_popover/multifile_popover.dart | 4 +- .../multifile_popover_button.dart | 6 +- .../components/search_field/search_field.dart | 11 +- .../modules/examples/example_selector.dart | 46 +-- ...og_default_example_loading_descriptor.dart | 44 -- .../examples_loading_descriptor_factory.dart | 29 +- .../standard_example_loading_descriptor.dart | 46 --- ...ser_shared_example_loading_descriptor.dart | 47 --- .../examples/models/example_token_type.dart | 4 +- .../example_client/example_client.dart | 66 --- .../example_client/grpc_example_client.dart | 366 ----------------- .../repositories/example_repository.dart | 98 ----- .../models/get_example_request.dart | 37 -- .../models/get_list_of_examples_request.dart | 37 -- .../messages/handlers/messages_handler.dart | 8 +- .../handlers/set_content_message_handler.dart | 8 +- .../handlers/set_sdk_message_handler.dart | 8 +- .../messages/models/set_content_message.dart | 2 +- .../messages/models/set_sdk_message.dart | 8 +- .../output/components/output_area.dart | 74 ---- .../output_header/result_filter_bubble.dart | 80 ---- .../output_header/result_filter_popover.dart | 75 ---- .../output/models/output_placement.dart | 7 + .../modules/sdk/components/sdk_selector.dart | 41 +- .../sdk/components/sdk_selector_row.dart | 6 +- .../frontend/lib/modules/sdk/models/sdk.dart | 117 ------ .../shortcuts/components/shortcut_row.dart | 7 +- .../components/shortcuts_manager.dart | 4 +- .../shortcuts/components/shortcuts_modal.dart | 33 +- .../shortcuts/constants/global_shortcuts.dart | 67 +--- .../components/embedded_actions.dart | 14 +- .../components/embedded_appbar_title.dart | 48 +-- .../components/embedded_editor.dart | 23 +- .../embedded_playground_page.dart | 13 +- .../playground/components/close_listener.dart | 4 +- .../components/editor_textarea_wrapper.dart | 77 +--- .../feedback/feedback_dropdown_content.dart | 13 +- .../playground/components/more_actions.dart | 18 +- .../components/playground_page_body.dart | 36 +- .../components/playground_page_footer.dart | 6 +- .../components/playground_page_providers.dart | 53 ++- .../lib/pages/playground/playground_page.dart | 76 ++-- .../example_loaders/examples_loader.dart | 110 ----- .../states/example_selector_state.dart | 32 +- .../playground/states/examples_state.dart | 218 ---------- playground/frontend/lib/playground_app.dart | 54 ++- .../frontend/lib/utils/analytics_utils.dart | 11 +- .../assets/buttons}/reset.svg | 0 .../assets/buttons}/theme-mode.svg | 0 .../assets/notification_icons/error.svg} | 0 .../assets/notification_icons/info.svg} | 0 .../assets/notification_icons/success.svg} | 0 .../assets/notification_icons/warning.svg} | 0 .../svg/{drag.svg => drag-horizontal.svg} | 6 +- .../assets/svg/drag-vertical.svg | 23 ++ .../assets/svg/theme-mode.svg | 20 - .../assets/translations/en.yaml | 32 ++ .../lib/playground_components.dart | 46 ++- .../iis_workaround_channel.dart} | 5 +- .../api/iis_workaround_channel_non_web.dart | 30 ++ .../src/api/iis_workaround_channel_web.dart} | 9 +- .../lib/src}/api/v1/api.pb.dart | 0 .../lib/src}/api/v1/api.pbenum.dart | 0 .../lib/src}/api/v1/api.pbgrpc.dart | 0 .../lib/src}/api/v1/api.pbjson.dart | 0 .../lib/src/cache/example_cache.dart | 237 +++++++++++ .../lib/src/constants/colors.dart | 27 ++ .../src/constants/playground_components.dart} | 28 +- .../lib/src/constants/sizes.dart | 10 + .../catalog_default_example_loader.dart | 20 +- .../content_example_loader.dart | 13 +- .../example_loaders/empty_example_loader.dart | 14 +- .../example_loaders/example_loader.dart | 4 +- .../example_loader_factory.dart | 53 +++ .../example_loaders/examples_loader.dart | 103 +++++ .../standard_example_loader.dart | 35 +- .../user_shared_example_loader.dart | 16 +- .../controllers/playground_controller.dart} | 141 ++++--- .../snippet_editing_controller.dart | 22 +- .../src/models/category_with_examples.dart} | 28 +- .../lib/src/models/example.dart | 60 +++ .../lib/src/models/example_base.dart} | 61 +-- ...og_default_example_loading_descriptor.dart | 31 ++ .../content_example_loading_descriptor.dart | 33 +- .../empty_example_loading_descriptor.dart | 31 ++ .../example_loading_descriptor.dart | 11 +- .../examples_loading_descriptor.dart | 13 +- .../standard_example_loading_descriptor.dart} | 14 +- ...ser_shared_example_loading_descriptor.dart | 30 ++ .../lib/src/models/intents.dart | 35 ++ .../lib/src/models/outputs.dart} | 8 +- .../lib/src/models/sdk.dart | 96 +++++ .../lib/src/models/shortcut.dart} | 41 +- .../src/notifications}/base_notification.dart | 21 +- .../lib/src/notifications}/notification.dart | 23 +- .../code_client/code_client.dart | 18 +- .../code_client/grpc_code_client.dart | 256 ++++++++++++ .../src/repositories}/code_repository.dart | 74 ++-- .../example_client/example_client.dart | 67 ++++ .../example_client/grpc_example_client.dart | 376 ++++++++++++++++++ .../src/repositories/example_repository.dart | 99 +++++ .../models}/check_status_response.dart | 6 +- ...et_default_precompiled_object_request.dart | 34 ++ ...get_precompiled_object_code_response.dart} | 6 +- .../get_precompiled_object_request.dart | 39 ++ .../get_precompiled_object_response.dart} | 10 +- .../get_precompiled_objects_request.dart} | 26 +- .../get_precompiled_objects_response.dart} | 11 +- .../models/get_snippet_request.dart | 4 +- .../models/get_snippet_response.dart | 6 +- .../repositories/models}/output_response.dart | 5 +- .../repositories/models}/run_code_error.dart | 4 +- .../models}/run_code_request.dart | 8 +- .../models}/run_code_response.dart | 4 +- .../repositories/models}/run_code_result.dart | 28 +- .../models/save_snippet_request.dart | 10 +- .../models/save_snippet_response.dart | 0 .../src/repositories/models/shared_file.dart} | 0 .../src/repositories/sdk_grpc_extension.dart} | 39 +- .../lib/src/theme/color_provider.dart | 82 ---- .../lib/src/theme/switch_notifier.dart | 19 +- .../lib/src/theme/theme.dart | 231 ++++++++++- .../lib/src/util/pipeline_options.dart} | 0 .../src/util}/replace_incorrect_symbols.dart | 6 +- .../lib/src/util}/run_with_retry.dart | 0 .../lib/src/widgets/bubble.dart | 69 ++++ .../{drag_indicator.dart => drag_handle.dart} | 18 +- .../lib/src/widgets}/editor_textarea.dart | 54 ++- .../lib/src/widgets}/header_icon_button.dart | 9 +- .../lib/src/widgets}/loading_indicator.dart | 12 +- .../lib/src/widgets/logo.dart | 4 +- .../lib/src/widgets/output/graph}/graph.dart | 21 +- .../graph/graph_builder/canvas_drawer.dart | 22 +- .../extractors/edge_extractor.dart | 4 +- .../extractors/element_extractor.dart | 6 +- .../extractors/extractor_utils.dart | 0 .../graph_builder/extractors/extractors.dart | 0 .../extractors/label_extractor.dart | 2 +- .../graph/graph_builder/graph_builder.dart | 51 +-- .../graph_builder/painters/edge_painter.dart | 21 +- .../graph_builder/painters/graph_painter.dart | 21 +- .../graph_builder/painters/node_painter.dart | 18 +- .../widgets/output}/graph/models/graph.dart | 0 .../output}/graph/models/table_cell.dart | 0 .../lib/src/widgets/output}/output.dart | 45 ++- .../lib/src/widgets/output/output_area.dart | 63 +++ .../src/widgets/output}/output_result.dart | 13 +- .../lib/src/widgets/output}/output_tab.dart | 31 +- .../lib/src/widgets/output}/output_tabs.dart | 57 ++- .../widgets/output/result_filter_bubble.dart | 52 +++ .../widgets/output/result_filter_popover.dart | 84 ++++ .../lib/src/widgets/reset_button.dart | 57 +++ .../lib/src/widgets}/run_button.dart | 39 +- .../lib/src/widgets/run_or_cancel_button.dart | 63 +++ .../lib/src/widgets}/shortcut_tooltip.dart | 12 +- .../lib/src/widgets/snippet_editor.dart | 46 +++ .../lib/src/widgets}/split_view.dart | 41 +- .../lib/src/widgets}/tab_header.dart | 6 +- .../lib/src/widgets/toggle_theme_button.dart | 6 +- .../widgets}/toggle_theme_icon_button.dart | 19 +- .../playground_components/pubspec.yaml | 17 +- .../test/src/cache/example_cache_test.dart} | 124 +++--- .../test/src/common/categories.dart} | 42 +- .../src/common}/example_repository_mock.dart | 14 +- .../test/src/common/examples.dart} | 30 +- .../test/src/common/requests.dart | 66 +++ .../playground_controller_test.dart} | 41 +- .../repositories}/code_repository_test.dart | 126 +++--- .../repositories/example_repository_test.dart | 130 ++++++ .../test/src/util/pipeline_options_test.dart} | 2 +- .../test/src/util}/run_with_retry_test.dart | 2 +- playground/frontend/pubspec.lock | 104 +++-- playground/frontend/pubspec.yaml | 17 +- .../code_repository_test.mocks.dart | 138 ------- .../example_repository_test.dart | 130 ------ .../example_repository_test.mocks.dart | 147 ------- .../handlers/messages_debouncer_test.dart | 10 +- .../models/set_content_message_test.dart | 17 +- .../messages/models/set_sdk_message_test.dart | 6 +- .../messages/parsers/message_parser_test.dart | 10 +- .../states/example_selector_state_test.dart | 38 +- .../example_selector_state_test.mocks.dart | 47 ++- .../states/mocks/categories_mock.dart | 45 --- .../mocks/example_repository_mock.mocks.dart | 113 ------ .../playground/states/mocks/request_mock.dart | 45 --- .../states/playground_state_test.mocks.dart | 216 ---------- 232 files changed, 4459 insertions(+), 4768 deletions(-) delete mode 100644 learning/tour-of-beam/frontend/lib/components/split_view/widget.dart create mode 100644 learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart delete mode 100644 playground/frontend/assets/theme.svg create mode 100644 playground/frontend/assets/translations/en.yaml create mode 100644 playground/frontend/lib/components/playground_run_or_cancel_button.dart delete mode 100644 playground/frontend/lib/components/toggle_theme_button/toggle_theme_button.dart delete mode 100644 playground/frontend/lib/config/theme.dart delete mode 100644 playground/frontend/lib/configure_nonweb.dart delete mode 100644 playground/frontend/lib/modules/editor/components/editor_themes.dart delete mode 100644 playground/frontend/lib/modules/editor/repository/code_repository/code_client/grpc_code_client.dart delete mode 100644 playground/frontend/lib/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart delete mode 100644 playground/frontend/lib/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart delete mode 100644 playground/frontend/lib/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart delete mode 100644 playground/frontend/lib/modules/examples/repositories/example_client/example_client.dart delete mode 100644 playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart delete mode 100644 playground/frontend/lib/modules/examples/repositories/example_repository.dart delete mode 100644 playground/frontend/lib/modules/examples/repositories/models/get_example_request.dart delete mode 100644 playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_request.dart delete mode 100644 playground/frontend/lib/modules/output/components/output_area.dart delete mode 100644 playground/frontend/lib/modules/output/components/output_header/result_filter_bubble.dart delete mode 100644 playground/frontend/lib/modules/output/components/output_header/result_filter_popover.dart delete mode 100644 playground/frontend/lib/modules/sdk/models/sdk.dart delete mode 100644 playground/frontend/lib/pages/playground/states/example_loaders/examples_loader.dart delete mode 100644 playground/frontend/lib/pages/playground/states/examples_state.dart rename playground/frontend/{assets => playground_components/assets/buttons}/reset.svg (100%) rename {learning/tour-of-beam/frontend/assets/svg => playground/frontend/playground_components/assets/buttons}/theme-mode.svg (100%) rename playground/frontend/{assets/error_notification.svg => playground_components/assets/notification_icons/error.svg} (100%) rename playground/frontend/{assets/info_notification.svg => playground_components/assets/notification_icons/info.svg} (100%) rename playground/frontend/{assets/success_notification.svg => playground_components/assets/notification_icons/success.svg} (100%) rename playground/frontend/{assets/warning_notification.svg => playground_components/assets/notification_icons/warning.svg} (100%) rename playground/frontend/playground_components/assets/svg/{drag.svg => drag-horizontal.svg} (86%) create mode 100644 playground/frontend/playground_components/assets/svg/drag-vertical.svg delete mode 100644 playground/frontend/playground_components/assets/svg/theme-mode.svg rename playground/frontend/playground_components/lib/src/{constants/names.dart => api/iis_workaround_channel.dart} (88%) create mode 100644 playground/frontend/playground_components/lib/src/api/iis_workaround_channel_non_web.dart rename playground/frontend/{lib/api/iis_workaround_channel.dart => playground_components/lib/src/api/iis_workaround_channel_web.dart} (88%) rename playground/frontend/{lib => playground_components/lib/src}/api/v1/api.pb.dart (100%) rename playground/frontend/{lib => playground_components/lib/src}/api/v1/api.pbenum.dart (100%) rename playground/frontend/{lib => playground_components/lib/src}/api/v1/api.pbgrpc.dart (100%) rename playground/frontend/{lib => playground_components/lib/src}/api/v1/api.pbjson.dart (100%) create mode 100644 playground/frontend/playground_components/lib/src/cache/example_cache.dart rename playground/frontend/{lib/modules/examples/models/example_origin.dart => playground_components/lib/src/constants/playground_components.dart} (68%) rename playground/frontend/{lib/pages/playground/states => playground_components/lib/src/controllers}/example_loaders/catalog_default_example_loader.dart (65%) rename playground/frontend/{lib/pages/playground/states => playground_components/lib/src/controllers}/example_loaders/content_example_loader.dart (72%) rename playground/frontend/{lib/pages/playground/states => playground_components/lib/src/controllers}/example_loaders/empty_example_loader.dart (70%) rename playground/frontend/{lib/pages/playground/states => playground_components/lib/src/controllers}/example_loaders/example_loader.dart (89%) create mode 100644 playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader_factory.dart create mode 100644 playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart rename playground/frontend/{lib/pages/playground/states => playground_components/lib/src/controllers}/example_loaders/standard_example_loader.dart (61%) rename playground/frontend/{lib/pages/playground/states => playground_components/lib/src/controllers}/example_loaders/user_shared_example_loader.dart (65%) rename playground/frontend/{lib/pages/playground/states/playground_state.dart => playground_components/lib/src/controllers/playground_controller.dart} (75%) rename playground/frontend/{lib/modules/editor => playground_components/lib/src}/controllers/snippet_editing_controller.dart (80%) rename playground/frontend/{lib/modules/examples/models/category_model.dart => playground_components/lib/src/models/category_with_examples.dart} (56%) create mode 100644 playground/frontend/playground_components/lib/src/models/example.dart rename playground/frontend/{lib/modules/examples/models/example_model.dart => playground_components/lib/src/models/example_base.dart} (60%) create mode 100644 playground/frontend/playground_components/lib/src/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart rename playground/frontend/{lib/modules/examples => playground_components/lib/src}/models/example_loading_descriptors/content_example_loading_descriptor.dart (70%) create mode 100644 playground/frontend/playground_components/lib/src/models/example_loading_descriptors/empty_example_loading_descriptor.dart rename playground/frontend/{lib/modules/examples => playground_components/lib/src}/models/example_loading_descriptors/example_loading_descriptor.dart (79%) rename playground/frontend/{lib/modules/examples => playground_components/lib/src}/models/example_loading_descriptors/examples_loading_descriptor.dart (88%) rename playground/frontend/{lib/modules/examples/repositories/models/get_list_of_examples_response.dart => playground_components/lib/src/models/example_loading_descriptors/standard_example_loading_descriptor.dart} (75%) create mode 100644 playground/frontend/playground_components/lib/src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart create mode 100644 playground/frontend/playground_components/lib/src/models/intents.dart rename playground/frontend/{lib/modules/examples/models/outputs_model.dart => playground_components/lib/src/models/outputs.dart} (89%) create mode 100644 playground/frontend/playground_components/lib/src/models/sdk.dart rename playground/frontend/{lib/components/horizontal_divider/horizontal_divider.dart => playground_components/lib/src/models/shortcut.dart} (56%) rename playground/frontend/{lib/modules/notifications/components => playground_components/lib/src/notifications}/base_notification.dart (82%) rename playground/frontend/{lib/modules/notifications/components => playground_components/lib/src/notifications}/notification.dart (82%) rename playground/frontend/{lib/modules/editor/repository/code_repository => playground_components/lib/src/repositories}/code_client/code_client.dart (66%) create mode 100644 playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart rename playground/frontend/{lib/modules/editor/repository/code_repository => playground_components/lib/src/repositories}/code_repository.dart (76%) create mode 100644 playground/frontend/playground_components/lib/src/repositories/example_client/example_client.dart create mode 100644 playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart create mode 100644 playground/frontend/playground_components/lib/src/repositories/example_repository.dart rename playground/frontend/{lib/modules/editor/repository/code_repository/code_client => playground_components/lib/src/repositories/models}/check_status_response.dart (87%) create mode 100644 playground/frontend/playground_components/lib/src/repositories/models/get_default_precompiled_object_request.dart rename playground/frontend/{lib/modules/examples/repositories/models/get_example_code_response.dart => playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart} (87%) create mode 100644 playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_request.dart rename playground/frontend/{lib/modules/examples/repositories/models/get_example_response.dart => playground_components/lib/src/repositories/models/get_precompiled_object_response.dart} (81%) rename playground/frontend/{lib/modules/shortcuts/models/shortcut.dart => playground_components/lib/src/repositories/models/get_precompiled_objects_request.dart} (70%) rename playground/frontend/{lib/configure_web.dart => playground_components/lib/src/repositories/models/get_precompiled_objects_response.dart} (76%) rename playground/frontend/{lib/modules/examples => playground_components/lib/src}/repositories/models/get_snippet_request.dart (92%) rename playground/frontend/{lib/modules/examples => playground_components/lib/src}/repositories/models/get_snippet_response.dart (86%) rename playground/frontend/{lib/modules/editor/repository/code_repository/code_client => playground_components/lib/src/repositories/models}/output_response.dart (87%) rename playground/frontend/{lib/modules/editor/repository/code_repository => playground_components/lib/src/repositories/models}/run_code_error.dart (94%) rename playground/frontend/{lib/modules/editor/repository/code_repository => playground_components/lib/src/repositories/models}/run_code_request.dart (88%) rename playground/frontend/{lib/modules/editor/repository/code_repository/code_client => playground_components/lib/src/repositories/models}/run_code_response.dart (93%) rename playground/frontend/{lib/modules/editor/repository/code_repository => playground_components/lib/src/repositories/models}/run_code_result.dart (75%) rename playground/frontend/{lib/modules/examples => playground_components/lib/src}/repositories/models/save_snippet_request.dart (80%) rename playground/frontend/{lib/modules/examples => playground_components/lib/src}/repositories/models/save_snippet_response.dart (100%) rename playground/frontend/{lib/modules/examples/repositories/models/shared_file_model.dart => playground_components/lib/src/repositories/models/shared_file.dart} (100%) rename playground/frontend/{lib/modules/shortcuts/utils/shortcuts_display_name.dart => playground_components/lib/src/repositories/sdk_grpc_extension.dart} (53%) delete mode 100644 playground/frontend/playground_components/lib/src/theme/color_provider.dart rename playground/frontend/{lib/modules/editor/parsers/run_options_parser.dart => playground_components/lib/src/util/pipeline_options.dart} (100%) rename playground/frontend/{lib/utils => playground_components/lib/src/util}/replace_incorrect_symbols.dart (87%) rename playground/frontend/{lib/utils => playground_components/lib/src/util}/run_with_retry.dart (100%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/bubble.dart rename playground/frontend/playground_components/lib/src/widgets/{drag_indicator.dart => drag_handle.dart} (69%) rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/editor_textarea.dart (80%) rename playground/frontend/{lib/modules/actions/components => playground_components/lib/src/widgets}/header_icon_button.dart (91%) rename playground/frontend/{lib/components/loading_indicator => playground_components/lib/src/widgets}/loading_indicator.dart (83%) rename playground/frontend/{lib/modules/output/components => playground_components/lib/src/widgets/output/graph}/graph.dart (83%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/canvas_drawer.dart (93%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/extractors/edge_extractor.dart (93%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/extractors/element_extractor.dart (90%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/extractors/extractor_utils.dart (100%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/extractors/extractors.dart (100%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/extractors/label_extractor.dart (93%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/graph_builder.dart (85%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/painters/edge_painter.dart (90%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/painters/graph_painter.dart (83%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/graph_builder/painters/node_painter.dart (82%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/models/graph.dart (100%) rename playground/frontend/{lib/modules => playground_components/lib/src/widgets/output}/graph/models/table_cell.dart (100%) rename playground/frontend/{lib/modules/output/components => playground_components/lib/src/widgets/output}/output.dart (64%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/output/output_area.dart rename playground/frontend/{lib/modules/output/components => playground_components/lib/src/widgets/output}/output_result.dart (86%) rename playground/frontend/{lib/modules/output/components/output_header => playground_components/lib/src/widgets/output}/output_tab.dart (78%) rename playground/frontend/{lib/modules/output/components/output_header => playground_components/lib/src/widgets/output}/output_tabs.dart (51%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/output/result_filter_bubble.dart create mode 100644 playground/frontend/playground_components/lib/src/widgets/output/result_filter_popover.dart create mode 100644 playground/frontend/playground_components/lib/src/widgets/reset_button.dart rename playground/frontend/{lib/modules/editor/components => playground_components/lib/src/widgets}/run_button.dart (69%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart rename playground/frontend/{lib/modules/shortcuts/components => playground_components/lib/src/widgets}/shortcut_tooltip.dart (81%) create mode 100644 playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart rename playground/frontend/{lib/components/split_view => playground_components/lib/src/widgets}/split_view.dart (79%) rename playground/frontend/{lib/modules/output/components/output_header => playground_components/lib/src/widgets}/tab_header.dart (91%) rename playground/frontend/{lib/components/toggle_theme_button => playground_components/lib/src/widgets}/toggle_theme_icon_button.dart (73%) rename playground/frontend/{test/pages/playground/states/examples_state_test.dart => playground_components/test/src/cache/example_cache_test.dart} (52%) rename playground/frontend/{lib/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart => playground_components/test/src/common/categories.dart} (51%) rename playground/frontend/{test/pages/playground/states/mocks => playground_components/test/src/common}/example_repository_mock.dart (82%) rename playground/frontend/{test/pages/playground/states/mocks/example_mock.dart => playground_components/test/src/common/examples.dart} (72%) create mode 100644 playground/frontend/playground_components/test/src/common/requests.dart rename playground/frontend/{test/pages/playground/states/playground_state_test.dart => playground_components/test/src/controllers/playground_controller_test.dart} (79%) rename playground/frontend/{test/modules/editor/repository/code_repository => playground_components/test/src/repositories}/code_repository_test.dart (65%) create mode 100644 playground/frontend/playground_components/test/src/repositories/example_repository_test.dart rename playground/frontend/{test/modules/editor/parsers/run_options_parser_test.dart => playground_components/test/src/util/pipeline_options_test.dart} (96%) rename playground/frontend/{test/utils => playground_components/test/src/util}/run_with_retry_test.dart (97%) delete mode 100644 playground/frontend/test/modules/editor/repository/code_repository/code_repository_test.mocks.dart delete mode 100644 playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.dart delete mode 100644 playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.mocks.dart delete mode 100644 playground/frontend/test/pages/playground/states/mocks/categories_mock.dart delete mode 100644 playground/frontend/test/pages/playground/states/mocks/example_repository_mock.mocks.dart delete mode 100644 playground/frontend/test/pages/playground/states/mocks/request_mock.dart delete mode 100644 playground/frontend/test/pages/playground/states/playground_state_test.mocks.dart diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index ca8f929fdb73..919c2a39ae72 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -21,14 +21,13 @@ ui: continueGitHub: Continue with GitHub continueGoogle: Continue with Google copyright: © The Apache Software Foundation - darkMode: Dark Mode - lightMode: Light Mode privacyPolicy: Privacy Policy reportIssue: Report Issue in GitHub signIn: Sign in signOut: Sign out toWebsite: To Apache Beam website deleteAccount: Delete my account + pages: welcome: title: Welcome to the Tour of Beam! @@ -39,8 +38,10 @@ pages: tour: summaryTitle: Table of Contents completeUnit: Complete Unit + dialogs: signInIf: If you would like to save your progress and track completed modules + complexity: basic: Basic level medium: Medium level diff --git a/learning/tour-of-beam/frontend/lib/components/footer.dart b/learning/tour-of-beam/frontend/lib/components/footer.dart index fc7af8cd4691..e801836bb898 100644 --- a/learning/tour-of-beam/frontend/lib/components/footer.dart +++ b/learning/tour-of-beam/frontend/lib/components/footer.dart @@ -42,7 +42,7 @@ class Footer extends StatelessWidget { const Text('ui.copyright').tr(), ], ), - // TODO(nausharipov): get version + // TODO(nausharipov): get version, https://github.com/apache/beam/issues/23038 Text( '${'ui.builtWith'.tr()} (TODO: Version)', style: const TextStyle( @@ -61,6 +61,9 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { + final themeData = Theme.of(context); + final ext = themeData.extension()!; + return Container( width: double.infinity, height: TobSizes.footerHeight, @@ -69,9 +72,9 @@ class _Body extends StatelessWidget { horizontal: BeamSizes.size16, ), decoration: BoxDecoration( - color: ThemeColors.of(context).secondaryBackground, + color: ext.secondaryBackgroundColor, border: Border( - top: BorderSide(color: ThemeColors.of(context).divider), + top: BorderSide(color: themeData.dividerColor), ), ), child: child, diff --git a/learning/tour-of-beam/frontend/lib/components/split_view/widget.dart b/learning/tour-of-beam/frontend/lib/components/split_view/widget.dart deleted file mode 100644 index fb28cb4f7ee1..000000000000 --- a/learning/tour-of-beam/frontend/lib/components/split_view/widget.dart +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:playground_components/playground_components.dart'; -import 'package:split_view/split_view.dart'; - -import 'pan.dart'; - -class TobSplitView extends StatelessWidget { - final Axis direction; - final List pans; - - const TobSplitView({ - required this.direction, - required this.pans, - }); - - @override - Widget build(BuildContext context) { - return SplitView( - gripSize: BeamSizes.splitViewSeparator, - gripColor: ThemeColors.of(context).divider, - gripColorActive: ThemeColors.of(context).divider, - indicator: const DragIndicator(), - viewMode: direction == Axis.horizontal - ? SplitViewMode.Horizontal - : SplitViewMode.Vertical, - controller: SplitViewController( - limits: pans - .map( - (pan) => WeightLimit( - min: pan.minWeight, - max: pan.maxWeight, - ), - ) - .toList(growable: false), - ), - children: pans.map((pan) => pan.child).toList(growable: false), - ); - } -} diff --git a/learning/tour-of-beam/frontend/lib/main.dart b/learning/tour-of-beam/frontend/lib/main.dart index ffdb55c697d9..c7eb698c3782 100644 --- a/learning/tour-of-beam/frontend/lib/main.dart +++ b/learning/tour-of-beam/frontend/lib/main.dart @@ -17,6 +17,7 @@ */ import 'package:easy_localization/easy_localization.dart'; +import 'package:easy_localization_ext/easy_localization_ext.dart'; import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:flutter/material.dart'; import 'package:playground_components/playground_components.dart'; @@ -38,7 +39,10 @@ void main() async { startLocale: englishLocale, fallbackLocale: englishLocale, path: 'assets/translations', - assetLoader: YamlAssetLoader(), + assetLoader: MultiAssetLoader([ + PlaygroundComponents.translationLoader, + YamlAssetLoader(), + ]), child: const TourOfBeamApp(), ), ); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart new file mode 100644 index 000000000000..ed9dace49dc2 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:playground_components/playground_components.dart'; + +const String kApiClientURL = + 'https://backend-router-beta-dot-apache-beam-testing.appspot.com'; +const String kApiJavaClientURL = + 'https://backend-java-beta-dot-apache-beam-testing.appspot.com'; +const String kApiGoClientURL = + 'https://backend-go-beta-dot-apache-beam-testing.appspot.com'; +const String kApiPythonClientURL = + 'https://backend-python-beta-dot-apache-beam-testing.appspot.com'; +const String kApiScioClientURL = + 'https://backend-scio-beta-dot-apache-beam-testing.appspot.com'; + +class PlaygroundDemoWidget extends StatefulWidget { + const PlaygroundDemoWidget({Key? key}) : super(key: key); + + @override + State createState() => _PlaygroundDemoWidgetState(); +} + +class _PlaygroundDemoWidgetState extends State { + late final PlaygroundController playgroundController; + + @override + void initState() { + super.initState(); + + final exampleRepository = ExampleRepository( + client: GrpcExampleClient(url: kApiClientURL), + ); + + final codeRepository = CodeRepository( + client: GrpcCodeClient( + url: kApiClientURL, + runnerUrlsById: { + Sdk.java.id: kApiJavaClientURL, + Sdk.go.id: kApiGoClientURL, + Sdk.python.id: kApiPythonClientURL, + Sdk.scio.id: kApiScioClientURL, + }, + ), + ); + + final exampleCache = ExampleCache( + exampleRepository: exampleRepository, + hasCatalog: true, + ); + + playgroundController = PlaygroundController( + codeRepository: codeRepository, + exampleCache: exampleCache, + examplesLoader: ExamplesLoader(), + ); + + playgroundController.examplesLoader.load( + ExamplesLoadingDescriptor( + descriptors: [ + CatalogDefaultExampleLoadingDescriptor(sdk: Sdk.java), + ], + //initialSdk: Sdk.java, + ), + ); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: playgroundController, + builder: _buildOnChange, + ); + } + + Widget _buildOnChange(BuildContext context, Widget? child) { + final snippetController = playgroundController.snippetEditingController; + if (snippetController == null) { + return const LoadingIndicator(); + } + + return Stack( + children: [ + SplitView( + direction: Axis.vertical, + first: SnippetEditor( + controller: snippetController, + isEditable: true, + goToContextLine: false, + ), + second: OutputWidget( + playgroundController: playgroundController, + graphDirection: Axis.horizontal, + ), + ), + Positioned( + top: 30, + right: 30, + child: Row( + children: [ + RunOrCancelButton(playgroundController: playgroundController), + ], + ), + ), + ], + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart index da6246f80afd..513a6879f729 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -24,10 +24,9 @@ import 'package:playground_components/playground_components.dart'; import '../../components/expansion_tile_wrapper.dart'; import '../../components/filler_text.dart'; import '../../components/scaffold.dart'; -import '../../components/split_view/pan.dart'; -import '../../components/split_view/widget.dart'; import '../../constants/sizes.dart'; import '../../generated/assets.gen.dart'; +import 'playground_demo.dart'; class TourScreen extends StatelessWidget { const TourScreen(); @@ -52,18 +51,10 @@ class _WideTour extends StatelessWidget { children: const [ _ContentTree(), Expanded( - child: TobSplitView( + child: SplitView( direction: Axis.horizontal, - pans: [ - Pan( - child: _Content(), - minWeight: 0.3, - ), - Pan( - child: _Playground(), - minWeight: 0.3, - ), - ], + first: _Content(), + second: PlaygroundDemoWidget(), ), ), ], @@ -89,7 +80,7 @@ class _NarrowTour extends StatelessWidget { DecoratedBox( decoration: BoxDecoration( border: Border( - top: BorderSide(color: ThemeColors.of(context).divider), + top: BorderSide(color: Theme.of(context).dividerColor), ), ), child: const _Playground(), @@ -283,14 +274,16 @@ class _Content extends StatelessWidget { @override Widget build(BuildContext context) { + final themeData = Theme.of(context); + return Container( height: MediaQuery.of(context).size.height - BeamSizes.appBarHeight - TobSizes.footerHeight, decoration: BoxDecoration( - color: ThemeColors.of(context).background, + color: themeData.backgroundColor, border: Border( - left: BorderSide(color: ThemeColors.of(context).divider), + left: BorderSide(color: themeData.dividerColor), ), ), child: Column( @@ -314,12 +307,14 @@ class _ContentFooter extends StatelessWidget { @override Widget build(BuildContext context) { + final themeData = Theme.of(context); + return Container( decoration: BoxDecoration( border: Border( - top: BorderSide(color: ThemeColors.of(context).divider), + top: BorderSide(color: themeData.dividerColor), ), - color: ThemeColors.of(context).secondaryBackground, + color: themeData.extension()?.secondaryBackgroundColor, ), width: double.infinity, padding: const EdgeInsets.all(BeamSizes.size20), @@ -329,8 +324,8 @@ class _ContentFooter extends StatelessWidget { Flexible( child: OutlinedButton( style: OutlinedButton.styleFrom( - foregroundColor: ThemeColors.of(context).primary, - side: BorderSide(color: ThemeColors.of(context).primary), + foregroundColor: themeData.primaryColor, + side: BorderSide(color: themeData.primaryColor), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(BeamSizes.size4), @@ -357,6 +352,7 @@ class _Playground extends StatelessWidget { @override Widget build(BuildContext context) { - return const Center(child: Text('Playground')); + // TODO(alexeyinkin): Even this way the narrow layout breaks, https://github.com/apache/beam/issues/23244 + return const Center(child: Text('TODO: Playground for narrow screen')); } } diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart index fdcf63e13591..8c295687cedf 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart @@ -90,7 +90,7 @@ class _SdkSelection extends StatelessWidget { BeamSizes.appBarHeight - TobSizes.footerHeight, ), - color: ThemeColors.of(context).background, + color: Theme.of(context).backgroundColor, child: Stack( children: [ Positioned( @@ -183,7 +183,7 @@ class _IntroText extends StatelessWidget { style: Theme.of(context) .textTheme .bodyLarge! - .copyWith(color: ThemeColors.of(context).primary), + .copyWith(color: Theme.of(context).primaryColor), recognizer: TapGestureRecognizer() ..onTap = () { // TODO(nausharipov): sign in @@ -250,7 +250,7 @@ class _SdkButton extends StatelessWidget { padding: const EdgeInsets.only(right: 15, bottom: 10), child: OutlinedButton( style: OutlinedButton.styleFrom( - backgroundColor: ThemeColors.of(context).background, + backgroundColor: Theme.of(context).backgroundColor, side: groupValue == value ? null : const BorderSide(color: BeamColors.grey1), @@ -336,12 +336,14 @@ class _ModuleBody extends StatelessWidget { @override Widget build(BuildContext context) { + final themeData = Theme.of(context); + return Container( margin: _moduleLeftMargin, decoration: BoxDecoration( border: Border( left: BorderSide( - color: ThemeColors.of(context).divider, + color: themeData.dividerColor, ), ), ), @@ -351,7 +353,7 @@ class _ModuleBody extends StatelessWidget { const FillerText(width: 20), const SizedBox(height: BeamSizes.size16), Divider( - color: ThemeColors.of(context).divider, + color: themeData.dividerColor, ), ], ), diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index 46d831721eca..85f5d587816b 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -8,6 +8,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "46.0.0" + aligned_dialog: + dependency: transitive + description: + name: aligned_dialog + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.6" analyzer: dependency: transitive description: @@ -127,6 +134,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.0" + code_text_field: + dependency: "direct main" + description: + path: "." + ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437" + resolved-ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437" + url: "https://github.com/BertrandBev/code_field.git" + source: git + version: "1.0.3" collection: dependency: transitive description: @@ -183,6 +199,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + easy_localization_ext: + dependency: "direct main" + description: + name: easy_localization_ext + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" easy_localization_loader: dependency: "direct main" description: @@ -197,6 +220,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.2" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -249,6 +279,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.3.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.0" flutter_localizations: dependency: transitive description: flutter @@ -304,6 +341,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" graphs: dependency: transitive description: @@ -311,6 +355,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + grpc: + dependency: transitive + description: + name: grpc + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + highlight: + dependency: transitive + description: + name: highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.0" http: dependency: transitive description: @@ -318,6 +376,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.4" + http2: + dependency: transitive + description: + name: http2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" http_multi_server: dependency: transitive description: @@ -365,6 +430,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.6.0" + linked_scroll_controller: + dependency: transitive + description: + name: linked_scroll_controller + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" logging: dependency: transitive description: @@ -526,6 +598,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + protobuf: + dependency: transitive + description: + name: protobuf + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" provider: dependency: "direct main" description: @@ -629,13 +708,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.9.0" - split_view: - dependency: "direct main" - description: - name: split_view - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.1" stack_trace: dependency: transitive description: @@ -841,4 +913,4 @@ packages: version: "3.1.1" sdks: dart: ">=2.17.6 <3.0.0" - flutter: ">=3.0.0" + flutter: ">=3.3.1" diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index c11c3f0be732..547d378d2f93 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -18,26 +18,29 @@ name: tour_of_beam description: Tour of Beam -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' version: 0.1.0 environment: sdk: ">=2.17.6 <3.0.0" - flutter: ">=3.0.0 <4.0.0" + flutter: ">=3.3.1" dependencies: + code_text_field: + git: + url: https://github.com/BertrandBev/code_field.git + ref: 9e2c9fe52a69481f038f4b6609e8a0a776429437 easy_localization: ^3.0.1 + easy_localization_ext: ^0.1.0 easy_localization_loader: ^1.0.0 flutter: { sdk: flutter } flutter_svg: ^1.0.3 get_it: ^7.2.0 google_fonts: ^3.0.1 - playground_components: - path: ../../../playground/frontend/playground_components + playground_components: { path: ../../../playground/frontend/playground_components } provider: ^6.0.3 shared_preferences: ^2.0.15 - split_view: ^3.2.1 url_launcher: ^6.1.5 url_strategy: ^0.2.0 diff --git a/playground/buf.gen.yaml b/playground/buf.gen.yaml index bd9b9f3d8309..d04b54a6c5dd 100644 --- a/playground/buf.gen.yaml +++ b/playground/buf.gen.yaml @@ -28,5 +28,5 @@ plugins: - paths=source_relative - require_unimplemented_servers=false - name: dart - out: frontend/lib - opt: grpc \ No newline at end of file + out: frontend/playground_components/lib/src + opt: grpc diff --git a/playground/frontend/assets/theme.svg b/playground/frontend/assets/theme.svg deleted file mode 100644 index 10d8b3d5c9be..000000000000 --- a/playground/frontend/assets/theme.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/playground/frontend/assets/translations/en.yaml b/playground/frontend/assets/translations/en.yaml new file mode 100644 index 000000000000..c7d74f96d44f --- /dev/null +++ b/playground/frontend/assets/translations/en.yaml @@ -0,0 +1,21 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +intents: + playground: + clearOutput: 'Clear Output' + newExample: 'New Example' diff --git a/playground/frontend/lib/components/banner/banner_description.dart b/playground/frontend/lib/components/banner/banner_description.dart index ab9ccfd51793..34ff3b51cefa 100644 --- a/playground/frontend/lib/components/banner/banner_description.dart +++ b/playground/frontend/lib/components/banner/banner_description.dart @@ -18,7 +18,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/examples/components/description_popover/description_popover.dart'; @@ -61,10 +60,9 @@ class BannerDescription extends StatelessWidget { RichText( text: TextSpan( children: [ - TextSpan( + const TextSpan( text: kBannerDescription1, style: TextStyle( - color: ThemeColors.of(context).textColor, height: kDescriptionLineHeight, ), ), @@ -76,10 +74,9 @@ class BannerDescription extends StatelessWidget { ..onTap = () async { launchUrl(Uri.parse(kBannerUrl)); }), - TextSpan( + const TextSpan( text: kHyperlinkText, style: TextStyle( - color: ThemeColors.of(context).textColor, height: kDescriptionLineHeight, ), ), diff --git a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart index fa89fce72ff0..c2aff6f2f950 100644 --- a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart +++ b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart @@ -17,9 +17,9 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/utils/dropdown_utils.dart'; +import 'package:playground_components/playground_components.dart'; const int kAnimationDurationInMilliseconds = 80; const Offset kAnimationBeginOffset = Offset(0.0, -0.02); @@ -87,10 +87,12 @@ class _AppDropdownButtonState extends State @override Widget build(BuildContext context) { + final ext = Theme.of(context).extension()!; + return Container( height: kContainerHeight, decoration: BoxDecoration( - color: ThemeColors.of(context).dropdownButton, + color: ext.fieldBackgroundColor, borderRadius: BorderRadius.circular(kSmBorderRadius), ), child: TextButton( @@ -149,7 +151,7 @@ class _AppDropdownButtonState extends State height: widget.height, width: widget.width, decoration: BoxDecoration( - color: ThemeColors.of(context).background, + color: Theme.of(context).backgroundColor, borderRadius: BorderRadius.circular(kMdBorderRadius), ), child: widget.createDropdown(_close), diff --git a/playground/frontend/lib/components/playground_run_or_cancel_button.dart b/playground/frontend/lib/components/playground_run_or_cancel_button.dart new file mode 100644 index 000000000000..b555a71a882c --- /dev/null +++ b/playground/frontend/lib/components/playground_run_or_cancel_button.dart @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/widgets.dart'; +import 'package:playground_components/playground_components.dart'; +import 'package:provider/provider.dart'; + +import '../modules/analytics/analytics_service.dart'; +import '../utils/analytics_utils.dart'; + +class PlaygroundRunOrCancelButton extends StatelessWidget { + const PlaygroundRunOrCancelButton(); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, playgroundController, child) { + final analyticsService = AnalyticsService.get(context); + final stopwatch = Stopwatch(); + final exampleName = getAnalyticsExampleName(playgroundController); + + return RunOrCancelButton( + playgroundController: playgroundController, + beforeCancel: () { + final exampleName = getAnalyticsExampleName(playgroundController); + analyticsService.trackClickCancelRunEvent(exampleName); + }, + beforeRun: () { + stopwatch.start(); + analyticsService.trackClickRunEvent(exampleName); + }, + onComplete: () { + analyticsService.trackRunTimeEvent( + exampleName, + stopwatch.elapsedMilliseconds, + ); + }, + ); + } + ); + } +} diff --git a/playground/frontend/lib/components/toggle_theme_button/toggle_theme_button.dart b/playground/frontend/lib/components/toggle_theme_button/toggle_theme_button.dart deleted file mode 100644 index 730b5b240b92..000000000000 --- a/playground/frontend/lib/components/toggle_theme_button/toggle_theme_button.dart +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/assets.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/analytics/analytics_service.dart'; -import 'package:provider/provider.dart'; - -class ToggleThemeButton extends StatelessWidget { - const ToggleThemeButton({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final appLocale = AppLocalizations.of(context)!; - - return Consumer(builder: (context, notifier, child) { - final text = notifier.isDarkMode ? appLocale.lightMode : appLocale.darkMode; - - return Padding( - padding: const EdgeInsets.symmetric( - vertical: kSmSpacing, - horizontal: kMdSpacing, - ), - child: TextButton.icon( - icon: SvgPicture.asset(kThemeIconAsset), - label: Text(text), - onPressed: () { - notifier.toggleTheme(); - AnalyticsService.get(context) - .trackClickToggleTheme(!notifier.isDarkMode); - }, - ), - ); - }); - } -} diff --git a/playground/frontend/lib/config/theme.dart b/playground/frontend/lib/config/theme.dart deleted file mode 100644 index 62a30f0082b0..000000000000 --- a/playground/frontend/lib/config/theme.dart +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:code_text_field/code_text_field.dart'; -import 'package:flutter/material.dart'; -import 'package:playground/constants/colors.dart'; -import 'package:playground/constants/font_weight.dart'; -import 'package:playground/constants/fonts.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/editor/components/editor_themes.dart'; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -const kThemeMode = 'theme_mode'; - -class ThemeSwitchNotifier extends ChangeNotifier { - late SharedPreferences _preferences; - ThemeMode themeMode = ThemeMode.light; - - static const _darkThemeColors = ThemeColors.fromBrightness(isDark: true); - static const _lightThemeColors = ThemeColors.fromBrightness(isDark: false); - - ThemeColors get themeColors { - switch (themeMode) { - case ThemeMode.dark: - return _darkThemeColors; - default: - return _lightThemeColors; - } - } - - final _darkCodeTheme = createTheme(_darkThemeColors); - final _lightCodeTheme = createTheme(_lightThemeColors); - - CodeThemeData get codeTheme { - switch (themeMode) { - case ThemeMode.dark: - return _darkCodeTheme; - default: - return _lightCodeTheme; - } - } - - init() { - _setPreferences(); - } - - _setPreferences() async { - _preferences = await SharedPreferences.getInstance(); - themeMode = _preferences.getString(kThemeMode) == ThemeMode.dark.toString() - ? ThemeMode.dark - : ThemeMode.light; - notifyListeners(); - } - - bool get isDarkMode { - return themeMode == ThemeMode.dark; - } - - void toggleTheme() { - themeMode = themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; - _preferences.setString(kThemeMode, themeMode.toString()); - notifyListeners(); - } -} - -class ThemeSwitchNotifierProvider extends StatelessWidget { - final Widget child; - - const ThemeSwitchNotifierProvider({ - super.key, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ThemeSwitchNotifier()..init(), - child: Consumer( - builder: (context, themeSwitchNotifier, _) => ThemeColorsProvider( - data: themeSwitchNotifier.themeColors, - child: child, - ), - ), - ); - } -} - -class ThemeColorsProvider extends StatelessWidget { - final ThemeColors data; - final Widget child; - - const ThemeColorsProvider({ - super.key, - required this.data, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return Provider.value( - value: data, - child: child, - ); - } -} - -TextTheme createTextTheme(Color textColor) { - return getBaseFontTheme( - const TextTheme( - headline1: TextStyle(), - headline2: TextStyle(), - headline3: TextStyle(), - headline4: TextStyle(), - headline5: TextStyle(), - headline6: TextStyle(), - subtitle1: TextStyle(), - subtitle2: TextStyle(), - bodyText1: TextStyle(), - bodyText2: TextStyle(), - caption: TextStyle(), - overline: TextStyle(), - button: TextStyle(fontWeight: kBoldWeight), - ).apply( - bodyColor: textColor, - displayColor: textColor, - ), - ); -} - -TextButtonThemeData createTextButtonTheme(Color textColor) { - return TextButtonThemeData( - style: TextButton.styleFrom( - primary: textColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(kLgBorderRadius)), - ), - ), - ); -} - -OutlinedButtonThemeData createOutlineButtonTheme(Color textColor) { - return OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - primary: textColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(kSmBorderRadius)), - ), - ), - ); -} - -ElevatedButtonThemeData createElevatedButtonTheme(Color primaryColor) { - return ElevatedButtonThemeData( - style: ElevatedButton.styleFrom(primary: primaryColor), - ); -} - -PopupMenuThemeData createPopupMenuTheme() { - return const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(kLgBorderRadius), - ), - ), - ); -} - -AppBarTheme createAppBarTheme(Color backgroundColor) { - return AppBarTheme( - color: backgroundColor, - elevation: 1, - centerTitle: false, - ); -} - -TabBarTheme createTabBarTheme(Color textColor, Color indicatorColor) { - const labelStyle = TextStyle(fontWeight: kMediumWeight); - return TabBarTheme( - unselectedLabelColor: textColor, - labelColor: textColor, - labelStyle: labelStyle, - unselectedLabelStyle: labelStyle, - indicator: UnderlineTabIndicator( - borderSide: BorderSide(width: 2.0, color: indicatorColor), - ), - ); -} - -DialogTheme createDialogTheme(Color textColor) { - return DialogTheme( - titleTextStyle: TextStyle( - color: textColor, - fontSize: 32.0, - fontWeight: kBoldWeight, - ), - ); -} - -final kLightTheme = ThemeData( - brightness: Brightness.light, - primaryColor: kLightPrimary, - backgroundColor: kLightPrimaryBackground, - appBarTheme: createAppBarTheme(kLightSecondaryBackground), - textTheme: createTextTheme(kLightText), - popupMenuTheme: createPopupMenuTheme(), - textButtonTheme: createTextButtonTheme(kLightText), - outlinedButtonTheme: createOutlineButtonTheme(kLightText), - elevatedButtonTheme: createElevatedButtonTheme(kLightPrimary), - tabBarTheme: createTabBarTheme(kLightText, kLightPrimary), - dialogTheme: createDialogTheme(kLightText), -); - -final kDarkTheme = ThemeData( - brightness: Brightness.dark, - primaryColor: kDarkPrimary, - backgroundColor: kDarkPrimaryBackground, - appBarTheme: createAppBarTheme(kDarkSecondaryBackground), - textTheme: createTextTheme(kDarkText), - popupMenuTheme: createPopupMenuTheme(), - textButtonTheme: createTextButtonTheme(kDarkText), - outlinedButtonTheme: createOutlineButtonTheme(kDarkText), - elevatedButtonTheme: createElevatedButtonTheme(kDarkPrimary), - tabBarTheme: createTabBarTheme(kDarkText, kDarkPrimary), - dialogTheme: createDialogTheme(kDarkText), -); - -class ThemeColors { - final Color? _background; - final Color? _dropdownButton; - - final bool isDark; - - static ThemeColors of(BuildContext context, {bool listen = true}) { - return Provider.of(context, listen: listen); - } - - ThemeColors({ - required this.isDark, - Color? background, - Color? dropdownButtonColor, - }) : _background = background, - _dropdownButton = dropdownButtonColor; - - const ThemeColors.fromBrightness({ - required this.isDark, - }) : _background = null, - _dropdownButton = null; - - ThemeColors copyWith({ - Color? background, - Color? dropdownButton, - }) { - return ThemeColors( - isDark: isDark, - background: background ?? this.background, - dropdownButtonColor: dropdownButton ?? this.dropdownButton, - ); - } - - Color get dropdownButton => - _dropdownButton ?? (isDark ? kDarkGrey : kLightGrey); - - Color get divider => isDark ? kDarkGrey : kLightGrey; - - Color get lightGreyColor => isDark ? kLightGrey1 : kLightGrey; - - Color get primary => isDark ? kLightPrimary : kDarkPrimary; - - Color get primaryBackgroundTextColor => Colors.white; - - Color get lightGreyBackgroundTextColor => Colors.black; - - Color get grey1Color => isDark ? kDarkGrey1 : kLightGrey1; - - Color get secondaryBackground => - isDark ? kDarkSecondaryBackground : kLightSecondaryBackground; - - Color get background => - _background ?? - (isDark ? kDarkPrimaryBackground : kLightPrimaryBackground); - - Color get code1 => isDark ? kDarkCode2 : kLightCode2; - - Color get code2 => isDark ? kDarkCode1 : kLightCode1; - - Color get codeComment => isDark ? kDarkCodeComment : kLightCodeComment; - - Color get textColor => isDark ? kDarkText : kLightText; -} diff --git a/playground/frontend/lib/configure_nonweb.dart b/playground/frontend/lib/configure_nonweb.dart deleted file mode 100644 index 4568ded62f94..000000000000 --- a/playground/frontend/lib/configure_nonweb.dart +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -void configureApp() { - // see https://flutter.dev/docs/development/ui/navigation/url-strategies -} diff --git a/playground/frontend/lib/constants/sizes.dart b/playground/frontend/lib/constants/sizes.dart index 3d895aae285d..c4515a5e3dc1 100644 --- a/playground/frontend/lib/constants/sizes.dart +++ b/playground/frontend/lib/constants/sizes.dart @@ -25,23 +25,17 @@ const double kXlSpacing = 16.0; const double kXxlSpacing = 36.0; // sizes -const kHeaderButtonHeight = 46.0; -const kRunButtonWidth = 150.0; const kButtonHeight = 40.0; const kIconButtonSplashRadius = 24.0; -const kFooterHeight = 32.0; // border radius const double kSmBorderRadius = 4.0; const double kMdBorderRadius = 6.0; -const double kLgBorderRadius = 8.0; -const double kXlBorderRadius = 28.0; // elevation const double kElevation = 2; // icon sizes -const double kIconSizeXs = 8.0; const double kIconSizeSm = 16.0; const double kIconSizeMd = 24.0; const double kIconSizeLg = 32.0; @@ -52,16 +46,8 @@ const double kCursorSize = 1.0; // container size const double kContainerHeight = 40.0; -const double kCaptionFontSize = 10.0; -const double kCodeFontSize = 14.0; const double kLabelFontSize = 16.0; -const double kHintFontSize = 16.0; const double kTitleFontSize = 18.0; //divider size const double kDividerHeight = 1.0; -const double kLgDividerHeight = 2.0; - -//loading indicator size -const double kMdLoadingIndicatorSize = 40.0; -const double kLgLoadingIndicatorSize = 50.0; diff --git a/playground/frontend/lib/l10n/app_en.arb b/playground/frontend/lib/l10n/app_en.arb index 48a3a8675b82..538df437994a 100644 --- a/playground/frontend/lib/l10n/app_en.arb +++ b/playground/frontend/lib/l10n/app_en.arb @@ -7,14 +7,6 @@ "@darkMode": { "description": "Title for a theme switch" }, - "newExample": "New Example", - "@newExample": { - "description": "Title for the New Example button" - }, - "reset": "Reset", - "@reset": { - "description": "Title for the reset button" - }, "run": "Run", "@run": { "description": "Title for the run button" @@ -143,10 +135,6 @@ "@reportIssue": { "description": "Title for the Report issue in GitHub button" }, - "codeTextArea": "Code Text Area", - "@codeTextArea": { - "description": "Title for the Code text area semantics" - }, "bottom": "Bottom", "@bottom": { "description": "Part of the output placements semantics label" @@ -159,10 +147,6 @@ "@left": { "description": "Part of the output placements semantics label" }, - "clearOutput": "Clear Output", - "@clearOutput": { - "description": "Title for the Clear Output shortcut row" - }, "pipelineOptions": "Pipeline Options", "@pipelineOptions": { "description": "Title for the Pipeline Options" diff --git a/playground/frontend/lib/l10n/l10n.dart b/playground/frontend/lib/l10n/l10n.dart index cd63ec779909..9f7e197317e4 100644 --- a/playground/frontend/lib/l10n/l10n.dart +++ b/playground/frontend/lib/l10n/l10n.dart @@ -16,10 +16,12 @@ * limitations under the License. */ -import 'package:flutter/material.dart'; +import 'dart:ui'; class L10n { + static const en = Locale('en'); + static const locales = [ - Locale('en'), + en, ]; } diff --git a/playground/frontend/lib/main.dart b/playground/frontend/lib/main.dart index 2e57fc49a120..0c36af3e7bcf 100644 --- a/playground/frontend/lib/main.dart +++ b/playground/frontend/lib/main.dart @@ -17,15 +17,34 @@ */ import 'package:akvelon_flutter_issue_106664_workaround/akvelon_flutter_issue_106664_workaround.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:easy_localization_ext/easy_localization_ext.dart'; +import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl_browser.dart'; -import 'package:playground/configure_nonweb.dart' -if (dart.library.html) 'package:playground/configure_web.dart'; import 'package:playground/playground_app.dart'; +import 'package:playground_components/playground_components.dart'; +import 'package:url_strategy/url_strategy.dart'; -void main() { +import 'l10n/l10n.dart'; + +void main() async { FlutterIssue106664Workaround.instance.apply(); - findSystemLocale(); - configureApp(); - runApp(const PlaygroundApp()); + setPathUrlStrategy(); + await EasyLocalization.ensureInitialized(); + + await findSystemLocale(); + runApp( + EasyLocalization( + supportedLocales: L10n.locales, + startLocale: L10n.en, + fallbackLocale: L10n.en, + path: 'assets/translations', + assetLoader: MultiAssetLoader([ + PlaygroundComponents.translationLoader, + YamlAssetLoader(), + ]), + child: const PlaygroundApp(), + ), + ); } diff --git a/playground/frontend/lib/modules/actions/components/new_example_action.dart b/playground/frontend/lib/modules/actions/components/new_example_action.dart index 332b4beba397..594492987c11 100644 --- a/playground/frontend/lib/modules/actions/components/new_example_action.dart +++ b/playground/frontend/lib/modules/actions/components/new_example_action.dart @@ -16,13 +16,11 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/modules/actions/components/header_icon_button.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; -import 'package:playground/modules/shortcuts/components/shortcut_tooltip.dart'; import 'package:playground/modules/shortcuts/constants/global_shortcuts.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:url_launcher/url_launcher.dart'; class NewExampleAction extends StatelessWidget { @@ -35,9 +33,9 @@ class NewExampleAction extends StatelessWidget { child: HeaderIconButton( icon: Icon( Icons.add_circle_outline, - color: ThemeColors.of(context).grey1Color, + color: Theme.of(context).extension()?.iconColor, ), - label: AppLocalizations.of(context)!.newExample, + label: 'intents.playground.newExample'.tr(), onPressed: () { launchUrl(Uri.parse('/')); AnalyticsService.get(context).trackClickNewExample(); diff --git a/playground/frontend/lib/modules/actions/components/reset_action.dart b/playground/frontend/lib/modules/actions/components/reset_action.dart index 533bdec2d1c4..a0e111996322 100644 --- a/playground/frontend/lib/modules/actions/components/reset_action.dart +++ b/playground/frontend/lib/modules/actions/components/reset_action.dart @@ -17,33 +17,21 @@ */ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/assets.dart'; -import 'package:playground/modules/actions/components/header_icon_button.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; -import 'package:playground/modules/shortcuts/components/shortcut_tooltip.dart'; -import 'package:playground/modules/shortcuts/constants/global_shortcuts.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:playground_components/playground_components.dart'; +import 'package:provider/provider.dart'; class ResetAction extends StatelessWidget { - final VoidCallback reset; - - const ResetAction({Key? key, required this.reset}) : super(key: key); + const ResetAction(); @override Widget build(BuildContext context) { - return ShortcutTooltip( - shortcut: kResetShortcut, - child: HeaderIconButton( - icon: SvgPicture.asset( - kResetIconAsset, - color: ThemeColors.of(context).grey1Color, - ), - label: AppLocalizations.of(context)!.reset, - onPressed: () { - reset(); - AnalyticsService.get(context).trackReset(); + final analyticsService = AnalyticsService.get(context); + return Consumer( + builder: (context, playgroundController, child) => ResetButton( + playgroundController: playgroundController, + beforeReset: () { + analyticsService.trackReset(); }, ), ); diff --git a/playground/frontend/lib/modules/analytics/analytics_service.dart b/playground/frontend/lib/modules/analytics/analytics_service.dart index 6571a4898d44..0416e7dcc520 100644 --- a/playground/frontend/lib/modules/analytics/analytics_service.dart +++ b/playground/frontend/lib/modules/analytics/analytics_service.dart @@ -17,8 +17,7 @@ */ import 'package:flutter/widgets.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; abstract class AnalyticsService { @@ -26,8 +25,8 @@ abstract class AnalyticsService { return Provider.of(context, listen: false); } - void trackSelectSdk(SDK? oldSdk, SDK newSdk); - void trackSelectExample(ExampleModel newExample); + void trackSelectSdk(Sdk? oldSdk, Sdk newSdk); + void trackSelectExample(ExampleBase newExample); void trackClickNewExample(); void trackReset(); void trackClickToggleTheme(bool isDark); diff --git a/playground/frontend/lib/modules/analytics/google_analytics_service.dart b/playground/frontend/lib/modules/analytics/google_analytics_service.dart index d9eaf2e6f120..7b083b3bc25e 100644 --- a/playground/frontend/lib/modules/analytics/google_analytics_service.dart +++ b/playground/frontend/lib/modules/analytics/google_analytics_service.dart @@ -19,24 +19,23 @@ import 'package:playground/config.g.dart'; import 'package:playground/modules/analytics/analytics_events.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:usage/usage_html.dart'; class GoogleAnalyticsService implements AnalyticsService { final _analytics = AnalyticsHtml(kAnalyticsUA, 'beam', '1.0'); @override - void trackSelectSdk(SDK? oldSdk, SDK newSdk) { + void trackSelectSdk(Sdk? oldSdk, Sdk newSdk) { safeSendEvent( kSdkCategory, kSelectSdkEvent, - label: '${oldSdk?.displayName}_${newSdk.displayName}', + label: '${oldSdk?.title}_${newSdk.title}', ); } @override - void trackSelectExample(ExampleModel newExample) { + void trackSelectExample(ExampleBase newExample) { safeSendEvent( kExampleCategory, kSelectExampleEvent, diff --git a/playground/frontend/lib/modules/editor/components/editor_themes.dart b/playground/frontend/lib/modules/editor/components/editor_themes.dart deleted file mode 100644 index 42c68905f962..000000000000 --- a/playground/frontend/lib/modules/editor/components/editor_themes.dart +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:code_text_field/code_text_field.dart'; -import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; - -CodeThemeData createTheme(ThemeColors colors) { - return CodeThemeData( - styles: _createThemeStyles(colors), - ); -} - -Map _createThemeStyles(ThemeColors colors) { - return { - 'root': TextStyle( - backgroundColor: colors.background, - color: colors.textColor, - ), - 'comment': TextStyle(color: colors.codeComment), - 'quote': TextStyle(color: colors.code2), - 'variable': TextStyle(color: colors.code2), - 'keyword': TextStyle(color: colors.code2), - 'selector-tag': TextStyle(color: colors.code2), - 'built_in': TextStyle(color: colors.code2), - 'name': TextStyle(color: colors.code2), - 'tag': TextStyle(color: colors.code2), - 'string': TextStyle(color: colors.code1), - 'title': TextStyle(color: colors.code1), - 'section': TextStyle(color: colors.code1), - 'attribute': TextStyle(color: colors.code1), - 'literal': TextStyle(color: colors.code1), - 'template-tag': TextStyle(color: colors.code1), - 'template-variable': TextStyle(color: colors.code1), - 'type': TextStyle(color: colors.code1), - 'addition': TextStyle(color: colors.code1), - 'deletion': TextStyle(color: colors.code2), - 'selector-attr': TextStyle(color: colors.code2), - 'selector-pseudo': TextStyle(color: colors.code2), - 'meta': TextStyle(color: colors.code2), - 'doctag': TextStyle(color: colors.codeComment), - 'attr': TextStyle(color: colors.primary), - 'symbol': TextStyle(color: colors.code2), - 'bullet': TextStyle(color: colors.code2), - 'link': TextStyle(color: colors.code2), - 'emphasis': const TextStyle(fontStyle: FontStyle.italic), - 'strong': const TextStyle(fontWeight: FontWeight.bold), - }; -} diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart index fd1d6c9eedaa..0b8cd5836b4b 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart +++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_body.dart @@ -24,7 +24,7 @@ import 'package:playground/modules/editor/components/pipeline_options_dropdown/p import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_input.dart'; import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart'; import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart'; -import 'package:playground/modules/editor/parsers/run_options_parser.dart'; +import 'package:playground_components/playground_components.dart'; const kOptionsTabIndex = 0; const kRawTabIndex = 1; diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart index 17070dd84f16..78f8b0d7c34c 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart +++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_dropdown_separator.dart @@ -17,8 +17,8 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/sizes.dart'; +import 'package:playground_components/playground_components.dart'; class PipelineOptionsDropdownSeparator extends StatelessWidget { const PipelineOptionsDropdownSeparator({Key? key}) : super(key: key); @@ -28,7 +28,7 @@ class PipelineOptionsDropdownSeparator extends StatelessWidget { return Container( height: kDividerHeight, decoration: BoxDecoration( - color: ThemeColors.of(context).lightGreyColor, + color: Theme.of(context).extension()?.borderColor, ), ); } diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart index a7f2eabf87e3..6aa41c0c3cdf 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart +++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_form.dart @@ -16,10 +16,9 @@ * limitations under the License. */ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:collection/collection.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/colors.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/editor/components/pipeline_options_dropdown/pipeline_option_controller.dart'; @@ -81,7 +80,7 @@ class PipelineOptionsForm extends StatelessWidget { Icons.delete_outlined, color: kLightPrimary, ), - color: ThemeColors.of(context).grey1Color, + color: Theme.of(context).dividerColor, onPressed: () => onDelete(index), ), ), diff --git a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart index 2c1187365288..d4202b125697 100644 --- a/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart +++ b/playground/frontend/lib/modules/editor/components/pipeline_options_dropdown/pipeline_options_text_field.dart @@ -17,8 +17,8 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/sizes.dart'; +import 'package:playground_components/playground_components.dart'; class PipelineOptionsTextField extends StatelessWidget { final TextEditingController controller; @@ -32,6 +32,9 @@ class PipelineOptionsTextField extends StatelessWidget { @override Widget build(BuildContext context) { + final themeData = Theme.of(context); + final ext = themeData.extension()!; + return Container( margin: const EdgeInsets.only( top: kMdSpacing, @@ -48,16 +51,15 @@ class PipelineOptionsTextField extends StatelessWidget { controller: controller, decoration: InputDecoration( contentPadding: const EdgeInsets.all(kMdSpacing), - border: _getInputBorder(ThemeColors.of(context).lightGreyColor), - focusedBorder: _getInputBorder(ThemeColors.of(context).primary), + border: _getInputBorder(ext.borderColor), + focusedBorder: _getInputBorder(themeData.primaryColor), ), - cursorColor: ThemeColors.of(context).textColor, ), ), ); } - _getInputBorder(Color color) { + OutlineInputBorder _getInputBorder(Color color) { return OutlineInputBorder( borderSide: BorderSide(color: color), borderRadius: BorderRadius.circular(kMdBorderRadius), diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart index 9708a88208a0..67430514e5a3 100644 --- a/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart +++ b/playground/frontend/lib/modules/editor/components/share_dropdown/link_text_field.dart @@ -18,9 +18,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/sizes.dart'; +import 'package:playground_components/playground_components.dart'; const _kTextFieldMaxHeight = 45.0; @@ -45,9 +45,11 @@ class _LinkTextFieldState extends State { @override Widget build(BuildContext context) { + final themeData = Theme.of(context); + return Container( decoration: BoxDecoration( - color: ThemeColors.of(context).dropdownButton, + color: themeData.extension()?.borderColor, borderRadius: BorderRadius.circular(kSmBorderRadius), ), child: Container( @@ -66,7 +68,7 @@ class _LinkTextFieldState extends State { style: TextStyle( fontSize: kLabelFontSize, fontWeight: kNormalWeight, - color: ThemeColors.of(context).primary, + color: themeData.primaryColor, ), ), ), diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart index 9355934e7186..f0f6026ea693 100644 --- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart +++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_button.dart @@ -19,8 +19,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground/components/dropdown_button/dropdown_button.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_dropdown_body.dart'; +import 'package:playground_components/playground_components.dart'; const _kShareDropdownHeight = 140.0; const _kShareDropdownWidth = 460.0; @@ -31,22 +31,28 @@ class ShareButton extends StatelessWidget { @override Widget build(BuildContext context) { + final parentThemeData = Theme.of(context); + final ext = parentThemeData.extension()!; final appLocale = AppLocalizations.of(context)!; - final parentThemeData = ThemeColors.of(context); final themeData = parentThemeData.copyWith( - background: parentThemeData.secondaryBackground, - dropdownButton: parentThemeData.primary.withOpacity(_kButtonColorOpacity), + backgroundColor: ext.secondaryBackgroundColor, + extensions: { + ext.copyWith( + fieldBackgroundColor: + parentThemeData.primaryColor.withOpacity(_kButtonColorOpacity), + ), + }, ); - return ThemeColorsProvider( + return Theme( data: themeData, child: AppDropdownButton( buttonText: Text(appLocale.shareMyCode), showArrow: false, leading: Icon( Icons.share_outlined, - color: ThemeColors.of(context).primary, + color: themeData.primaryColor, ), height: _kShareDropdownHeight, width: _kShareDropdownWidth, diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart index 68a208373671..18366335fee7 100644 --- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart +++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_dropdown_body.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_tabs_headers.dart'; -import 'package:playground/modules/output/components/output_header/tab_header.dart'; +import 'package:playground_components/playground_components.dart'; const _kTabsCount = 2; diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart index 8c2e72948ae0..45d5b82cd005 100644 --- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart +++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/share_tabs.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_tabs/example_share_tabs.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class ShareTabs extends StatelessWidget { @@ -34,17 +34,17 @@ class ShareTabs extends StatelessWidget { Widget build(BuildContext context) { return Container( color: Theme.of(context).backgroundColor, - child: Consumer( - builder: (context, playgroundState, _) { - if (playgroundState.isExampleChanged) { + child: Consumer( + builder: (context, playgroundController, _) { + if (playgroundController.isExampleChanged) { return SnippetSaveAndShareTabs( - playgroundState: playgroundState, + playgroundController: playgroundController, tabController: tabController, ); } return ExampleShareTabs( - examplePath: playgroundState.selectedExample!.path, + examplePath: playgroundController.selectedExample!.path, tabController: tabController, ); }, diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart index ddd1bbb0f536..050725de8d92 100644 --- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart +++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs/snippet_save_and_share_tabs.dart @@ -17,28 +17,26 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/components/loading_indicator/loading_indicator.dart'; -import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_tabs/example_share_tabs.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; class SnippetSaveAndShareTabs extends StatelessWidget { - final PlaygroundState playgroundState; + final PlaygroundController playgroundController; final TabController tabController; const SnippetSaveAndShareTabs({ super.key, - required this.playgroundState, + required this.playgroundController, required this.tabController, }); @override Widget build(BuildContext context) { return FutureBuilder( - future: playgroundState.getSnippetId(), + future: playgroundController.getSnippetId(), builder: (context, snapshot) { if (!snapshot.hasData) { - return const LoadingIndicator(size: kLgLoadingIndicatorSize); + return const LoadingIndicator(); } return ExampleShareTabs( diff --git a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart index 4d80b4309bd2..1d528a0b228e 100644 --- a/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart +++ b/playground/frontend/lib/modules/editor/components/share_dropdown/share_tabs_headers.dart @@ -17,7 +17,7 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -35,17 +35,19 @@ class ShareTabsHeaders extends StatelessWidget { Widget build(BuildContext context) { final appLocale = AppLocalizations.of(context)!; - return Consumer(builder: (context, state, child) { - return SizedBox( - width: _width, - child: TabBar( - controller: tabController, - tabs: [ - Text(appLocale.link), - Text(appLocale.embed), - ], - ), - ); - }); + return Consumer( + builder: (context, controller, child) { + return SizedBox( + width: _width, + child: TabBar( + controller: tabController, + tabs: [ + Text(appLocale.link), + Text(appLocale.embed), + ], + ), + ); + }, + ); } } diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/grpc_code_client.dart b/playground/frontend/lib/modules/editor/repository/code_repository/code_client/grpc_code_client.dart deleted file mode 100644 index 6ec86f1826b2..000000000000 --- a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/grpc_code_client.dart +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:grpc/grpc_web.dart'; -import 'package:playground/api/iis_workaround_channel.dart'; -import 'package:playground/api/v1/api.pbgrpc.dart' as grpc; -import 'package:playground/config.g.dart'; -import 'package:playground/modules/editor/parsers/run_options_parser.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/check_status_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/code_client.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/run_code_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_error.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_request.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_result.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/utils/replace_incorrect_symbols.dart'; - -const kGeneralError = 'Failed to execute code'; - -class GrpcCodeClient implements CodeClient { - late final grpc.PlaygroundServiceClient _defaultClient; - - GrpcCodeClient() { - final channel = IisWorkaroundChannel.xhr( - Uri.parse(kApiClientURL), - ); - _defaultClient = grpc.PlaygroundServiceClient(channel); - } - - @override - Future runCode(RunCodeRequestWrapper request) { - return _runSafely(() => _createRunCodeClient(request.sdk) - .runCode(_toGrpcRequest(request)) - .then((response) => RunCodeResponse(response.pipelineUuid))); - } - - @override - Future cancelExecution(String pipelineUuid) { - return _runSafely(() => - _defaultClient.cancel(grpc.CancelRequest(pipelineUuid: pipelineUuid))); - } - - @override - Future checkStatus( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .checkStatus(grpc.CheckStatusRequest(pipelineUuid: pipelineUuid)) - .then( - (response) => CheckStatusResponse(_toClientStatus(response.status)), - )); - } - - @override - Future getCompileOutput( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .getCompileOutput( - grpc.GetCompileOutputRequest(pipelineUuid: pipelineUuid), - ) - .then((response) => _toOutputResponse(response.output))); - } - - @override - Future getRunOutput( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .getRunOutput(grpc.GetRunOutputRequest(pipelineUuid: pipelineUuid)) - .then((response) => _toOutputResponse(response.output)) - .catchError((err) { - print(err); - return _toOutputResponse(''); - })); - } - - @override - Future getLogOutput( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .getLogs(grpc.GetLogsRequest(pipelineUuid: pipelineUuid)) - .then((response) => _toOutputResponse(response.output)) - .catchError((err) { - print(err); - return _toOutputResponse(''); - })); - } - - @override - Future getRunErrorOutput( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .getRunError(grpc.GetRunErrorRequest(pipelineUuid: pipelineUuid)) - .then((response) => _toOutputResponse(response.output))); - } - - @override - Future getValidationErrorOutput( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .getValidationOutput( - grpc.GetValidationOutputRequest(pipelineUuid: pipelineUuid)) - .then((response) => _toOutputResponse(response.output))); - } - - @override - Future getPreparationErrorOutput( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .getPreparationOutput( - grpc.GetPreparationOutputRequest(pipelineUuid: pipelineUuid)) - .then((response) => _toOutputResponse(response.output))); - } - - @override - Future getGraphOutput( - String pipelineUuid, - RunCodeRequestWrapper request, - ) { - return _runSafely(() => _defaultClient - .getGraph(grpc.GetGraphRequest(pipelineUuid: pipelineUuid)) - .then((response) => OutputResponse(response.graph)) - .catchError((err) { - print(err); - return _toOutputResponse(''); - })); - } - - Future _runSafely(Future Function() invoke) async { - try { - return await invoke(); - } on GrpcError catch (error) { - throw RunCodeError(error.message); - } on Exception catch (_) { - throw RunCodeError(null); - } - } - - /// Run Code request should use different urls for each sdk - /// instead of the default one, because we need to code - /// sdk services for it - grpc.PlaygroundServiceClient _createRunCodeClient(SDK? sdk) { - String apiClientURL = kApiClientURL; - if (sdk != null) { - apiClientURL = sdk.getRoute; - } - IisWorkaroundChannel channel = IisWorkaroundChannel.xhr( - Uri.parse(apiClientURL), - ); - return grpc.PlaygroundServiceClient(channel); - } - - grpc.RunCodeRequest _toGrpcRequest(RunCodeRequestWrapper request) { - return grpc.RunCodeRequest() - ..code = request.code - ..sdk = _getGrpcSdk(request.sdk) - ..pipelineOptions = pipelineOptionsToString(request.pipelineOptions); - } - - grpc.Sdk _getGrpcSdk(SDK sdk) { - switch (sdk) { - case SDK.java: - return grpc.Sdk.SDK_JAVA; - case SDK.go: - return grpc.Sdk.SDK_GO; - case SDK.python: - return grpc.Sdk.SDK_PYTHON; - case SDK.scio: - return grpc.Sdk.SDK_SCIO; - } - } - - RunCodeStatus _toClientStatus(grpc.Status status) { - switch (status) { - case grpc.Status.STATUS_UNSPECIFIED: - return RunCodeStatus.unspecified; - case grpc.Status.STATUS_VALIDATING: - case grpc.Status.STATUS_PREPARING: - return RunCodeStatus.preparation; - case grpc.Status.STATUS_COMPILING: - return RunCodeStatus.compiling; - case grpc.Status.STATUS_EXECUTING: - return RunCodeStatus.executing; - case grpc.Status.STATUS_CANCELED: - case grpc.Status.STATUS_FINISHED: - return RunCodeStatus.finished; - case grpc.Status.STATUS_COMPILE_ERROR: - return RunCodeStatus.compileError; - case grpc.Status.STATUS_RUN_TIMEOUT: - return RunCodeStatus.timeout; - case grpc.Status.STATUS_RUN_ERROR: - return RunCodeStatus.runError; - case grpc.Status.STATUS_VALIDATION_ERROR: - return RunCodeStatus.validationError; - case grpc.Status.STATUS_PREPARATION_ERROR: - return RunCodeStatus.preparationError; - case grpc.Status.STATUS_ERROR: - return RunCodeStatus.unknownError; - } - return RunCodeStatus.unspecified; - } - - OutputResponse _toOutputResponse(String response) { - return OutputResponse(replaceIncorrectSymbols(response)); - } -} diff --git a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart index a438aac5baed..e368973cb64b 100644 --- a/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart +++ b/playground/frontend/lib/modules/examples/components/description_popover/description_popover.dart @@ -22,13 +22,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground/constants/assets.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:url_launcher/url_launcher.dart'; const kDescriptionWidth = 300.0; class DescriptionPopover extends StatelessWidget { - final ExampleModel example; + final ExampleBase example; const DescriptionPopover({Key? key, required this.example}) : super(key: key); diff --git a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart index 5e935eb0328d..8df888ed76fa 100644 --- a/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart +++ b/playground/frontend/lib/modules/examples/components/description_popover/description_popover_button.dart @@ -19,14 +19,13 @@ import 'package:aligned_dialog/aligned_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/examples/components/description_popover/description_popover.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'package:playground_components/playground_components.dart'; class DescriptionPopoverButton extends StatelessWidget { final BuildContext? parentContext; - final ExampleModel example; + final ExampleBase example; final Alignment followerAnchor; final Alignment targetAnchor; final void Function()? onOpen; @@ -52,7 +51,7 @@ class DescriptionPopoverButton extends StatelessWidget { splashRadius: kIconButtonSplashRadius, icon: Icon( Icons.info_outline_rounded, - color: ThemeColors.of(context).grey1Color, + color: Theme.of(context).extension()?.iconColor, ), tooltip: appLocale.exampleDescription, onPressed: () { @@ -69,7 +68,7 @@ class DescriptionPopoverButton extends StatelessWidget { void _showDescriptionPopover( BuildContext context, - ExampleModel example, + ExampleBase example, Alignment followerAnchor, Alignment targetAnchor, ) async { diff --git a/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart b/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart index c6dcf47271e6..049db9e5968e 100644 --- a/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart +++ b/playground/frontend/lib/modules/examples/components/example_list/category_expansion_panel.dart @@ -22,12 +22,12 @@ import 'package:expansion_widget/expansion_widget.dart'; import 'package:flutter/material.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/examples/components/example_list/expansion_panel_item.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'package:playground_components/playground_components.dart'; class CategoryExpansionPanel extends StatelessWidget { final String categoryName; final List examples; - final ExampleModel selectedExample; + final ExampleBase selectedExample; final AnimationController animationController; final OverlayEntry? dropdown; diff --git a/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart index 9cae77f6073b..584b4c157316 100644 --- a/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart +++ b/playground/frontend/lib/modules/examples/components/example_list/example_item_actions.dart @@ -18,14 +18,13 @@ import 'package:flutter/material.dart'; import 'package:playground/modules/examples/components/description_popover/description_popover_button.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'package:playground/modules/examples/components/multifile_popover/multifile_popover_button.dart'; import 'package:playground/modules/examples/models/popover_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; -import '../multifile_popover/multifile_popover_button.dart'; - class ExampleItemActions extends StatelessWidget { - final ExampleModel example; + final ExampleBase example; final BuildContext parentContext; const ExampleItemActions( diff --git a/playground/frontend/lib/modules/examples/components/example_list/example_list.dart b/playground/frontend/lib/modules/examples/components/example_list/example_list.dart index 6b175e2692a1..1c9227d2f59e 100644 --- a/playground/frontend/lib/modules/examples/components/example_list/example_list.dart +++ b/playground/frontend/lib/modules/examples/components/example_list/example_list.dart @@ -18,15 +18,15 @@ import 'package:flutter/material.dart'; import 'package:playground/modules/examples/components/examples_components.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; import 'package:playground/pages/playground/states/example_selector_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class ExampleList extends StatelessWidget { final ScrollController controller; final AnimationController animationController; final OverlayEntry? dropdown; - final ExampleModel selectedExample; + final ExampleBase selectedExample; const ExampleList({ Key? key, @@ -50,7 +50,7 @@ class ExampleList extends StatelessWidget { itemCount: state.categories.length, itemBuilder: (context, index) => CategoryExpansionPanel( selectedExample: selectedExample, - categoryName: state.categories[index].name, + categoryName: state.categories[index].title, examples: state.categories[index].examples, animationController: animationController, dropdown: dropdown, diff --git a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart index f212f3c07e4c..b035e35d2d99 100644 --- a/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart +++ b/playground/frontend/lib/modules/examples/components/example_list/expansion_panel_item.dart @@ -20,14 +20,12 @@ import 'package:flutter/material.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; import 'package:playground/modules/examples/components/example_list/example_item_actions.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class ExpansionPanelItem extends StatelessWidget { - final ExampleModel example; - final ExampleModel selectedExample; + final ExampleBase example; + final ExampleBase selectedExample; final AnimationController animationController; final OverlayEntry? dropdown; @@ -41,20 +39,21 @@ class ExpansionPanelItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, playgroundState, child) => MouseRegion( + return Consumer( + builder: (context, controller, child) => MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () async { - if (playgroundState.selectedExample != example) { - _closeDropdown(playgroundState.exampleState); + if (controller.selectedExample != example) { + _closeDropdown(controller.exampleCache); AnalyticsService.get(context).trackSelectExample(example); final exampleWithInfo = - await playgroundState.exampleState.loadExampleInfo(example); + await controller.exampleCache.loadExampleInfo(example); // TODO: setCurrentSdk = false when we do // per-SDK output and run status. // Now using true to reset the output and run status. - playgroundState.setExample(exampleWithInfo, setCurrentSdk: true); + // https://github.com/apache/beam/issues/23248 + controller.setExample(exampleWithInfo, setCurrentSdk: true); } }, child: Container( @@ -83,9 +82,9 @@ class ExpansionPanelItem extends StatelessWidget { ); } - void _closeDropdown(ExampleState exampleState) { + void _closeDropdown(ExampleCache exampleCache) { animationController.reverse(); dropdown?.remove(); - exampleState.changeSelectorVisibility(); + exampleCache.changeSelectorVisibility(); } } diff --git a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart b/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart index aabe00735ee6..849f19db877c 100644 --- a/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart +++ b/playground/frontend/lib/modules/examples/components/filter/category_bubble.dart @@ -17,10 +17,8 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; import 'package:playground/pages/playground/states/example_selector_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class CategoryBubble extends StatelessWidget { @@ -35,46 +33,21 @@ class CategoryBubble extends StatelessWidget { @override Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: Padding( - padding: const EdgeInsets.only(right: kMdSpacing), - child: Consumer( - builder: (context, state, child) { - final isSelected = type == state.selectedFilterType; + return Consumer( + builder: (context, state, child) { + final isSelected = type == state.selectedFilterType; - return GestureDetector( - onTap: () { - if (!isSelected) { - state.setSelectedFilterType(type); - state.sortCategories(); - } - }, - child: Container( - height: kContainerHeight, - padding: const EdgeInsets.symmetric(horizontal: kXlSpacing), - decoration: BoxDecoration( - color: isSelected - ? ThemeColors.of(context).primary - : ThemeColors.of(context).lightGreyColor, - borderRadius: BorderRadius.circular(kXlBorderRadius), - ), - child: Center( - child: Text( - name, - style: TextStyle( - color: isSelected - ? ThemeColors.of(context).primaryBackgroundTextColor - : ThemeColors.of(context) - .lightGreyBackgroundTextColor, - ), - ), - ), - ), - ); + return BubbleWidget( + isSelected: isSelected, + title: name, + onTap: () { + if (!isSelected) { + state.setSelectedFilterType(type); + state.sortCategories(); + } }, - ), - ), + ); + }, ); } } diff --git a/playground/frontend/lib/modules/examples/components/filter/type_filter.dart b/playground/frontend/lib/modules/examples/components/filter/type_filter.dart index c56f6fcde415..a221999f3ce6 100644 --- a/playground/frontend/lib/modules/examples/components/filter/type_filter.dart +++ b/playground/frontend/lib/modules/examples/components/filter/type_filter.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/examples/components/examples_components.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'package:playground_components/playground_components.dart'; class TypeFilter extends StatelessWidget { const TypeFilter({Key? key}) : super(key: key); diff --git a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart index 6d90bc7bfd56..c4fb93fa4706 100644 --- a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart +++ b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover.dart @@ -22,13 +22,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground/constants/assets.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:url_launcher/url_launcher.dart'; const kMultifileWidth = 300.0; class MultifilePopover extends StatelessWidget { - final ExampleModel example; + final ExampleBase example; const MultifilePopover({Key? key, required this.example}) : super(key: key); diff --git a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart index 069830c65c76..d530aa645ebe 100644 --- a/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart +++ b/playground/frontend/lib/modules/examples/components/multifile_popover/multifile_popover_button.dart @@ -23,11 +23,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground/constants/assets.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/examples/components/multifile_popover/multifile_popover.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'package:playground_components/playground_components.dart'; class MultifilePopoverButton extends StatelessWidget { final BuildContext? parentContext; - final ExampleModel example; + final ExampleBase example; final Alignment followerAnchor; final Alignment targetAnchor; final void Function()? onOpen; @@ -67,7 +67,7 @@ class MultifilePopoverButton extends StatelessWidget { void _showMultifilePopover( BuildContext context, - ExampleModel example, + ExampleBase example, Alignment followerAnchor, Alignment targetAnchor, ) async { diff --git a/playground/frontend/lib/modules/examples/components/search_field/search_field.dart b/playground/frontend/lib/modules/examples/components/search_field/search_field.dart index 80939ac15554..2095dfac1d23 100644 --- a/playground/frontend/lib/modules/examples/components/search_field/search_field.dart +++ b/playground/frontend/lib/modules/examples/components/search_field/search_field.dart @@ -18,9 +18,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/pages/playground/states/example_selector_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; const double kContainerWidth = 376.0; @@ -34,8 +34,11 @@ class SearchField extends StatelessWidget { @override Widget build(BuildContext context) { + final borderColor = + Theme.of(context).extension()!.borderColor; + final OutlineInputBorder border = OutlineInputBorder( - borderSide: BorderSide(color: ThemeColors.of(context).lightGreyColor), + borderSide: BorderSide(color: borderColor), borderRadius: BorderRadius.circular(kMdBorderRadius), ); @@ -61,7 +64,7 @@ class SearchField extends StatelessWidget { ), child: Icon( Icons.search, - color: ThemeColors.of(context).lightGreyColor, + color: borderColor, size: kIconSizeMd, ), ), @@ -72,7 +75,7 @@ class SearchField extends StatelessWidget { hintText: AppLocalizations.of(context)!.search, contentPadding: const EdgeInsets.only(left: kLgSpacing), ), - cursorColor: ThemeColors.of(context).lightGreyColor, + cursorColor: borderColor, cursorWidth: kCursorSize, textAlignVertical: TextAlignVertical.center, onFieldSubmitted: (String filterText) => diff --git a/playground/frontend/lib/modules/examples/example_selector.dart b/playground/frontend/lib/modules/examples/example_selector.dart index f65c45bfacfd..c08fdb3cea2d 100644 --- a/playground/frontend/lib/modules/examples/example_selector.dart +++ b/playground/frontend/lib/modules/examples/example_selector.dart @@ -18,18 +18,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/components/horizontal_divider/horizontal_divider.dart'; -import 'package:playground/components/loading_indicator/loading_indicator.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/links.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/examples/components/examples_components.dart'; import 'package:playground/modules/examples/components/outside_click_handler.dart'; import 'package:playground/modules/examples/models/popover_state.dart'; import 'package:playground/pages/playground/states/example_selector_state.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; import 'package:playground/utils/dropdown_utils.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -90,10 +86,10 @@ class _ExampleSelectorState extends State return Container( height: kContainerHeight, decoration: BoxDecoration( - color: ThemeColors.of(context).dropdownButton, + color: Theme.of(context).dividerColor, borderRadius: BorderRadius.circular(kSmBorderRadius), ), - child: Consumer( + child: Consumer( builder: (context, state, child) => TextButton( key: selectorKey, onPressed: () { @@ -111,7 +107,7 @@ class _ExampleSelectorState extends State alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ - Consumer( + Consumer( builder: (context, state, child) => Text(state.examplesTitle), ), const Icon(Icons.keyboard_arrow_down), @@ -130,12 +126,12 @@ class _ExampleSelectorState extends State return ChangeNotifierProvider( create: (context) => PopoverState(false), builder: (context, state) { - return Consumer( - builder: (context, playgroundState, child) => Stack( + return Consumer( + builder: (context, playgroundController, child) => Stack( children: [ OutsideClickHandler( onTap: () { - _closeDropdown(playgroundState.exampleState); + _closeDropdown(playgroundController.exampleCache); // handle description dialogs Navigator.of(context, rootNavigator: true) .popUntil((route) { @@ -145,9 +141,9 @@ class _ExampleSelectorState extends State ), ChangeNotifierProvider( create: (context) => ExampleSelectorState( - playgroundState, - playgroundState.exampleState - .getCategories(playgroundState.sdk), + playgroundController, + playgroundController.exampleCache + .getCategories(playgroundController.sdk), ), builder: (context, _) => Positioned( left: dropdownOffset.dx, @@ -166,7 +162,7 @@ class _ExampleSelectorState extends State ), child: _buildDropdownContent( context, - playgroundState, + playgroundController, ), ), ), @@ -184,13 +180,11 @@ class _ExampleSelectorState extends State Widget _buildDropdownContent( BuildContext context, - PlaygroundState playgroundState, + PlaygroundController playgroundController, ) { - if (playgroundState.exampleState.sdkCategories == null || - playgroundState.selectedExample == null) { - return const LoadingIndicator( - size: kMdLoadingIndicatorSize, - ); + if (playgroundController.exampleCache.categoryListsBySdk.isEmpty || + playgroundController.selectedExample == null) { + return const LoadingIndicator(); } return Column( @@ -199,11 +193,11 @@ class _ExampleSelectorState extends State const TypeFilter(), ExampleList( controller: scrollController, - selectedExample: playgroundState.selectedExample!, + selectedExample: playgroundController.selectedExample!, animationController: animationController, dropdown: examplesDropdown, ), - const HorizontalDivider(indent: kLgSpacing), + const BeamDivider(), SizedBox( width: double.infinity, child: TextButton( @@ -213,7 +207,7 @@ class _ExampleSelectorState extends State alignment: Alignment.centerLeft, child: Text( AppLocalizations.of(context)!.addExample, - style: TextStyle(color: ThemeColors.of(context).primary), + style: TextStyle(color: Theme.of(context).primaryColor), ), ), ), @@ -224,9 +218,9 @@ class _ExampleSelectorState extends State ); } - void _closeDropdown(ExampleState exampleState) { + void _closeDropdown(ExampleCache exampleCache) { animationController.reverse(); examplesDropdown?.remove(); - exampleState.changeSelectorVisibility(); + exampleCache.changeSelectorVisibility(); } } diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart deleted file mode 100644 index 5841e0e294c6..000000000000 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_origin.dart'; - -class CatalogDefaultExampleLoadingDescriptor extends ExampleLoadingDescriptor { - final SDK sdk; - - const CatalogDefaultExampleLoadingDescriptor({ - required this.sdk, - }); - - @override - ExampleOrigin get origin => ExampleOrigin.catalogDefault; - - @override - int get hashCode => sdk.hashCode; - - @override - bool operator ==(Object other) { - return other is CatalogDefaultExampleLoadingDescriptor && sdk == other.sdk; - } - - // Only ContentExampleLoadingDescriptor is serialized now. - @override - Map toJson() => throw UnimplementedError(); -} diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart index ca9199f12855..7b3854d6f31f 100644 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart +++ b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart @@ -19,18 +19,11 @@ import 'dart:convert'; import 'package:playground/constants/params.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart'; import 'package:playground/modules/examples/models/example_token_type.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; class ExamplesLoadingDescriptorFactory { - static const _defaultSdk = SDK.java; + static const _defaultSdk = Sdk.java; static ExamplesLoadingDescriptor fromUriParts({ required String path, @@ -61,7 +54,7 @@ class ExamplesLoadingDescriptorFactory { return null; } - final sdk = SDK.tryParse(params[kSdkParam]); + final sdk = Sdk.tryParse(params[kSdkParam]); return ExamplesLoadingDescriptor( descriptors: _parseMultipleInstantExamples(list, sdk), @@ -75,7 +68,7 @@ class ExamplesLoadingDescriptorFactory { static List _parseMultipleInstantExamples( List list, - SDK? sdk, + Sdk? sdk, ) { final result = []; @@ -132,7 +125,7 @@ class ExamplesLoadingDescriptorFactory { return null; } - final sdk = SDK.tryParse(params[kSdkParam]) ?? _defaultSdk; + final sdk = Sdk.tryParse(params[kSdkParam]) ?? _defaultSdk; return ExamplesLoadingDescriptor( descriptors: [ @@ -161,14 +154,14 @@ class ExamplesLoadingDescriptorFactory { return ExamplesLoadingDescriptor( descriptors: [ EmptyExampleLoadingDescriptor( - sdk: SDK.tryParse(params[kSdkParam]) ?? _defaultSdk, + sdk: Sdk.tryParse(params[kSdkParam]) ?? _defaultSdk, ), ], lazyLoadDescriptors: _emptyLazyLoadDescriptors, ); } - static Map> _getLazyLoadDescriptors() { + static Map> _getLazyLoadDescriptors() { if (isEmbedded()) { return _emptyLazyLoadDescriptors; } @@ -176,18 +169,18 @@ class ExamplesLoadingDescriptorFactory { return _defaultLazyLoadDescriptors; } - static Map> + static Map> get _emptyLazyLoadDescriptors { return { - for (final sdk in SDK.values) + for (final sdk in Sdk.known) sdk: [EmptyExampleLoadingDescriptor(sdk: sdk)] }; } - static Map> + static Map> get _defaultLazyLoadDescriptors { return { - for (final sdk in SDK.values) + for (final sdk in Sdk.known) sdk: [CatalogDefaultExampleLoadingDescriptor(sdk: sdk)] }; } diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart deleted file mode 100644 index a5adbff51ac6..000000000000 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_origin.dart'; - -class StandardExampleLoadingDescriptor extends ExampleLoadingDescriptor { - final String path; - - const StandardExampleLoadingDescriptor({ - required this.path, - }); - - @override - ExampleOrigin get origin => ExampleOrigin.standard; - - @override - String toString() => '$origin-$path'; - - @override - int get hashCode => path.hashCode; - - @override - bool operator ==(Object other) { - return other is StandardExampleLoadingDescriptor && path == other.path; - } - - // Only ContentExampleLoadingDescriptor is serialized now. - @override - Map toJson() => throw UnimplementedError(); -} diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart b/playground/frontend/lib/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart deleted file mode 100644 index 26e58a8453fb..000000000000 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_origin.dart'; - -class UserSharedExampleLoadingDescriptor extends ExampleLoadingDescriptor { - final String snippetId; - - const UserSharedExampleLoadingDescriptor({ - required this.snippetId, - }); - - @override - ExampleOrigin get origin => ExampleOrigin.userShared; - - @override - String toString() => '$origin-$snippetId'; - - @override - int get hashCode => snippetId.hashCode; - - @override - bool operator ==(Object other) { - return other is UserSharedExampleLoadingDescriptor && - snippetId == other.snippetId; - } - - // Only ContentExampleLoadingDescriptor is serialized now. - @override - Map toJson() => throw UnimplementedError(); -} diff --git a/playground/frontend/lib/modules/examples/models/example_token_type.dart b/playground/frontend/lib/modules/examples/models/example_token_type.dart index 789b1c3a179a..48c7c5806358 100644 --- a/playground/frontend/lib/modules/examples/models/example_token_type.dart +++ b/playground/frontend/lib/modules/examples/models/example_token_type.dart @@ -16,7 +16,7 @@ * limitations under the License. */ -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; enum ExampleTokenType { standard, @@ -24,7 +24,7 @@ enum ExampleTokenType { ; static ExampleTokenType fromToken(String token) { - final sdk = SDK.tryParseExamplePath(token); + final sdk = Sdk.tryParseExamplePath(token); if (sdk != null) { return standard; } diff --git a/playground/frontend/lib/modules/examples/repositories/example_client/example_client.dart b/playground/frontend/lib/modules/examples/repositories/example_client/example_client.dart deleted file mode 100644 index 039a567c5efa..000000000000 --- a/playground/frontend/lib/modules/examples/repositories/example_client/example_client.dart +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_code_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_response.dart'; -import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart'; -import 'package:playground/modules/examples/repositories/models/save_snippet_response.dart'; - -abstract class ExampleClient { - Future getListOfExamples( - GetListOfExamplesRequestWrapper request, - ); - - Future getExampleSource( - GetExampleRequestWrapper request, - ); - - Future getDefaultExample( - GetExampleRequestWrapper request, - ); - - Future getExample( - GetExampleRequestWrapper request, - ); - - Future getExampleOutput( - GetExampleRequestWrapper request, - ); - - Future getExampleLogs( - GetExampleRequestWrapper request, - ); - - Future getExampleGraph( - GetExampleRequestWrapper request, - ); - - Future getSnippet( - GetSnippetRequestWrapper request, - ); - - Future saveSnippet( - SaveSnippetRequestWrapper request, - ); -} diff --git a/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart b/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart deleted file mode 100644 index f4a11be58aae..000000000000 --- a/playground/frontend/lib/modules/examples/repositories/example_client/grpc_example_client.dart +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:grpc/grpc_web.dart'; -import 'package:playground/api/iis_workaround_channel.dart'; -import 'package:playground/api/v1/api.pbgrpc.dart' as grpc; -import 'package:playground/config.g.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart'; -import 'package:playground/modules/examples/models/category_model.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/examples/repositories/example_client/example_client.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_code_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart'; -import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart'; -import 'package:playground/modules/examples/repositories/models/save_snippet_response.dart'; -import 'package:playground/modules/examples/repositories/models/shared_file_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/utils/replace_incorrect_symbols.dart'; - -class GrpcExampleClient implements ExampleClient { - late final grpc.PlaygroundServiceClient _defaultClient; - - GrpcExampleClient() { - final channel = IisWorkaroundChannel.xhr( - Uri.parse(kApiClientURL), - ); - _defaultClient = grpc.PlaygroundServiceClient(channel); - } - - @override - Future getListOfExamples( - GetListOfExamplesRequestWrapper request, - ) { - return _runSafely( - () => _defaultClient - .getPrecompiledObjects( - _getListOfExamplesRequestToGrpcRequest(request)) - .then((response) => GetListOfExampleResponse( - _toClientCategories(response.sdkCategories))), - ); - } - - @override - Future getDefaultExample( - GetExampleRequestWrapper request, - ) { - return _runSafely( - () => _defaultClient - .getDefaultPrecompiledObject( - _getDefaultExampleRequestToGrpcRequest(request)) - .then( - (response) => GetExampleResponse( - _toExampleModel( - request.sdk, - response.precompiledObject, - ), - ), - ), - ); - } - - @override - Future getExample( - GetExampleRequestWrapper request, - ) { - return _runSafely( - () => _defaultClient - .getPrecompiledObject( - grpc.GetPrecompiledObjectRequest()..cloudPath = request.path) - .then( - (response) => GetExampleResponse( - _toExampleModel( - request.sdk, - response.precompiledObject, - ), - ), - ), - ); - } - - @override - Future getExampleSource( - GetExampleRequestWrapper request) { - return _runSafely( - () => _defaultClient - .getPrecompiledObjectCode( - _getExampleCodeRequestToGrpcRequest(request)) - .then((response) => - GetExampleCodeResponse(replaceIncorrectSymbols(response.code))), - ); - } - - @override - Future getExampleOutput(GetExampleRequestWrapper request) { - return _runSafely( - () => _defaultClient - .getPrecompiledObjectOutput( - _getExampleOutputRequestToGrpcRequest(request)) - .then((response) => - OutputResponse(replaceIncorrectSymbols(response.output))) - .catchError((err) { - print(err); - return OutputResponse(''); - }), - ); - } - - @override - Future getExampleLogs(GetExampleRequestWrapper request) { - return _runSafely( - () => _defaultClient - .getPrecompiledObjectLogs(_getExampleLogRequestToGrpcRequest(request)) - .then((response) => - OutputResponse(replaceIncorrectSymbols(response.output))) - .catchError((err) { - print(err); - return OutputResponse(''); - }), - ); - } - - @override - Future getExampleGraph(GetExampleRequestWrapper request) { - return _runSafely( - () => _defaultClient - .getPrecompiledObjectGraph( - _getExampleGraphRequestToGrpcRequest(request)) - .then((response) => OutputResponse(response.graph)) - .catchError((err) { - print(err); - return OutputResponse(''); - }), - ); - } - - @override - Future getSnippet( - GetSnippetRequestWrapper request, - ) { - return _runSafely( - () => _defaultClient - .getSnippet(_getSnippetRequestToGrpcRequest(request)) - .then( - (response) => GetSnippetResponse( - files: _convertToSharedFileList(response.files), - sdk: _getAppSdk(response.sdk), - pipelineOptions: response.pipelineOptions, - ), - ), - ); - } - - @override - Future saveSnippet( - SaveSnippetRequestWrapper request, - ) { - return _runSafely( - () => _defaultClient - .saveSnippet(_saveSnippetRequestToGrpcRequest(request)) - .then( - (response) => SaveSnippetResponse( - id: response.id, - ), - ), - ); - } - - Future _runSafely(Future Function() invoke) { - try { - return invoke(); - } on GrpcError catch (error) { - throw Exception(error.message); - } - } - - grpc.GetPrecompiledObjectsRequest _getListOfExamplesRequestToGrpcRequest( - GetListOfExamplesRequestWrapper request, - ) { - return grpc.GetPrecompiledObjectsRequest() - ..category = request.category ?? '' - ..sdk = request.sdk == null - ? grpc.Sdk.SDK_UNSPECIFIED - : _getGrpcSdk(request.sdk!); - } - - grpc.GetDefaultPrecompiledObjectRequest - _getDefaultExampleRequestToGrpcRequest( - GetExampleRequestWrapper request, - ) { - return grpc.GetDefaultPrecompiledObjectRequest() - ..sdk = _getGrpcSdk(request.sdk); - } - - grpc.GetPrecompiledObjectCodeRequest _getExampleCodeRequestToGrpcRequest( - GetExampleRequestWrapper request, - ) { - return grpc.GetPrecompiledObjectCodeRequest()..cloudPath = request.path; - } - - grpc.GetPrecompiledObjectOutputRequest _getExampleOutputRequestToGrpcRequest( - GetExampleRequestWrapper request, - ) { - return grpc.GetPrecompiledObjectOutputRequest()..cloudPath = request.path; - } - - grpc.GetPrecompiledObjectLogsRequest _getExampleLogRequestToGrpcRequest( - GetExampleRequestWrapper request, - ) { - return grpc.GetPrecompiledObjectLogsRequest()..cloudPath = request.path; - } - - grpc.GetPrecompiledObjectGraphRequest _getExampleGraphRequestToGrpcRequest( - GetExampleRequestWrapper request, - ) { - return grpc.GetPrecompiledObjectGraphRequest()..cloudPath = request.path; - } - - grpc.GetSnippetRequest _getSnippetRequestToGrpcRequest( - GetSnippetRequestWrapper request, - ) { - return grpc.GetSnippetRequest()..id = request.id; - } - - grpc.SaveSnippetRequest _saveSnippetRequestToGrpcRequest( - SaveSnippetRequestWrapper request, - ) { - return grpc.SaveSnippetRequest() - ..sdk = _getGrpcSdk(request.sdk) - ..pipelineOptions = request.pipelineOptions - ..files.addAll(_convertToSnippetFileList(request.files)); - } - - grpc.Sdk _getGrpcSdk(SDK sdk) { - switch (sdk) { - case SDK.java: - return grpc.Sdk.SDK_JAVA; - case SDK.go: - return grpc.Sdk.SDK_GO; - case SDK.python: - return grpc.Sdk.SDK_PYTHON; - case SDK.scio: - return grpc.Sdk.SDK_SCIO; - } - } - - SDK _getAppSdk(grpc.Sdk sdk) { - switch (sdk) { - case grpc.Sdk.SDK_JAVA: - return SDK.java; - case grpc.Sdk.SDK_GO: - return SDK.go; - case grpc.Sdk.SDK_PYTHON: - return SDK.python; - case grpc.Sdk.SDK_SCIO: - return SDK.scio; - default: - return SDK.java; - } - } - - ExampleType _exampleTypeFromString(grpc.PrecompiledObjectType type) { - switch (type) { - case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_EXAMPLE: - return ExampleType.example; - case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_KATA: - return ExampleType.kata; - case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_UNIT_TEST: - return ExampleType.test; - case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_UNSPECIFIED: - return ExampleType.all; - default: - return ExampleType.example; - } - } - - Map> _toClientCategories( - List response, - ) { - Map> sdkCategoriesMap = {}; - List>> entries = []; - for (var sdkMap in response) { - SDK sdk = _getAppSdk(sdkMap.sdk); - List categoriesForSdk = []; - for (var category in sdkMap.categories) { - List examples = category.precompiledObjects - .map((example) => _toExampleModel(sdk, example)) - .toList() - ..sort(); - categoriesForSdk.add(CategoryModel( - name: category.categoryName, - examples: examples, - )); - } - entries.add(MapEntry(sdk, categoriesForSdk..sort())); - } - sdkCategoriesMap.addEntries(entries); - return sdkCategoriesMap; - } - - ExampleModel _toExampleModel(SDK sdk, grpc.PrecompiledObject example) { - return ExampleModel( - sdk: sdk, - name: example.name, - description: example.description, - type: _exampleTypeFromString(example.type), - path: example.cloudPath, - contextLine: example.contextLine, - pipelineOptions: example.pipelineOptions, - isMultiFile: example.multifile, - link: example.link, - ); - } - - List _convertToSharedFileList( - List snippetFileList, - ) { - final sharedFilesList = []; - - for (grpc.SnippetFile item in snippetFileList) { - sharedFilesList.add(SharedFile( - code: item.content, - isMain: item.isMain, - name: item.name, - )); - } - - return sharedFilesList; - } - - List _convertToSnippetFileList( - List sharedFilesList, - ) { - final snippetFileList = []; - - for (SharedFile item in sharedFilesList) { - snippetFileList.add( - grpc.SnippetFile() - ..name = item.name - ..isMain = true - ..content = item.code, - ); - } - - return snippetFileList; - } -} diff --git a/playground/frontend/lib/modules/examples/repositories/example_repository.dart b/playground/frontend/lib/modules/examples/repositories/example_repository.dart deleted file mode 100644 index 65c09f885fba..000000000000 --- a/playground/frontend/lib/modules/examples/repositories/example_repository.dart +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/examples/models/category_model.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/examples/repositories/example_client/example_client.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart'; -import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; - -class ExampleRepository { - late final ExampleClient _client; - - ExampleRepository(ExampleClient client) { - _client = client; - } - - Future>> getListOfExamples( - GetListOfExamplesRequestWrapper request, - ) async { - final result = await _client.getListOfExamples(request); - return result.categories; - } - - Future getDefaultExample( - GetExampleRequestWrapper request, - ) async { - final result = await _client.getDefaultExample(request); - return result.example; - } - - Future getExampleSource( - GetExampleRequestWrapper request, - ) async { - final result = await _client.getExampleSource(request); - return result.code; - } - - Future getExampleOutput( - GetExampleRequestWrapper request, - ) async { - final result = await _client.getExampleOutput(request); - return result.output; - } - - Future getExampleLogs( - GetExampleRequestWrapper request, - ) async { - final result = await _client.getExampleLogs(request); - return result.output; - } - - Future getExampleGraph( - GetExampleRequestWrapper request, - ) async { - final result = await _client.getExampleGraph(request); - return result.output; - } - - Future getExample( - GetExampleRequestWrapper request, - ) async { - final result = await _client.getExample(request); - return result.example; - } - - Future getSnippet( - GetSnippetRequestWrapper request, - ) async { - final result = await _client.getSnippet(request); - return result; - } - - Future saveSnippet( - SaveSnippetRequestWrapper request, - ) async { - final result = await _client.saveSnippet(request); - return result.id; - } -} diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_example_request.dart b/playground/frontend/lib/modules/examples/repositories/models/get_example_request.dart deleted file mode 100644 index da87f09b2881..000000000000 --- a/playground/frontend/lib/modules/examples/repositories/models/get_example_request.dart +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/cupertino.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; - -class GetExampleRequestWrapper { - final String path; - final SDK sdk; - - GetExampleRequestWrapper(this.path, this.sdk); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is GetExampleRequestWrapper && - path == other.path && - sdk == other.sdk; - - @override - int get hashCode => hashValues(path.hashCode, sdk.hashCode); -} diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_request.dart b/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_request.dart deleted file mode 100644 index 0d39df522d5c..000000000000 --- a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_request.dart +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/cupertino.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; - -class GetListOfExamplesRequestWrapper { - final SDK? sdk; - final String? category; - - GetListOfExamplesRequestWrapper({required this.sdk, required this.category}); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is GetListOfExamplesRequestWrapper && - category == other.category && - sdk == other.sdk; - - @override - int get hashCode => hashValues(category.hashCode, sdk.hashCode); -} diff --git a/playground/frontend/lib/modules/messages/handlers/messages_handler.dart b/playground/frontend/lib/modules/messages/handlers/messages_handler.dart index 3a4047afbb52..12538d7daecb 100644 --- a/playground/frontend/lib/modules/messages/handlers/messages_handler.dart +++ b/playground/frontend/lib/modules/messages/handlers/messages_handler.dart @@ -20,16 +20,16 @@ import 'package:playground/modules/messages/handlers/abstract_message_handler.da import 'package:playground/modules/messages/handlers/set_content_message_handler.dart'; import 'package:playground/modules/messages/handlers/set_sdk_message_handler.dart'; import 'package:playground/modules/messages/models/abstract_message.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; class MessagesHandler extends AbstractMessageHandler { final List handlers; MessagesHandler({ - required PlaygroundState playgroundState, + required PlaygroundController playgroundController, }) : handlers = [ - SetContentMessageHandler(playgroundState: playgroundState), - SetSdkMessageHandler(playgroundState: playgroundState), + SetContentMessageHandler(playgroundController: playgroundController), + SetSdkMessageHandler(playgroundController: playgroundController), ]; @override diff --git a/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart b/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart index 1e16e397f6e0..c2424b38af64 100644 --- a/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart +++ b/playground/frontend/lib/modules/messages/handlers/set_content_message_handler.dart @@ -19,13 +19,13 @@ import 'package:playground/modules/messages/handlers/abstract_message_handler.dart'; import 'package:playground/modules/messages/models/abstract_message.dart'; import 'package:playground/modules/messages/models/set_content_message.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; class SetContentMessageHandler extends AbstractMessageHandler { - final PlaygroundState playgroundState; + final PlaygroundController playgroundController; const SetContentMessageHandler({ - required this.playgroundState, + required this.playgroundController, }); @override @@ -39,6 +39,6 @@ class SetContentMessageHandler extends AbstractMessageHandler { } void _handle(SetContentMessage message) { - playgroundState.examplesLoader.load(message.descriptor); + playgroundController.examplesLoader.load(message.descriptor); } } diff --git a/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart b/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart index 9a2595f9215c..2d066a9e8462 100644 --- a/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart +++ b/playground/frontend/lib/modules/messages/handlers/set_sdk_message_handler.dart @@ -19,13 +19,13 @@ import 'package:playground/modules/messages/handlers/abstract_message_handler.dart'; import 'package:playground/modules/messages/models/abstract_message.dart'; import 'package:playground/modules/messages/models/set_sdk_message.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; class SetSdkMessageHandler extends AbstractMessageHandler { - final PlaygroundState playgroundState; + final PlaygroundController playgroundController; const SetSdkMessageHandler({ - required this.playgroundState, + required this.playgroundController, }); @override @@ -39,6 +39,6 @@ class SetSdkMessageHandler extends AbstractMessageHandler { } void _handle(SetSdkMessage message) { - playgroundState.setSdk(message.sdk); + playgroundController.setSdk(message.sdk); } } diff --git a/playground/frontend/lib/modules/messages/models/set_content_message.dart b/playground/frontend/lib/modules/messages/models/set_content_message.dart index 707bf5deb68b..fd8e1a04ba8f 100644 --- a/playground/frontend/lib/modules/messages/models/set_content_message.dart +++ b/playground/frontend/lib/modules/messages/models/set_content_message.dart @@ -16,9 +16,9 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart'; import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart'; import 'package:playground/modules/messages/models/abstract_message.dart'; +import 'package:playground_components/playground_components.dart'; /// A message that sets content for multiple snippets. /// diff --git a/playground/frontend/lib/modules/messages/models/set_sdk_message.dart b/playground/frontend/lib/modules/messages/models/set_sdk_message.dart index ed195fd8013c..af3841afadfb 100644 --- a/playground/frontend/lib/modules/messages/models/set_sdk_message.dart +++ b/playground/frontend/lib/modules/messages/models/set_sdk_message.dart @@ -17,13 +17,13 @@ */ import 'package:playground/modules/messages/models/abstract_message.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; /// A message that switches the SDK. /// /// Sent to iframes by Beam documentation HTML when the language is switched. class SetSdkMessage extends AbstractMessage { - final SDK sdk; + final Sdk sdk; static const type = 'SetSdk'; @@ -36,7 +36,7 @@ class SetSdkMessage extends AbstractMessage { return null; } - final sdk = SDK.tryParse(map['sdk']); + final sdk = Sdk.tryParse(map['sdk']); if (sdk == null) { return null; } @@ -63,7 +63,7 @@ class SetSdkMessage extends AbstractMessage { @override Map toJson() { return { - 'sdk': sdk.name, + 'sdk': sdk.id, }; } } diff --git a/playground/frontend/lib/modules/output/components/output_area.dart b/playground/frontend/lib/modules/output/components/output_area.dart deleted file mode 100644 index 1492124ef356..000000000000 --- a/playground/frontend/lib/modules/output/components/output_area.dart +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:playground/modules/graph/graph_builder/painters/graph_painter.dart'; -import 'package:playground/modules/output/components/graph.dart'; -import 'package:playground/modules/output/components/output_result.dart'; -import 'package:playground/modules/output/models/output_placement.dart'; -import 'package:playground/modules/output/models/output_placement_state.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; -import 'package:provider/provider.dart'; - -class OutputArea extends StatelessWidget { - final TabController tabController; - final bool showGraph; - - const OutputArea({ - Key? key, - required this.tabController, - required this.showGraph, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - color: Theme.of(context).backgroundColor, - child: Consumer2( - builder: (context, playgroundState, placementState, child) { - final sdk = playgroundState.sdk; - - return TabBarView( - controller: tabController, - physics: const NeverScrollableScrollPhysics(), - children: [ - OutputResult( - text: playgroundState.outputResult, - isSelected: tabController.index == 0, - ), - if (showGraph) - sdk == null - ? Container() - : GraphTab( - graph: playgroundState.result?.graph ?? '', - sdk: sdk, - direction: _getGraphDirection(placementState.placement), - ), - ], - ); - }, - ), - ); - } - - GraphDirection _getGraphDirection(OutputPlacement placement) { - return placement == OutputPlacement.bottom - ? GraphDirection.horizontal - : GraphDirection.vertical; - } -} diff --git a/playground/frontend/lib/modules/output/components/output_header/result_filter_bubble.dart b/playground/frontend/lib/modules/output/components/output_header/result_filter_bubble.dart deleted file mode 100644 index d4401e775613..000000000000 --- a/playground/frontend/lib/modules/output/components/output_header/result_filter_bubble.dart +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/examples/models/outputs_model.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; -import 'package:provider/provider.dart'; - -class ResultFilterBubble extends StatelessWidget { - final OutputType type; - final String name; - - const ResultFilterBubble({ - Key? key, - required this.type, - required this.name, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: Padding( - padding: const EdgeInsets.only(right: kMdSpacing), - child: Consumer( - builder: (context, state, child) { - final isSelected = type == state.selectedOutputFilterType; - - return GestureDetector( - onTap: () { - if (!isSelected) { - state.setSelectedOutputFilterType(type); - state.filterOutput(type); - } - }, - child: Container( - height: kContainerHeight, - padding: const EdgeInsets.symmetric(horizontal: kXlSpacing), - decoration: BoxDecoration( - color: isSelected - ? ThemeColors.of(context).primary - : ThemeColors.of(context).lightGreyColor, - borderRadius: BorderRadius.circular(kXlBorderRadius), - ), - child: Center( - child: Text( - name, - style: TextStyle( - color: isSelected - ? ThemeColors.of(context).primaryBackgroundTextColor - : ThemeColors.of(context) - .lightGreyBackgroundTextColor, - ), - ), - ), - ), - ); - }, - ), - ), - ); - } -} diff --git a/playground/frontend/lib/modules/output/components/output_header/result_filter_popover.dart b/playground/frontend/lib/modules/output/components/output_header/result_filter_popover.dart deleted file mode 100644 index d93d79e5b1fc..000000000000 --- a/playground/frontend/lib/modules/output/components/output_header/result_filter_popover.dart +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/examples/models/outputs_model.dart'; -import 'package:playground/modules/output/components/output_header/result_filter_bubble.dart'; - -const kPopoverWidth = 240.0; -const kPopoverPadding = 50.0; - -class ResultFilterPopover extends StatelessWidget { - const ResultFilterPopover({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - AppLocalizations appLocale = AppLocalizations.of(context)!; - - return Padding( - padding: const EdgeInsets.only(top: kPopoverPadding), - child: SizedBox( - width: kPopoverWidth, - child: Card( - child: Padding( - padding: const EdgeInsets.all(kMdSpacing), - child: Wrap( - runSpacing: kMdSpacing, - children: [ - Text(appLocale.displayAtThisTab), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: kSmSpacing, - vertical: kSmSpacing, - ), - child: Row( - children: [ - ResultFilterBubble( - type: OutputType.all, - name: appLocale.all, - ), - ResultFilterBubble( - type: OutputType.log, - name: appLocale.log, - ), - ResultFilterBubble( - type: OutputType.output, - name: appLocale.output, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/playground/frontend/lib/modules/output/models/output_placement.dart b/playground/frontend/lib/modules/output/models/output_placement.dart index d15c9d36c384..642553751ccd 100644 --- a/playground/frontend/lib/modules/output/models/output_placement.dart +++ b/playground/frontend/lib/modules/output/models/output_placement.dart @@ -24,6 +24,13 @@ enum OutputPlacement { right, left, bottom, + ; + + Axis get graphDirection { + return this == OutputPlacement.bottom + ? Axis.horizontal + : Axis.vertical; + } } extension OutputPlacementToIcon on OutputPlacement { diff --git a/playground/frontend/lib/modules/sdk/components/sdk_selector.dart b/playground/frontend/lib/modules/sdk/components/sdk_selector.dart index c8197e8acbc3..c4c1656a1748 100644 --- a/playground/frontend/lib/modules/sdk/components/sdk_selector.dart +++ b/playground/frontend/lib/modules/sdk/components/sdk_selector.dart @@ -21,8 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground/components/dropdown_button/dropdown_button.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/sdk/components/sdk_selector_row.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; const kEmptyExampleName = 'Catalog'; @@ -31,8 +30,8 @@ const double kWidth = 150; const double kHeight = 172; class SDKSelector extends StatelessWidget { - final SDK? value; - final ValueChanged onChanged; + final Sdk? value; + final ValueChanged onChanged; const SDKSelector({ Key? key, @@ -45,35 +44,35 @@ class SDKSelector extends StatelessWidget { final localizations = AppLocalizations.of(context)!; final text = value == null ? localizations.selectSdkPlaceholder - : 'SDK: ${value?.displayName}'; + : 'SDK: ${value?.title}'; return Semantics( container: true, button: true, label: localizations.selectSdkDropdownSemantics, - child: AppDropdownButton( - buttonText: Text(text), - createDropdown: (close) => Column( - children: [ - const SizedBox(height: kMdSpacing), - ...SDK.values.map((SDK value) { - return SizedBox( - width: double.infinity, - child: Consumer( - builder: (context, state, child) => SdkSelectorRow( + child: Consumer( + builder: (context, controller, child) => AppDropdownButton( + buttonText: Text(text), + createDropdown: (close) => Column( + children: [ + const SizedBox(height: kMdSpacing), + ...Sdk.known.map((Sdk value) { + return SizedBox( + width: double.infinity, + child: SdkSelectorRow( sdk: value, onSelect: () { close(); onChanged(value); }, ), - ), - ); - }), - ], + ); + }), + ], + ), + width: kWidth, + height: kHeight, ), - width: kWidth, - height: kHeight, ), ); } diff --git a/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart b/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart index d66dda2dee7f..7993723bf25d 100644 --- a/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart +++ b/playground/frontend/lib/modules/sdk/components/sdk_selector_row.dart @@ -19,10 +19,10 @@ import 'package:flutter/material.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; class SdkSelectorRow extends StatelessWidget { - final SDK sdk; + final Sdk sdk; final VoidCallback onSelect; const SdkSelectorRow({ @@ -48,7 +48,7 @@ class SdkSelectorRow extends StatelessWidget { onPressed: onSelect, child: Padding( padding: const EdgeInsets.all(kLgSpacing), - child: Text(sdk.displayName), + child: Text(sdk.title), ), ); } diff --git a/playground/frontend/lib/modules/sdk/models/sdk.dart b/playground/frontend/lib/modules/sdk/models/sdk.dart deleted file mode 100644 index ef1fd6f57283..000000000000 --- a/playground/frontend/lib/modules/sdk/models/sdk.dart +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:highlight/highlight.dart'; -import 'package:highlight/languages/go.dart'; -import 'package:highlight/languages/java.dart'; -import 'package:highlight/languages/python.dart'; -import 'package:highlight/languages/scala.dart'; -import 'package:playground/config.g.dart'; - -enum SDK { - java, - go, - python, - scio, - ; - - /// A temporary solution while we wait for the backend to add - /// sdk in example responses. - static SDK? tryParseExamplePath(String? path) { - if (path == null) { - return null; - } - - if (path.startsWith('SDK_JAVA')) { - return java; - } - - if (path.startsWith('SDK_GO')) { - return go; - } - - if (path.startsWith('SDK_PYTHON')) { - return python; - } - - if (path.startsWith('SDK_SCIO')) { - return scio; - } - - return null; - } - - static SDK? tryParse(Object? value) { - if (value is! String) { - return null; - } - - try { - return values.byName(value); - } catch (ex) { - return null; - } - } -} - -extension SDKToString on SDK { - String get displayName { - switch (this) { - case SDK.go: - return 'Go'; - case SDK.java: - return 'Java'; - case SDK.python: - return 'Python'; - case SDK.scio: - return 'SCIO'; - } - } -} - -extension SdkToRoute on SDK { - String get getRoute { - switch (this) { - case SDK.java: - return kApiJavaClientURL; - case SDK.go: - return kApiGoClientURL; - case SDK.python: - return kApiPythonClientURL; - case SDK.scio: - return kApiScioClientURL; - default: - return ''; - } - } -} - -extension SdkToHighlightMode on SDK { - Mode get highlightMode { - switch (this) { - case SDK.java: - return java; - case SDK.go: - return go; - case SDK.python: - return python; - case SDK.scio: - return scala; - } - } -} diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart b/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart index 538c20cdecfa..27cd34357038 100644 --- a/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart +++ b/playground/frontend/lib/modules/shortcuts/components/shortcut_row.dart @@ -18,11 +18,10 @@ import 'package:flutter/material.dart'; import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/shortcuts/models/shortcut.dart'; -import 'package:playground/modules/shortcuts/utils/shortcuts_display_name.dart'; +import 'package:playground_components/playground_components.dart'; class ShortcutRow extends StatelessWidget { - final Shortcut shortcut; + final BeamShortcut shortcut; const ShortcutRow({Key? key, required this.shortcut}) : super(key: key); @@ -39,7 +38,7 @@ class ShortcutRow extends StatelessWidget { ), padding: const EdgeInsets.all(kMdSpacing), child: Text( - getShortcutDisplayName(shortcut), + shortcut.title, style: TextStyle(color: primaryColor), ), ), diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart b/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart index 32d3ce26151f..5a9f1bc18453 100644 --- a/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart +++ b/playground/frontend/lib/modules/shortcuts/components/shortcuts_manager.dart @@ -17,11 +17,11 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/modules/shortcuts/models/shortcut.dart'; +import 'package:playground_components/playground_components.dart'; class ShortcutsManager extends StatelessWidget { final Widget child; - final List shortcuts; + final List shortcuts; const ShortcutsManager({ Key? key, diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart b/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart index c92c2748756f..2c5ee6a775a0 100644 --- a/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart +++ b/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart @@ -16,12 +16,14 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/shortcuts/components/shortcut_row.dart'; import 'package:playground/modules/shortcuts/constants/global_shortcuts.dart'; +import 'package:playground_components/playground_components.dart'; const kButtonBorderRadius = 24.0; const kButtonWidth = 120.0; @@ -29,7 +31,11 @@ const kButtonHeight = 40.0; const kDialogPadding = 40.0; class ShortcutsModal extends StatelessWidget { - const ShortcutsModal({Key? key}) : super(key: key); + final PlaygroundController playgroundController; + + const ShortcutsModal({ + required this.playgroundController, + }); @override Widget build(BuildContext context) { @@ -50,7 +56,10 @@ class ShortcutsModal extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.start, runSpacing: kXlSpacing, children: [ - ...globalShortcuts.map( + ...[ + ...playgroundController.shortcuts, + ...globalShortcuts, + ].map( (shortcut) => Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -58,7 +67,8 @@ class ShortcutsModal extends StatelessWidget { Expanded( flex: 3, child: Text( - localize(context, shortcut.name), + shortcut.actionIntent.slug.tr(), + //localize(context, shortcut.name), style: const TextStyle(fontWeight: kBoldWeight), ), ), @@ -84,21 +94,4 @@ class ShortcutsModal extends StatelessWidget { ], ); } - - String localize(BuildContext context, String shortcutName) { - AppLocalizations appLocale = AppLocalizations.of(context)!; - - switch(shortcutName) { - case 'Run': - return appLocale.run; - case 'Reset': - return appLocale.reset; - case 'Clear Output': - return appLocale.clearOutput; - case 'New Example': - return appLocale.newExample; - default: - return shortcutName; - } - } } diff --git a/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart b/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart index bf3b14e7a73b..03797eaff3a0 100644 --- a/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart +++ b/playground/frontend/lib/modules/shortcuts/constants/global_shortcuts.dart @@ -18,93 +18,44 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:playground/modules/shortcuts/models/shortcut.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -const kRunText = 'Run'; -const kResetText = 'Reset'; -const kClearOutputText = 'Clear Output'; -const kNewExampleText = 'New Example'; - -class ResetIntent extends Intent { - const ResetIntent(); -} - -class RunIntent extends Intent { - const RunIntent(); +class ClearOutputIntent extends BeamIntent { + const ClearOutputIntent() : super(slug: 'intents.playground.clearOutput'); } -class ClearOutputIntent extends Intent { - const ClearOutputIntent(); +class NewExampleIntent extends BeamIntent { + const NewExampleIntent() : super(slug: 'intents.playground.newExample'); } -class NewExampleIntent extends Intent { - const NewExampleIntent(); -} - -final kRunShortcut = Shortcut( - shortcuts: LogicalKeySet( - LogicalKeyboardKey.meta, - LogicalKeyboardKey.enter, - ), - actionIntent: const RunIntent(), - name: kRunText, - createAction: (BuildContext context) => CallbackAction( - onInvoke: (_) => Provider.of( - context, - listen: false, - ).runCode(), - ), -); - -final kResetShortcut = Shortcut( - shortcuts: LogicalKeySet( - LogicalKeyboardKey.meta, - LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyE, - ), - actionIntent: const ResetIntent(), - name: kResetText, - createAction: (BuildContext context) => CallbackAction( - onInvoke: (_) => Provider.of( - context, - listen: false, - ).reset(), - ), -); - -final kClearOutputShortcut = Shortcut( +final kClearOutputShortcut = BeamShortcut( shortcuts: LogicalKeySet( LogicalKeyboardKey.meta, LogicalKeyboardKey.keyB, ), actionIntent: const ClearOutputIntent(), - name: kClearOutputText, createAction: (BuildContext context) => CallbackAction( - onInvoke: (_) => Provider.of( + onInvoke: (_) => Provider.of( context, listen: false, ).clearOutput(), ), ); -final kNewExampleShortcut = Shortcut( +final kNewExampleShortcut = BeamShortcut( shortcuts: LogicalKeySet( LogicalKeyboardKey.meta, LogicalKeyboardKey.keyM, ), actionIntent: const NewExampleIntent(), - name: kNewExampleText, createAction: (_) => CallbackAction( onInvoke: (_) => launchUrl(Uri.parse('/')), ), ); -final List globalShortcuts = [ - kRunShortcut, - kResetShortcut, +final List globalShortcuts = [ kClearOutputShortcut, kNewExampleShortcut, ]; diff --git a/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart index 5e8e56bf4faa..10ccf96ba4f8 100644 --- a/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart +++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_actions.dart @@ -27,8 +27,8 @@ import 'package:playground/constants/assets.dart'; import 'package:playground/constants/params.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/messages/models/set_content_message.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; import 'package:playground/utils/javascript_post_message.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; const kTryPlaygroundButtonWidth = 200.0; @@ -44,29 +44,29 @@ class EmbeddedActions extends StatelessWidget { child: SizedBox( width: kTryPlaygroundButtonWidth, height: kTryPlaygroundButtonHeight, - child: Consumer( - builder: (context, state, child) => ElevatedButton.icon( + child: Consumer( + builder: (context, controller, child) => ElevatedButton.icon( icon: SvgPicture.asset(kLinkIconAsset), label: Text(AppLocalizations.of(context)!.tryInPlayground), - onPressed: () => _openStandalonePlayground(state), + onPressed: () => _openStandalonePlayground(controller), ), ), ), ); } - void _openStandalonePlayground(PlaygroundState state) { + void _openStandalonePlayground(PlaygroundController controller) { // The empty list forces the parsing of EmptyExampleLoadingDescriptor // and prevents the glimpse of the default catalog example. final window = html.window.open( - '/?$kExamplesParam=[]&$kSdkParam=${state.sdk?.name}', + '/?$kExamplesParam=[]&$kSdkParam=${controller.sdk?.id}', '', ); javaScriptPostMessageRepeated( window, SetContentMessage( - descriptor: state.getLoadingDescriptor(), + descriptor: controller.getLoadingDescriptor(), ), ); } diff --git a/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart index fd981f598751..112bea826ddb 100644 --- a/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart +++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_appbar_title.dart @@ -18,16 +18,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground/components/toggle_theme_button/toggle_theme_icon_button.dart'; +import 'package:playground/components/playground_run_or_cancel_button.dart'; import 'package:playground/constants/assets.dart'; import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/analytics/analytics_service.dart'; -import 'package:playground/modules/editor/components/run_button.dart'; -import 'package:playground/modules/notifications/components/notification.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; -import 'package:playground/utils/analytics_utils.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class EmbeddedAppBarTitle extends StatelessWidget { @@ -35,50 +30,19 @@ class EmbeddedAppBarTitle extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, state, child) => Wrap( + return Consumer( + builder: (context, controller, child) => Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: kXlSpacing, children: [ - RunButton( - isRunning: state.isCodeRunning, - cancelRun: () { - final exampleName = getAnalyticsExampleName(state); - final analyticsService = AnalyticsService.get(context); - analyticsService.trackClickCancelRunEvent(exampleName); - - state.cancelRun().catchError( - (_) => NotificationManager.showError( - context, - AppLocalizations.of(context)!.runCode, - AppLocalizations.of(context)!.cancelExecution, - ), - ); - }, - runCode: () { - final stopwatch = Stopwatch()..start(); - final exampleName = getAnalyticsExampleName(state); - final analyticsService = AnalyticsService.get(context); - - state.runCode( - onFinish: () { - analyticsService.trackRunTimeEvent( - exampleName, - stopwatch.elapsedMilliseconds, - ); - }, - ); - analyticsService.trackClickRunEvent(exampleName); - }, - ), + const PlaygroundRunOrCancelButton(), const ToggleThemeIconButton(), IconButton( iconSize: kIconSizeLg, splashRadius: kIconButtonSplashRadius, icon: SvgPicture.asset(kCopyIconAsset), onPressed: () { - final source = - Provider.of(context, listen: false).source; + final source = controller.source; Clipboard.setData(ClipboardData(text: source)); }, ), diff --git a/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart b/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart index 9728bdb1f438..94000034f299 100644 --- a/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart +++ b/playground/frontend/lib/pages/embedded_playground/components/embedded_editor.dart @@ -17,10 +17,7 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/components/loading_indicator/loading_indicator.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/editor/components/editor_textarea.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class EmbeddedEditor extends StatelessWidget { @@ -30,21 +27,17 @@ class EmbeddedEditor extends StatelessWidget { @override Widget build(BuildContext context) { - final state = Provider.of(context); - final controller = state.snippetEditingController; + final controller = Provider.of(context); + final snippetController = controller.snippetEditingController; - if (controller == null) { - return const LoadingIndicator(size: kLgLoadingIndicatorSize); + if (snippetController == null) { + return const LoadingIndicator(); } - return EditorTextArea( - codeController: controller.codeController, - key: ValueKey(state.selectedExample), - enabled: true, - sdk: controller.sdk, - example: state.selectedExample, + return SnippetEditor( + controller: snippetController, isEditable: isEditable, - isEmbedded: true, + goToContextLine: false, ); } } diff --git a/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart b/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart index 8d4c41454f3c..396c31a8264d 100644 --- a/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart +++ b/playground/frontend/lib/pages/embedded_playground/embedded_playground_page.dart @@ -17,12 +17,11 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/modules/output/components/output.dart'; import 'package:playground/pages/embedded_playground/components/embedded_actions.dart'; import 'package:playground/pages/embedded_playground/components/embedded_appbar_title.dart'; import 'package:playground/pages/embedded_playground/components/embedded_editor.dart'; import 'package:playground/pages/embedded_playground/components/embedded_split_view.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; const kActionsWidth = 300.0; @@ -38,8 +37,8 @@ class EmbeddedPlaygroundPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, state, child) => Scaffold( + return Consumer( + builder: (context, controller, child) => Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: const EmbeddedAppBarTitle(), @@ -49,9 +48,9 @@ class EmbeddedPlaygroundPage extends StatelessWidget { first: EmbeddedEditor(isEditable: isEditable), second: Container( color: Theme.of(context).backgroundColor, - child: Output( - isEmbedded: true, - playgroundState: state, + child: OutputWidget( + playgroundController: controller, + graphDirection: Axis.horizontal, ), ), ), diff --git a/playground/frontend/lib/pages/playground/components/close_listener.dart b/playground/frontend/lib/pages/playground/components/close_listener.dart index 682355585b09..ba2a19a7ae22 100644 --- a/playground/frontend/lib/pages/playground/components/close_listener.dart +++ b/playground/frontend/lib/pages/playground/components/close_listener.dart @@ -17,7 +17,7 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'dart:html' as html; import 'package:provider/provider.dart'; @@ -36,7 +36,7 @@ class _CloseListenerState extends State { void initState() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { html.window.onBeforeUnload.listen((event) async { - Provider.of(context, listen: false).cancelRun(); + Provider.of(context, listen: false).cancelRun(); }); }); super.initState(); diff --git a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart index be58a603d181..5d427f41d98f 100644 --- a/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart +++ b/playground/frontend/lib/pages/playground/components/editor_textarea_wrapper.dart @@ -18,17 +18,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/components/loading_indicator/loading_indicator.dart'; +import 'package:playground/components/playground_run_or_cancel_button.dart'; import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/analytics/analytics_service.dart'; -import 'package:playground/modules/editor/components/editor_textarea.dart'; -import 'package:playground/modules/editor/components/run_button.dart'; import 'package:playground/modules/editor/components/share_dropdown/share_button.dart'; import 'package:playground/modules/examples/components/description_popover/description_popover_button.dart'; import 'package:playground/modules/examples/components/multifile_popover/multifile_popover_button.dart'; -import 'package:playground/modules/notifications/components/notification.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; -import 'package:playground/utils/analytics_utils.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class CodeTextAreaWrapper extends StatelessWidget { @@ -36,17 +31,17 @@ class CodeTextAreaWrapper extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer(builder: (context, state, child) { - if (state.result?.errorMessage?.isNotEmpty ?? false) { + return Consumer(builder: (context, controller, child) { + if (controller.result?.errorMessage?.isNotEmpty ?? false) { WidgetsBinding.instance.addPostFrameCallback((_) { - _handleError(context, state); + _handleError(context, controller); }); } - final controller = state.snippetEditingController; + final snippetController = controller.snippetEditingController; - if (controller == null) { - return const LoadingIndicator(size: kLgLoadingIndicatorSize); + if (snippetController == null) { + return const LoadingIndicator(); } return Column( @@ -55,12 +50,10 @@ class CodeTextAreaWrapper extends StatelessWidget { child: Stack( children: [ Positioned.fill( - child: EditorTextArea( - codeController: controller.codeController, - enabled: !(state.selectedExample?.isMultiFile ?? false), - example: state.selectedExample, - sdk: controller.sdk, + child: SnippetEditor( + controller: snippetController, isEditable: true, + goToContextLine: true, ), ), Positioned( @@ -69,12 +62,12 @@ class CodeTextAreaWrapper extends StatelessWidget { height: kButtonHeight, child: Row( children: [ - if (state.selectedExample != null) ...[ - if (state.selectedExample?.isMultiFile ?? false) + if (controller.selectedExample != null) ...[ + if (controller.selectedExample?.isMultiFile ?? false) Semantics( container: true, child: MultifilePopoverButton( - example: state.selectedExample!, + example: controller.selectedExample!, followerAnchor: Alignment.topRight, targetAnchor: Alignment.bottomRight, ), @@ -82,7 +75,7 @@ class CodeTextAreaWrapper extends StatelessWidget { Semantics( container: true, child: DescriptionPopoverButton( - example: state.selectedExample!, + example: controller.selectedExample!, followerAnchor: Alignment.topRight, targetAnchor: Alignment.bottomRight, ), @@ -95,39 +88,7 @@ class CodeTextAreaWrapper extends StatelessWidget { const SizedBox(width: kLgSpacing), Semantics( container: true, - child: RunButton( - disabled: state.selectedExample?.isMultiFile ?? false, - isRunning: state.isCodeRunning, - cancelRun: () { - final exampleName = getAnalyticsExampleName(state); - AnalyticsService.get(context) - .trackClickCancelRunEvent(exampleName); - state.cancelRun().catchError( - (_) => NotificationManager.showError( - context, - AppLocalizations.of(context)!.runCode, - AppLocalizations.of(context)! - .cancelExecution, - ), - ); - }, - runCode: () { - AnalyticsService analyticsService = - AnalyticsService.get(context); - final stopwatch = Stopwatch()..start(); - final exampleName = getAnalyticsExampleName(state); - - state.runCode( - onFinish: () { - analyticsService.trackRunTimeEvent( - exampleName, - stopwatch.elapsedMilliseconds, - ); - }, - ); - analyticsService.trackClickRunEvent(exampleName); - }, - ), + child: const PlaygroundRunOrCancelButton(), ), ], ), @@ -140,12 +101,12 @@ class CodeTextAreaWrapper extends StatelessWidget { }); } - _handleError(BuildContext context, PlaygroundState state) { + void _handleError(BuildContext context, PlaygroundController controller) { NotificationManager.showError( context, AppLocalizations.of(context)!.runCode, - state.result?.errorMessage ?? '', + controller.result?.errorMessage ?? '', ); - state.resetError(); + controller.resetError(); } } diff --git a/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart b/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart index 38f17396cbff..278d973d84ef 100644 --- a/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart +++ b/playground/frontend/lib/pages/playground/components/feedback/feedback_dropdown_content.dart @@ -17,13 +17,12 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/components/horizontal_divider/horizontal_divider.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/fonts.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; import 'package:playground/pages/playground/components/feedback/feedback_dropdown_icon_button.dart'; +import 'package:playground_components/playground_components.dart'; const double kTextFieldWidth = 365.0; const double kTextFieldHeight = 68.0; @@ -46,8 +45,10 @@ class FeedbackDropdownContent extends StatelessWidget { @override Widget build(BuildContext context) { + final borderColor = Theme.of(context).extension()!.borderColor; + final OutlineInputBorder border = OutlineInputBorder( - borderSide: BorderSide(color: ThemeColors.of(context).lightGreyColor), + borderSide: BorderSide(color: borderColor), borderRadius: BorderRadius.circular(kMdBorderRadius), ); @@ -114,7 +115,7 @@ class FeedbackDropdownContent extends StatelessWidget { enabledBorder: border, contentPadding: const EdgeInsets.all(kMdSpacing), ), - cursorColor: ThemeColors.of(context).lightGreyColor, + cursorColor: borderColor, cursorWidth: kCursorSize, onFieldSubmitted: (String filterText) {}, maxLines: 3, @@ -124,7 +125,7 @@ class FeedbackDropdownContent extends StatelessWidget { ], ), ), - const HorizontalDivider(), + const BeamDivider(), Padding( padding: const EdgeInsets.only( top: kXlSpacing, @@ -141,7 +142,7 @@ class FeedbackDropdownContent extends StatelessWidget { color: Theme.of(context).backgroundColor, borderRadius: BorderRadius.circular(kSmBorderRadius), border: Border.all( - color: ThemeColors.of(context).lightGreyColor, + color: borderColor, ), ), child: TextButton( diff --git a/playground/frontend/lib/pages/playground/components/more_actions.dart b/playground/frontend/lib/pages/playground/components/more_actions.dart index 4fe7c9aa121c..60f4e0ff9b34 100644 --- a/playground/frontend/lib/pages/playground/components/more_actions.dart +++ b/playground/frontend/lib/pages/playground/components/more_actions.dart @@ -19,11 +19,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/assets.dart'; import 'package:playground/constants/links.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; import 'package:playground/modules/shortcuts/components/shortcuts_modal.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:url_launcher/url_launcher.dart'; enum HeaderAction { @@ -36,7 +36,11 @@ enum HeaderAction { } class MoreActions extends StatefulWidget { - const MoreActions({Key? key}) : super(key: key); + final PlaygroundController playgroundController; + + const MoreActions({ + required this.playgroundController, + }); @override State createState() => _MoreActionsState(); @@ -52,7 +56,7 @@ class _MoreActionsState extends State { child: PopupMenuButton( icon: Icon( Icons.more_horiz_outlined, - color: ThemeColors.of(context).grey1Color, + color: Theme.of(context).extension()?.iconColor, ), itemBuilder: (BuildContext context) => >[ PopupMenuItem( @@ -62,10 +66,12 @@ class _MoreActionsState extends State { leading: SvgPicture.asset(kShortcutsIconAsset), title: Text(appLocale.shortcuts), onTap: () { - AnalyticsService.get(context).trackOpenShortcutsModal(); - showDialog( + AnalyticsService.get(context).trackOpenShortcutsModal(); + showDialog( context: context, - builder: (BuildContext context) => const ShortcutsModal(), + builder: (BuildContext context) => ShortcutsModal( + playgroundController: widget.playgroundController, + ), ); }, ), diff --git a/playground/frontend/lib/pages/playground/components/playground_page_body.dart b/playground/frontend/lib/pages/playground/components/playground_page_body.dart index 8a723982ed40..419b44dca5df 100644 --- a/playground/frontend/lib/pages/playground/components/playground_page_body.dart +++ b/playground/frontend/lib/pages/playground/components/playground_page_body.dart @@ -17,14 +17,12 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/components/split_view/split_view.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/output/components/output.dart'; +import 'package:playground/modules/output/components/output_header/output_placements.dart'; import 'package:playground/modules/output/models/output_placement.dart'; import 'package:playground/modules/output/models/output_placement_state.dart'; import 'package:playground/pages/playground/components/editor_textarea_wrapper.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class PlaygroundPageBody extends StatelessWidget { @@ -32,30 +30,35 @@ class PlaygroundPageBody extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer2( + return Consumer2( builder: (context, outputState, playgroundState, child) { - final output = createOutput(playgroundState); + final output = OutputWidget( + // isEmbedded: false, + graphDirection: outputState.placement.graphDirection, + playgroundController: playgroundState, + trailing: const OutputPlacements(), + ); + switch (outputState.placement) { case OutputPlacement.bottom: return SplitView( - direction: SplitViewDirection.vertical, + direction: Axis.vertical, first: codeTextArea, second: output, - dividerSize: kMdSpacing, ); + case OutputPlacement.left: return SplitView( - direction: SplitViewDirection.horizontal, + direction: Axis.horizontal, first: output, second: codeTextArea, - dividerSize: kMdSpacing, ); + case OutputPlacement.right: return SplitView( - direction: SplitViewDirection.horizontal, + direction: Axis.horizontal, first: codeTextArea, second: output, - dividerSize: kMdSpacing, ); } }); @@ -63,18 +66,13 @@ class PlaygroundPageBody extends StatelessWidget { Widget get codeTextArea => const CodeTextAreaWrapper(); - Widget createOutput(PlaygroundState state) => Output( - isEmbedded: false, - playgroundState: state, - ); - Widget getVerticalSeparator(BuildContext context) => Container( width: kMdSpacing, - color: ThemeColors.of(context).divider, + color: Theme.of(context).dividerColor, ); Widget getHorizontalSeparator(BuildContext context) => Container( height: kMdSpacing, - color: ThemeColors.of(context).divider, + color: Theme.of(context).dividerColor, ); } diff --git a/playground/frontend/lib/pages/playground/components/playground_page_footer.dart b/playground/frontend/lib/pages/playground/components/playground_page_footer.dart index 7e6b6bcf799a..b97bafad949f 100644 --- a/playground/frontend/lib/pages/playground/components/playground_page_footer.dart +++ b/playground/frontend/lib/pages/playground/components/playground_page_footer.dart @@ -18,12 +18,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/constants/font_weight.dart'; import 'package:playground/constants/links.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; import 'package:playground/pages/playground/components/feedback/playground_feedback.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:url_launcher/url_launcher.dart'; class PlaygroundPageFooter extends StatelessWidget { @@ -34,7 +34,9 @@ class PlaygroundPageFooter extends StatelessWidget { AppLocalizations appLocale = AppLocalizations.of(context)!; return Container( - color: ThemeColors.of(context).secondaryBackground, + color: Theme.of(context) + .extension() + ?.secondaryBackgroundColor, width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric( diff --git a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart index 9b809c9c262c..69c2713e30f1 100644 --- a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart +++ b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart @@ -17,26 +17,18 @@ */ import 'package:flutter/material.dart'; +import 'package:playground/config.g.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; import 'package:playground/modules/analytics/google_analytics_service.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/grpc_code_client.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_repository.dart'; -import 'package:playground/modules/examples/repositories/example_client/grpc_example_client.dart'; -import 'package:playground/modules/examples/repositories/example_repository.dart'; +import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart'; import 'package:playground/modules/messages/handlers/messages_debouncer.dart'; import 'package:playground/modules/messages/handlers/messages_handler.dart'; import 'package:playground/modules/messages/listeners/messages_listener.dart'; import 'package:playground/modules/output/models/output_placement_state.dart'; -import 'package:playground/pages/playground/states/example_loaders/examples_loader.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; import 'package:playground/pages/playground/states/feedback_state.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; -final CodeRepository kCodeRepository = CodeRepository(GrpcCodeClient()); -final ExampleRepository kExampleRepository = - ExampleRepository(GrpcExampleClient()); - class PlaygroundPageProviders extends StatelessWidget { final Widget child; @@ -52,20 +44,47 @@ class PlaygroundPageProviders extends StatelessWidget { Provider( create: (context) => GoogleAnalyticsService(), ), - ChangeNotifierProvider( + ChangeNotifierProvider( create: (context) { - final state = PlaygroundState( + final codeRepository = CodeRepository( + client: GrpcCodeClient( + url: kApiClientURL, + runnerUrlsById: { + Sdk.java.id: kApiJavaClientURL, + Sdk.go.id: kApiGoClientURL, + Sdk.python.id: kApiPythonClientURL, + Sdk.scio.id: kApiScioClientURL, + }, + ), + ); + + final exampleRepository = ExampleRepository( + client: GrpcExampleClient(url: kApiClientURL), + ); + + final exampleCache = ExampleCache( + exampleRepository: exampleRepository, + hasCatalog: true, + )..init(); + + final controller = PlaygroundController( examplesLoader: ExamplesLoader(), - exampleState: ExampleState(kExampleRepository)..init(), - codeRepository: kCodeRepository, + exampleCache: exampleCache, + codeRepository: codeRepository, + ); + + final descriptor = ExamplesLoadingDescriptorFactory.fromUriParts( + path: Uri.base.path, + params: Uri.base.queryParameters, ); + controller.examplesLoader.load(descriptor); final handler = MessagesDebouncer( - handler: MessagesHandler(playgroundState: state), + handler: MessagesHandler(playgroundController: controller), ); MessagesListener(handler: handler); - return state; + return controller; }, ), ChangeNotifierProvider( diff --git a/playground/frontend/lib/pages/playground/playground_page.dart b/playground/frontend/lib/pages/playground/playground_page.dart index 7f68817f3e4a..a7a66f02d5ab 100644 --- a/playground/frontend/lib/pages/playground/playground_page.dart +++ b/playground/frontend/lib/pages/playground/playground_page.dart @@ -18,7 +18,6 @@ import 'package:flutter/material.dart'; import 'package:playground/components/logo/logo_component.dart'; -import 'package:playground/components/toggle_theme_button/toggle_theme_button.dart'; import 'package:playground/constants/sizes.dart'; import 'package:playground/modules/actions/components/new_example_action.dart'; import 'package:playground/modules/actions/components/reset_action.dart'; @@ -33,7 +32,7 @@ import 'package:playground/pages/playground/components/close_listener_nonweb.dar import 'package:playground/pages/playground/components/more_actions.dart'; import 'package:playground/pages/playground/components/playground_page_body.dart'; import 'package:playground/pages/playground/components/playground_page_footer.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class PlaygroundPage extends StatelessWidget { @@ -42,56 +41,65 @@ class PlaygroundPage extends StatelessWidget { @override Widget build(BuildContext context) { return CloseListener( - child: ShortcutsManager( - shortcuts: globalShortcuts, - child: Scaffold( - appBar: AppBar( - automaticallyImplyLeading: false, - title: Consumer( - builder: (context, state, child) { - final controller = state.snippetEditingController; + child: Consumer( + builder: (context, controller, child) { + final snippetController = controller.snippetEditingController; - return Wrap( + return ShortcutsManager( + shortcuts: [ + ...controller.shortcuts, + ...globalShortcuts, + ], + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Wrap( crossAxisAlignment: WrapCrossAlignment.center, spacing: kXlSpacing, children: [ const Logo(), ExampleSelector( changeSelectorVisibility: - state.exampleState.changeSelectorVisibility, - isSelectorOpened: state.exampleState.isSelectorOpened, + controller.exampleCache.changeSelectorVisibility, + isSelectorOpened: + controller.exampleCache.isSelectorOpened, ), SDKSelector( - value: state.sdk, + value: controller.sdk, onChanged: (newSdk) { AnalyticsService.get(context) - .trackSelectSdk(state.sdk, newSdk); - state.setSdk(newSdk); + .trackSelectSdk(controller.sdk, newSdk); + controller.setSdk(newSdk); }, ), - if (controller != null) + if (snippetController != null) PipelineOptionsDropdown( - pipelineOptions: controller.pipelineOptions, - setPipelineOptions: state.setPipelineOptions, + pipelineOptions: snippetController.pipelineOptions, + setPipelineOptions: controller.setPipelineOptions, ), const NewExampleAction(), - ResetAction(reset: state.reset), + const ResetAction(), ], - ); - }, - ), - actions: const [ToggleThemeButton(), MoreActions()], - ), - body: Column( - children: [ - const Expanded(child: PlaygroundPageBody()), - Semantics( - container: true, - child: const PlaygroundPageFooter(), + ), + actions: [ + const ToggleThemeButton(), + MoreActions( + playgroundController: controller, + ), + ], ), - ], - ), - ), + body: Column( + children: [ + const Expanded(child: PlaygroundPageBody()), + Semantics( + container: true, + child: const PlaygroundPageFooter(), + ), + ], + ), + ), + ); + }, ), ); } diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/examples_loader.dart b/playground/frontend/lib/pages/playground/states/example_loaders/examples_loader.dart deleted file mode 100644 index ad4efacd1309..000000000000 --- a/playground/frontend/lib/pages/playground/states/example_loaders/examples_loader.dart +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart'; -import 'package:playground/pages/playground/states/example_loaders/catalog_default_example_loader.dart'; -import 'package:playground/pages/playground/states/example_loaders/content_example_loader.dart'; -import 'package:playground/pages/playground/states/example_loaders/empty_example_loader.dart'; -import 'package:playground/pages/playground/states/example_loaders/example_loader.dart'; -import 'package:playground/pages/playground/states/example_loaders/standard_example_loader.dart'; -import 'package:playground/pages/playground/states/example_loaders/user_shared_example_loader.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; - -class ExamplesLoader { - PlaygroundState? _playgroundState; - ExamplesLoadingDescriptor? _descriptor; - - void setPlaygroundState(PlaygroundState value) { - _playgroundState = value; - } - - Future load(ExamplesLoadingDescriptor descriptor) async { - if (_descriptor == descriptor) { - return; - } - - _descriptor = descriptor; - await Future.wait( - descriptor.descriptors.map( - (one) => loadOne(group: descriptor, one: one), - ), - ); - - final sdk = descriptor.initialSdk; - if (sdk != null) { - _playgroundState!.setSdk(sdk); - } - } - - Future loadOne({ - required ExamplesLoadingDescriptor group, - required ExampleLoadingDescriptor one, - }) async { - final example = await _getOneLoader(one).future; - _playgroundState!.setExample( - example, - setCurrentSdk: - example.sdk == group.initialSdk || group.initialSdk == null, - ); - } - - ExampleLoader _getOneLoader(ExampleLoadingDescriptor descriptor) { - final exampleState = _playgroundState!.exampleState; - - if (descriptor is CatalogDefaultExampleLoadingDescriptor) { - return CatalogDefaultExampleLoader( - descriptor: descriptor, - exampleState: exampleState, - ); - } - - if (descriptor is ContentExampleLoadingDescriptor) { - return ContentExampleLoader( - descriptor: descriptor, - ); - } - - if (descriptor is EmptyExampleLoadingDescriptor) { - return EmptyExampleLoader( - descriptor: descriptor, - ); - } - - if (descriptor is StandardExampleLoadingDescriptor) { - return StandardExampleLoader( - descriptor: descriptor, - exampleState: exampleState, - ); - } - - if (descriptor is UserSharedExampleLoadingDescriptor) { - return UserSharedExampleLoader( - descriptor: descriptor, - exampleState: exampleState, - ); - } - - throw Exception('Unknown example loading descriptor: $descriptor'); - } -} diff --git a/playground/frontend/lib/pages/playground/states/example_selector_state.dart b/playground/frontend/lib/pages/playground/states/example_selector_state.dart index 2f6f462f7f01..f62a1773012f 100644 --- a/playground/frontend/lib/pages/playground/states/example_selector_state.dart +++ b/playground/frontend/lib/pages/playground/states/example_selector_state.dart @@ -17,18 +17,16 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/modules/examples/models/category_model.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; class ExampleSelectorState with ChangeNotifier { - final PlaygroundState _playgroundState; + final PlaygroundController _playgroundController; ExampleType _selectedFilterType; String _filterText; - List categories; + List categories; ExampleSelectorState( - this._playgroundState, + this._playgroundController, this.categories, [ this._selectedFilterType = ExampleType.all, this._filterText = '', @@ -48,26 +46,26 @@ class ExampleSelectorState with ChangeNotifier { notifyListeners(); } - void setCategories(List? categories) { - this.categories = categories ?? []; + void setCategories(List categories) { + this.categories = categories; notifyListeners(); } void sortCategories() { - final categories = _playgroundState.exampleState.getCategories( - _playgroundState.sdk, + final categories = _playgroundController.exampleCache.getCategories( + _playgroundController.sdk, ); final sortedCategories = categories - .map((category) => CategoryModel( - name: category.name, + .map((category) => CategoryWithExamples( + title: category.title, examples: _sortCategoryExamples(category.examples))) .where((category) => category.examples.isNotEmpty) .toList(); setCategories(sortedCategories); } - List _sortCategoryExamples(List examples) { + List _sortCategoryExamples(List examples) { final isAllFilterType = selectedFilterType == ExampleType.all; final isFilterTextEmpty = filterText.isEmpty; if (isAllFilterType && isFilterTextEmpty) { @@ -89,15 +87,15 @@ class ExampleSelectorState with ChangeNotifier { return sortExamplesByName(sorted, filterText); } - List sortExamplesByType( - List examples, + List sortExamplesByType( + List examples, ExampleType type, ) { return examples.where((element) => element.type == type).toList(); } - List sortExamplesByName( - List examples, + List sortExamplesByName( + List examples, String name, ) { return examples diff --git a/playground/frontend/lib/pages/playground/states/examples_state.dart b/playground/frontend/lib/pages/playground/states/examples_state.dart deleted file mode 100644 index 594e74a7a1b0..000000000000 --- a/playground/frontend/lib/pages/playground/states/examples_state.dart +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:playground/constants/params.dart'; -import 'package:playground/modules/examples/models/category_model.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/examples/repositories/example_repository.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart'; -import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart'; -import 'package:playground/modules/examples/repositories/models/shared_file_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; - -class ExampleState with ChangeNotifier { - final ExampleRepository _exampleRepository; - Map>? sdkCategories; - Map defaultExamplesMap = {}; - ExampleModel? defaultExample; - bool isSelectorOpened = false; - - final _allExamplesCompleter = Completer(); - - Future get allExamplesFuture => _allExamplesCompleter.future; - - bool get hasExampleCatalog => !isEmbedded(); - - ExampleState(this._exampleRepository); - - Future init() async { - if (hasExampleCatalog) { - await Future.wait([ - _loadCategories(), - loadDefaultExamplesIfNot(), - ]); - } - } - - void setSdkCategories(Map> map) { - sdkCategories = map; - _allExamplesCompleter.complete(); - } - - List getCategories(SDK? sdk) { - return sdkCategories?[sdk] ?? []; - } - - Future getExampleOutput(String id, SDK sdk) async { - return _exampleRepository.getExampleOutput( - GetExampleRequestWrapper(id, sdk), - ); - } - - Future getExampleSource(String id, SDK sdk) async { - return _exampleRepository.getExampleSource( - GetExampleRequestWrapper(id, sdk), - ); - } - - Future getExample(String path, SDK sdk) async { - return _exampleRepository.getExample( - GetExampleRequestWrapper(path, sdk), - ); - } - - Future getExampleLogs(String id, SDK sdk) async { - return _exampleRepository.getExampleLogs( - GetExampleRequestWrapper(id, sdk), - ); - } - - Future getExampleGraph(String id, SDK sdk) async { - return _exampleRepository.getExampleGraph( - GetExampleRequestWrapper(id, sdk), - ); - } - - Future loadSharedExample(String id) async { - GetSnippetResponse result = await _exampleRepository.getSnippet( - GetSnippetRequestWrapper(id: id), - ); - return ExampleModel( - sdk: result.sdk, - name: result.files.first.name, - path: id, - description: '', - type: ExampleType.example, - source: result.files.first.code, - pipelineOptions: result.pipelineOptions, - ); - } - - Future getSnippetId({ - required List files, - required SDK sdk, - required String pipelineOptions, - }) async { - String id = await _exampleRepository.saveSnippet(SaveSnippetRequestWrapper( - files: files, - sdk: sdk, - pipelineOptions: pipelineOptions, - )); - return id; - } - - Future loadExampleInfo(ExampleModel example) async { - if (example.isInfoFetched()) { - return example; - } - - //GRPC GetPrecompiledGraph errors hotfix - if (example.name == 'MinimalWordCount' && - (example.sdk == SDK.go || example.sdk == SDK.scio)) { - final exampleData = await Future.wait([ - getExampleSource(example.path, example.sdk), - getExampleOutput(example.path, example.sdk), - getExampleLogs(example.path, example.sdk), - ]); - example.setSource(exampleData[0]); - example.setOutputs(exampleData[1]); - example.setLogs(exampleData[2]); - return example; - } - - final exampleData = await Future.wait([ - getExampleSource(example.path, example.sdk), - getExampleOutput(example.path, example.sdk), - getExampleLogs(example.path, example.sdk), - getExampleGraph(example.path, example.sdk) - ]); - example.setSource(exampleData[0]); - example.setOutputs(exampleData[1]); - example.setLogs(exampleData[2]); - example.setGraph(exampleData[3]); - return example; - } - - Future _loadCategories() { - return _exampleRepository - .getListOfExamples( - GetListOfExamplesRequestWrapper(sdk: null, category: null), - ) - .then((map) => setSdkCategories(map)); - } - - void changeSelectorVisibility() { - isSelectorOpened = !isSelectorOpened; - notifyListeners(); - } - - Future loadDefaultExamples() async { - if (defaultExamplesMap.isNotEmpty) { - return; - } - - try { - await Future.wait(SDK.values.map(_loadDefaultExample)); - } catch (ex) { - if (defaultExamplesMap.isEmpty) { - rethrow; - } - // As long as any of the examples is loaded, continue. - print(ex); - // TODO: Log. - } - - notifyListeners(); - } - - Future _loadDefaultExample(SDK sdk) async { - final exampleWithoutInfo = await _exampleRepository.getDefaultExample( - // First parameter is an empty string, because we don't need path to get the default example. - GetExampleRequestWrapper('', sdk), - ); - - defaultExamplesMap[sdk] = await loadExampleInfo(exampleWithoutInfo); - } - - Future loadDefaultExamplesIfNot() async { - if (defaultExamplesMap.isNotEmpty) { - return; - } - - await loadDefaultExamples(); - } - - Future getCatalogExampleByPath(String path) async { - await allExamplesFuture; - - final allExamples = sdkCategories?.values - .expand((sdkCategory) => sdkCategory.map((e) => e.examples)) - .expand((element) => element); - - return allExamples?.firstWhereOrNull( - (e) => e.path == path, - ); - } -} diff --git a/playground/frontend/lib/playground_app.dart b/playground/frontend/lib/playground_app.dart index 8c5f2a489e6e..867bbad7f4b9 100644 --- a/playground/frontend/lib/playground_app.dart +++ b/playground/frontend/lib/playground_app.dart @@ -16,16 +16,16 @@ * limitations under the License. */ -import 'package:code_text_field/code_text_field.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:playground/config/locale.dart'; -import 'package:playground/config/theme.dart'; import 'package:playground/l10n/l10n.dart'; import 'package:playground/pages/playground/components/playground_page_providers.dart'; import 'package:playground/pages/playground/playground_page.dart'; import 'package:playground/pages/routes.dart'; +import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; class PlaygroundApp extends StatelessWidget { @@ -36,32 +36,30 @@ class PlaygroundApp extends StatelessWidget { return ThemeSwitchNotifierProvider( child: Consumer( builder: (context, themeSwitchNotifier, _) { - return CodeTheme( - data: themeSwitchNotifier.codeTheme, - child: ChangeNotifierProvider( - create: (context) => LocaleProvider(), - builder: (context, state) { - final localeProvider = Provider.of(context); - return PlaygroundPageProviders( - child: MaterialApp( - title: 'Apache Beam Playground', - themeMode: themeSwitchNotifier.themeMode, - theme: kLightTheme, - darkTheme: kDarkTheme, - onGenerateRoute: Routes.generateRoute, - home: const PlaygroundPage(), - debugShowCheckedModeBanner: false, - locale: localeProvider.locale, - supportedLocales: L10n.locales, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - ), - ); - }, - ), + return ChangeNotifierProvider( + create: (context) => LocaleProvider(), + builder: (context, state) { + final localeProvider = Provider.of(context); + return PlaygroundPageProviders( + child: MaterialApp( + title: 'Apache Beam Playground', + themeMode: themeSwitchNotifier.themeMode, + theme: kLightTheme, + darkTheme: kDarkTheme, + onGenerateRoute: Routes.generateRoute, + home: const PlaygroundPage(), + debugShowCheckedModeBanner: false, + locale: localeProvider.locale, + supportedLocales: L10n.locales, + localizationsDelegates: [ + ...context.localizationDelegates, + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + ), + ); + }, ); }, ), diff --git a/playground/frontend/lib/utils/analytics_utils.dart b/playground/frontend/lib/utils/analytics_utils.dart index ab77fc23d964..786b54ff382f 100644 --- a/playground/frontend/lib/utils/analytics_utils.dart +++ b/playground/frontend/lib/utils/analytics_utils.dart @@ -16,13 +16,12 @@ * limitations under the License. */ -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/playground_components.dart'; -String getAnalyticsExampleName(PlaygroundState state) { - final customCodeName = 'Custom code, sdk ${state.sdk?.displayName}'; - if (state.isExampleChanged) { +String getAnalyticsExampleName(PlaygroundController controller) { + final customCodeName = 'Custom code, sdk ${controller.sdk?.title}'; + if (controller.isExampleChanged) { return customCodeName; } - return state.selectedExample?.path ?? customCodeName; + return controller.selectedExample?.path ?? customCodeName; } diff --git a/playground/frontend/assets/reset.svg b/playground/frontend/playground_components/assets/buttons/reset.svg similarity index 100% rename from playground/frontend/assets/reset.svg rename to playground/frontend/playground_components/assets/buttons/reset.svg diff --git a/learning/tour-of-beam/frontend/assets/svg/theme-mode.svg b/playground/frontend/playground_components/assets/buttons/theme-mode.svg similarity index 100% rename from learning/tour-of-beam/frontend/assets/svg/theme-mode.svg rename to playground/frontend/playground_components/assets/buttons/theme-mode.svg diff --git a/playground/frontend/assets/error_notification.svg b/playground/frontend/playground_components/assets/notification_icons/error.svg similarity index 100% rename from playground/frontend/assets/error_notification.svg rename to playground/frontend/playground_components/assets/notification_icons/error.svg diff --git a/playground/frontend/assets/info_notification.svg b/playground/frontend/playground_components/assets/notification_icons/info.svg similarity index 100% rename from playground/frontend/assets/info_notification.svg rename to playground/frontend/playground_components/assets/notification_icons/info.svg diff --git a/playground/frontend/assets/success_notification.svg b/playground/frontend/playground_components/assets/notification_icons/success.svg similarity index 100% rename from playground/frontend/assets/success_notification.svg rename to playground/frontend/playground_components/assets/notification_icons/success.svg diff --git a/playground/frontend/assets/warning_notification.svg b/playground/frontend/playground_components/assets/notification_icons/warning.svg similarity index 100% rename from playground/frontend/assets/warning_notification.svg rename to playground/frontend/playground_components/assets/notification_icons/warning.svg diff --git a/playground/frontend/playground_components/assets/svg/drag.svg b/playground/frontend/playground_components/assets/svg/drag-horizontal.svg similarity index 86% rename from playground/frontend/playground_components/assets/svg/drag.svg rename to playground/frontend/playground_components/assets/svg/drag-horizontal.svg index 6ed7dd99200b..f5e8dcda558a 100644 --- a/playground/frontend/playground_components/assets/svg/drag.svg +++ b/playground/frontend/playground_components/assets/svg/drag-horizontal.svg @@ -17,7 +17,7 @@ under the License. --> - - - + + + diff --git a/playground/frontend/playground_components/assets/svg/drag-vertical.svg b/playground/frontend/playground_components/assets/svg/drag-vertical.svg new file mode 100644 index 000000000000..fea5377776ef --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/drag-vertical.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/playground/frontend/playground_components/assets/svg/theme-mode.svg b/playground/frontend/playground_components/assets/svg/theme-mode.svg deleted file mode 100644 index fc1438aecf32..000000000000 --- a/playground/frontend/playground_components/assets/svg/theme-mode.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/playground/frontend/playground_components/assets/translations/en.yaml b/playground/frontend/playground_components/assets/translations/en.yaml index 9fa82e84b45d..f80677c598a9 100644 --- a/playground/frontend/playground_components/assets/translations/en.yaml +++ b/playground/frontend/playground_components/assets/translations/en.yaml @@ -18,3 +18,35 @@ ui: darkMode: 'Dark Mode' lightMode: 'Light Mode' + +intents: + playground: + run: 'Run Code' + reset: 'Reset Code' + +widgets: + + codeEditor: + label: 'Code Text Area' + + output: + filter: + all: 'All' + log: 'Log' + output: 'Output' + filterTitle: 'Display at this tab' + graph: 'Graph' + result: 'Result' + + + resetButton: + label: 'Reset' + + runOrCancelButton: + titles: + run: 'Run' + cancel: 'Cancel' + notificationTitles: + run: 'Run Code' + cancelExecution: 'Cancel Execution' + diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart index 21338ebf9780..a7201940a84a 100644 --- a/playground/frontend/playground_components/lib/playground_components.dart +++ b/playground/frontend/playground_components/lib/playground_components.dart @@ -16,16 +16,58 @@ * limitations under the License. */ +export 'src/cache/example_cache.dart'; + export 'src/constants/colors.dart'; export 'src/constants/links.dart'; +export 'src/constants/playground_components.dart'; export 'src/constants/sizes.dart'; + +export 'src/controllers/example_loaders/examples_loader.dart'; +export 'src/controllers/playground_controller.dart'; + export 'src/enums/complexity.dart'; -export 'src/theme/color_provider.dart'; + +export 'src/models/category_with_examples.dart'; +export 'src/models/example.dart'; +export 'src/models/example_base.dart'; +export 'src/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart'; +export 'src/models/example_loading_descriptors/content_example_loading_descriptor.dart'; +export 'src/models/example_loading_descriptors/empty_example_loading_descriptor.dart'; +export 'src/models/example_loading_descriptors/example_loading_descriptor.dart'; +export 'src/models/example_loading_descriptors/examples_loading_descriptor.dart'; +export 'src/models/example_loading_descriptors/standard_example_loading_descriptor.dart'; +export 'src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart'; +export 'src/models/intents.dart'; +export 'src/models/outputs.dart'; +export 'src/models/sdk.dart'; +export 'src/models/shortcut.dart'; + +export 'src/notifications/notification.dart'; + +export 'src/repositories/code_client/grpc_code_client.dart'; +export 'src/repositories/code_repository.dart'; +export 'src/repositories/example_client/grpc_example_client.dart'; +export 'src/repositories/example_repository.dart'; + export 'src/theme/switch_notifier.dart'; export 'src/theme/theme.dart'; + +export 'src/util/pipeline_options.dart'; + +export 'src/widgets/bubble.dart'; export 'src/widgets/complexity.dart'; export 'src/widgets/dismissible_overlay.dart'; export 'src/widgets/divider.dart'; -export 'src/widgets/drag_indicator.dart'; +export 'src/widgets/header_icon_button.dart'; +export 'src/widgets/loading_indicator.dart'; export 'src/widgets/logo.dart'; +export 'src/widgets/output/output.dart'; +export 'src/widgets/reset_button.dart'; +export 'src/widgets/run_or_cancel_button.dart'; +export 'src/widgets/shortcut_tooltip.dart'; +export 'src/widgets/snippet_editor.dart'; +export 'src/widgets/split_view.dart'; +export 'src/widgets/tab_header.dart'; export 'src/widgets/toggle_theme_button.dart'; +export 'src/widgets/toggle_theme_icon_button.dart'; diff --git a/playground/frontend/playground_components/lib/src/constants/names.dart b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel.dart similarity index 88% rename from playground/frontend/playground_components/lib/src/constants/names.dart rename to playground/frontend/playground_components/lib/src/api/iis_workaround_channel.dart index 346d03dba5c3..5c41c00d327e 100644 --- a/playground/frontend/playground_components/lib/src/constants/names.dart +++ b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel.dart @@ -16,6 +16,5 @@ * limitations under the License. */ -class BeamNames { - static const package = 'playground_components'; -} +export 'iis_workaround_channel_non_web.dart' + if (dart.library.html) 'iis_workaround_channel_web.dart'; diff --git a/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_non_web.dart b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_non_web.dart new file mode 100644 index 000000000000..39fb9d0ba388 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_non_web.dart @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:grpc/grpc_connection_interface.dart'; + +class IisWorkaroundChannel extends ClientChannelBase { + final Uri uri; + + IisWorkaroundChannel.xhr(this.uri) : super(); + + @override + ClientConnection createConnection() { + throw UnimplementedError('This only works in web'); + } +} diff --git a/playground/frontend/lib/api/iis_workaround_channel.dart b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_web.dart similarity index 88% rename from playground/frontend/lib/api/iis_workaround_channel.dart rename to playground/frontend/playground_components/lib/src/api/iis_workaround_channel_web.dart index 2232c59e5034..b666677a643d 100644 --- a/playground/frontend/lib/api/iis_workaround_channel.dart +++ b/playground/frontend/playground_components/lib/src/api/iis_workaround_channel_web.dart @@ -28,12 +28,12 @@ class IisWorkaroundChannel extends ClientChannelBase { @override ClientConnection createConnection() { - return IisClientConnection(uri); + return _IisClientConnection(uri); } } -class IisClientConnection extends XhrClientConnection { - IisClientConnection(Uri uri) : super(uri); +class _IisClientConnection extends XhrClientConnection { + _IisClientConnection(super.uri); @override GrpcTransportStream makeRequest( @@ -43,7 +43,8 @@ class IisClientConnection extends XhrClientConnection { ErrorHandler onError, { CallOptions? callOptions, }) { - var pathWithoutFirstSlash = path.substring(1); + final pathWithoutFirstSlash = path.substring(1); + return super.makeRequest( pathWithoutFirstSlash, timeout, diff --git a/playground/frontend/lib/api/v1/api.pb.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pb.dart similarity index 100% rename from playground/frontend/lib/api/v1/api.pb.dart rename to playground/frontend/playground_components/lib/src/api/v1/api.pb.dart diff --git a/playground/frontend/lib/api/v1/api.pbenum.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pbenum.dart similarity index 100% rename from playground/frontend/lib/api/v1/api.pbenum.dart rename to playground/frontend/playground_components/lib/src/api/v1/api.pbenum.dart diff --git a/playground/frontend/lib/api/v1/api.pbgrpc.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pbgrpc.dart similarity index 100% rename from playground/frontend/lib/api/v1/api.pbgrpc.dart rename to playground/frontend/playground_components/lib/src/api/v1/api.pbgrpc.dart diff --git a/playground/frontend/lib/api/v1/api.pbjson.dart b/playground/frontend/playground_components/lib/src/api/v1/api.pbjson.dart similarity index 100% rename from playground/frontend/lib/api/v1/api.pbjson.dart rename to playground/frontend/playground_components/lib/src/api/v1/api.pbjson.dart diff --git a/playground/frontend/playground_components/lib/src/cache/example_cache.dart b/playground/frontend/playground_components/lib/src/cache/example_cache.dart new file mode 100644 index 000000000000..ea755ba5949a --- /dev/null +++ b/playground/frontend/playground_components/lib/src/cache/example_cache.dart @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +import '../models/category_with_examples.dart'; +import '../models/example.dart'; +import '../models/example_base.dart'; +import '../models/sdk.dart'; +import '../repositories/example_repository.dart'; +import '../repositories/models/get_default_precompiled_object_request.dart'; +import '../repositories/models/get_precompiled_object_request.dart'; +import '../repositories/models/get_precompiled_objects_request.dart'; +import '../repositories/models/shared_file.dart'; +import '../repositories/models/get_snippet_request.dart'; +import '../repositories/models/save_snippet_request.dart'; + +class ExampleCache extends ChangeNotifier { + /// If set, then categories and default examples are enabled. + /// Otherwise examples can only be queried by paths. + final bool hasCatalog; + + final ExampleRepository _exampleRepository; + final categoryListsBySdk = >{}; + + final Map defaultExamplesBySdk = {}; + + // TODO(alexeyinkin): Extract, https://github.com/apache/beam/issues/23249 + bool isSelectorOpened = false; + + final _allExamplesCompleter = Completer(); + + Future get allExamplesFuture => _allExamplesCompleter.future; + + ExampleCache({ + required ExampleRepository exampleRepository, + required this.hasCatalog, + }) : _exampleRepository = exampleRepository; + + Future init() async { + if (hasCatalog) { + await Future.wait([ + _loadCategories(), + loadDefaultExamplesIfNot(), + ]); + } + } + + void setSdkCategories(Map> map) { + categoryListsBySdk.addAll(map); + _allExamplesCompleter.complete(); + } + + List getCategories(Sdk? sdk) { + return categoryListsBySdk[sdk] ?? []; + } + + Future getExampleOutput(String path, Sdk sdk) async { + return _exampleRepository.getExampleOutput( + GetPrecompiledObjectRequest(path: path, sdk: sdk), + ); + } + + Future getExampleSource(String path, Sdk sdk) async { + return _exampleRepository.getExampleSource( + GetPrecompiledObjectRequest(path: path, sdk: sdk), + ); + } + + Future getExample(String path, Sdk sdk) async { + return _exampleRepository.getExample( + GetPrecompiledObjectRequest(path: path, sdk: sdk), + ); + } + + Future getExampleLogs(String path, Sdk sdk) async { + return _exampleRepository.getExampleLogs( + GetPrecompiledObjectRequest(path: path, sdk: sdk), + ); + } + + Future getExampleGraph(String id, Sdk sdk) async { + return _exampleRepository.getExampleGraph( + GetPrecompiledObjectRequest(path: id, sdk: sdk), + ); + } + + Future loadSharedExample(String id) async { + final result = await _exampleRepository.getSnippet( + GetSnippetRequest(id: id), + ); + + return Example( + sdk: result.sdk, + name: result.files.first.name, + path: id, + description: '', + type: ExampleType.example, + source: result.files.first.code, + pipelineOptions: result.pipelineOptions, + ); + } + + Future getSnippetId({ + required List files, + required Sdk sdk, + required String pipelineOptions, + }) async { + final id = await _exampleRepository.saveSnippet( + SaveSnippetRequest( + files: files, + sdk: sdk, + pipelineOptions: pipelineOptions, + ), + ); + return id; + } + + Future loadExampleInfo(ExampleBase example) async { + if (example is Example) { + return example; + } + + //GRPC GetPrecompiledGraph errors hotfix + if (example.name == 'MinimalWordCount' && + (example.sdk == Sdk.go || example.sdk == Sdk.scio)) { + final exampleData = await Future.wait([ + getExampleSource(example.path, example.sdk), + getExampleOutput(example.path, example.sdk), + getExampleLogs(example.path, example.sdk), + ]); + + return Example.fromBase( + example, + source: exampleData[0], + outputs: exampleData[1], + logs: exampleData[2], + ); + } + + final exampleData = await Future.wait([ + getExampleSource(example.path, example.sdk), + getExampleOutput(example.path, example.sdk), + getExampleLogs(example.path, example.sdk), + getExampleGraph(example.path, example.sdk) + ]); + + return Example.fromBase( + example, + source: exampleData[0], + outputs: exampleData[1], + logs: exampleData[2], + graph: exampleData[3], + ); + } + + Future _loadCategories() async { + final result = await _exampleRepository.getListOfExamples( + const GetPrecompiledObjectsRequest( + sdk: null, + category: null, + ), + ); + + setSdkCategories(result); + } + + void changeSelectorVisibility() { + isSelectorOpened = !isSelectorOpened; + notifyListeners(); + } + + Future loadDefaultExamples() async { + if (defaultExamplesBySdk.isNotEmpty) { + return; + } + + try { + await Future.wait(Sdk.known.map(_loadDefaultExample)); + } catch (ex) { + if (defaultExamplesBySdk.isEmpty) { + rethrow; + } + // As long as any of the examples is loaded, continue. + print(ex); + // TODO: Log. + } + + notifyListeners(); + } + + Future _loadDefaultExample(Sdk sdk) async { + final exampleWithoutInfo = await _exampleRepository.getDefaultExample( + GetDefaultPrecompiledObjectRequest(sdk: sdk), + ); + + defaultExamplesBySdk[sdk] = await loadExampleInfo(exampleWithoutInfo); + } + + Future loadDefaultExamplesIfNot() async { + if (defaultExamplesBySdk.isNotEmpty) { + return; + } + + await loadDefaultExamples(); + } + + Future getCatalogExampleByPath(String path) async { + await allExamplesFuture; + + final allExamples = categoryListsBySdk.values + .expand((categories) => categories.map((c) => c.examples)) + .expand((examples) => examples); + + return allExamples.firstWhereOrNull( + (e) => e.path == path, + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/constants/colors.dart b/playground/frontend/playground_components/lib/src/constants/colors.dart index 43aa2a56d85d..43be2453329d 100644 --- a/playground/frontend/playground_components/lib/src/constants/colors.dart +++ b/playground/frontend/playground_components/lib/src/constants/colors.dart @@ -34,18 +34,45 @@ class BeamColors { static const red = Color(0xffE54545); } +class BeamGraphColors { + static const node = BeamColors.grey3; + static const border = Color(0xFF45454E); + static const edge = BeamLightThemeColors.primary; +} + +class BeamNotificationColors { + static const error = Color(0xFFE54545); + static const info = Color(0xFF3E67F6); + static const success = Color(0xFF37AC66); + static const warning = Color(0xFFEEAB00); +} + class BeamLightThemeColors { + static const border = Color(0xFFE5E5E5); static const primaryBackground = BeamColors.white; static const secondaryBackground = Color(0xffFCFCFC); static const grey = Color(0xffE5E5E5); + static const listBackground = Color(0xFFA0A4AB); static const text = BeamColors.darkBlue; static const primary = Color(0xffE74D1A); + static const icon = Color(0xFFA0A4AB); + + static const code1 = Color(0xFFDA2833); + static const code2 = Color(0xFF5929B4); + static const codeComment = Color(0xFF4C6B60); } class BeamDarkThemeColors { + static const border = Color(0xFFA0A4AB); static const primaryBackground = Color(0xff18181B); static const secondaryBackground = BeamColors.darkGrey; static const grey = Color(0xff3F3F46); + static const listBackground = Color(0xFF606772); static const text = Color(0xffFFFFFF); static const primary = Color(0xffF26628); + static const icon = Color(0xFF606772); + + static const code1 = Color(0xFFDA2833); + static const code2 = Color(0xFF5929B4); + static const codeComment = Color(0xFF4C6B60); } diff --git a/playground/frontend/lib/modules/examples/models/example_origin.dart b/playground/frontend/playground_components/lib/src/constants/playground_components.dart similarity index 68% rename from playground/frontend/lib/modules/examples/models/example_origin.dart rename to playground/frontend/playground_components/lib/src/constants/playground_components.dart index ee4e0d789007..89909e112a81 100644 --- a/playground/frontend/lib/modules/examples/models/example_origin.dart +++ b/playground/frontend/playground_components/lib/src/constants/playground_components.dart @@ -16,26 +16,14 @@ * limitations under the License. */ -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:easy_localization_ext/easy_localization_ext.dart'; -enum ExampleOrigin { - empty, - content, - standard, - userShared, - catalogDefault, - ; +class PlaygroundComponents { + static const packageName = 'playground_components'; - static ExampleOrigin fromToken(String? token) { - if (token == null) { - return empty; - } - - final sdk = SDK.tryParseExamplePath(token); - if (sdk != null) { - return standard; - } - - return userShared; - } + // TODO(alexeyinkin): Make const when this is fixed: https://github.com/aissat/easy_localization_loader/issues/39 + static final translationLoader = YamlPackageAssetLoader( + packageName, + path: 'assets/translations', + ); } diff --git a/playground/frontend/playground_components/lib/src/constants/sizes.dart b/playground/frontend/playground_components/lib/src/constants/sizes.dart index 402f4351b3fe..916ae6f7243a 100644 --- a/playground/frontend/playground_components/lib/src/constants/sizes.dart +++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart @@ -29,19 +29,29 @@ class BeamSizes { static const double size18 = 18; static const double size20 = 20; static const double size24 = 24; + static const double size30 = 30; static const double size32 = 32; static const double size36 = 36; static const double size40 = 40; + static const double size64 = 64; static const double appBarHeight = 55; + static const double buttonHeight = 40; + static const double headerButtonHeight = 46; + static const double loadingIndicator = 40; static const double splitViewSeparator = BeamSizes.size8; } class BeamBorderRadius { static const double small = BeamSizes.size4; static const double large = BeamSizes.size8; + static const double infinite = 1000; // TODO: Use StadiumBorder } class BeamIconSizes { + static const double xs = BeamSizes.size8; + static const double small = BeamSizes.size16; static const double large = BeamSizes.size32; + + static const double largeSplashRadius = 24; } diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/catalog_default_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/catalog_default_example_loader.dart similarity index 65% rename from playground/frontend/lib/pages/playground/states/example_loaders/catalog_default_example_loader.dart rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/catalog_default_example_loader.dart index e07d4cb67d6e..d07d08970265 100644 --- a/playground/frontend/lib/pages/playground/states/example_loaders/catalog_default_example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/catalog_default_example_loader.dart @@ -16,28 +16,28 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/pages/playground/states/example_loaders/example_loader.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; +import '../../cache/example_cache.dart'; +import '../../models/example.dart'; +import '../../models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart'; +import 'example_loader.dart'; class CatalogDefaultExampleLoader extends ExampleLoader { final CatalogDefaultExampleLoadingDescriptor descriptor; - final ExampleState exampleState; + final ExampleCache exampleCache; const CatalogDefaultExampleLoader({ required this.descriptor, - required this.exampleState, + required this.exampleCache, }); @override - Future get future async { - if (!exampleState.hasExampleCatalog) { + Future get future async { + if (!exampleCache.hasCatalog) { throw Exception('Default example requires a catalog in ExampleState'); } - await exampleState.loadDefaultExamplesIfNot(); - final result = exampleState.defaultExamplesMap[descriptor.sdk]; + await exampleCache.loadDefaultExamplesIfNot(); + final result = exampleCache.defaultExamplesBySdk[descriptor.sdk]; if (result == null) { throw Exception('Default example not found for $descriptor'); diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/content_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart similarity index 72% rename from playground/frontend/lib/pages/playground/states/example_loaders/content_example_loader.dart rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart index 117bf139f525..a196d7122a7e 100644 --- a/playground/frontend/lib/pages/playground/states/example_loaders/content_example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/content_example_loader.dart @@ -16,24 +16,29 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/pages/playground/states/example_loaders/example_loader.dart'; +import '../../cache/example_cache.dart'; +import '../../models/example.dart'; +import '../../models/example_base.dart'; +import '../../models/example_loading_descriptors/content_example_loading_descriptor.dart'; +import 'example_loader.dart'; class ContentExampleLoader extends ExampleLoader { final ContentExampleLoadingDescriptor descriptor; const ContentExampleLoader({ required this.descriptor, + // TODO(alexeyinkin): Remove when this lands: https://github.com/dart-lang/language/issues/1813 + required ExampleCache exampleCache, }); @override - Future get future async => ExampleModel( + Future get future async => Example( sdk: descriptor.sdk, name: descriptor.name ?? 'Embedded_Example', path: '', description: '', type: ExampleType.example, source: descriptor.content, + pipelineOptions: '', ); } diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/empty_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart similarity index 70% rename from playground/frontend/lib/pages/playground/states/example_loaders/empty_example_loader.dart rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart index 3f1acdc87bd2..8f3b3d07e4c5 100644 --- a/playground/frontend/lib/pages/playground/states/example_loaders/empty_example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/empty_example_loader.dart @@ -16,23 +16,29 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/pages/playground/states/example_loaders/example_loader.dart'; +import '../../cache/example_cache.dart'; +import '../../models/example.dart'; +import '../../models/example_base.dart'; +import '../../models/example_loading_descriptors/empty_example_loading_descriptor.dart'; +import 'example_loader.dart'; class EmptyExampleLoader extends ExampleLoader { final EmptyExampleLoadingDescriptor descriptor; const EmptyExampleLoader({ required this.descriptor, + // TODO(alexeyinkin): Remove when this lands: https://github.com/dart-lang/language/issues/1813 + required ExampleCache exampleCache, }); @override - Future get future async => ExampleModel( + Future get future async => Example( sdk: descriptor.sdk, name: 'Embedded_Example', path: '', description: '', type: ExampleType.example, + source: '', + pipelineOptions: '', ); } diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader.dart similarity index 89% rename from playground/frontend/lib/pages/playground/states/example_loaders/example_loader.dart rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader.dart index 9c6b256683cc..c90f03745da7 100644 --- a/playground/frontend/lib/pages/playground/states/example_loaders/example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader.dart @@ -16,10 +16,10 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_model.dart'; +import '../../models/example.dart'; abstract class ExampleLoader { const ExampleLoader(); - Future get future; + Future get future; } diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader_factory.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader_factory.dart new file mode 100644 index 000000000000..9a16d7d4b990 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/example_loader_factory.dart @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../../cache/example_cache.dart'; +import '../../models/example_loading_descriptors/example_loading_descriptor.dart'; +import 'example_loader.dart'; + +typedef ExampleLoaderFactoryFunction + = ExampleLoader Function({ + required D descriptor, + required ExampleCache exampleCache, +}); + +class ExampleLoaderFactory { + final factories = {}; + + void add( + ExampleLoaderFactoryFunction function, + ) { + factories[D] = ({ + required ExampleLoadingDescriptor descriptor, + required ExampleCache exampleCache, + }) { + return function(descriptor: descriptor as D, exampleCache: exampleCache); + }; + } + + ExampleLoader? create({ + required D descriptor, + required ExampleCache exampleCache, + }) { + return factories[descriptor.runtimeType]?.call( + descriptor: descriptor, + exampleCache: exampleCache, + ); + } +} + diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart new file mode 100644 index 000000000000..6d872e677512 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/examples_loader.dart @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:collection/collection.dart'; + +import '../../models/example_loading_descriptors/example_loading_descriptor.dart'; +import '../../models/example_loading_descriptors/examples_loading_descriptor.dart'; +import '../../models/sdk.dart'; +import '../playground_controller.dart'; +import 'catalog_default_example_loader.dart'; +import 'content_example_loader.dart'; +import 'empty_example_loader.dart'; +import 'example_loader_factory.dart'; +import 'standard_example_loader.dart'; +import 'user_shared_example_loader.dart'; + +class ExamplesLoader { + final defaultFactory = ExampleLoaderFactory(); + PlaygroundController? _playgroundController; + ExamplesLoadingDescriptor? _descriptor; + + ExamplesLoader() { + defaultFactory.add(CatalogDefaultExampleLoader.new); + defaultFactory.add(ContentExampleLoader.new); + defaultFactory.add(EmptyExampleLoader.new); + defaultFactory.add(StandardExampleLoader.new); + defaultFactory.add(UserSharedExampleLoader.new); + } + + void setPlaygroundController(PlaygroundController value) { + _playgroundController = value; + } + + Future load(ExamplesLoadingDescriptor descriptor) async { + if (_descriptor == descriptor) { + return; + } + + _descriptor = descriptor; + await Future.wait( + descriptor.descriptors.map( + (one) => loadOne(group: descriptor, one: one), + ), + ); + + final sdk = descriptor.initialSdk; + if (sdk != null) { + _playgroundController!.setSdk(sdk); + } + } + + Future loadDefaultIfAny(Sdk sdk) async { + final group = _descriptor; + final one = group?.lazyLoadDescriptors[sdk]?.firstOrNull; + + if (group == null || one == null) { + return; + } + + return loadOne( + group: group, + one: one, + ); + } + + Future loadOne({ + required ExamplesLoadingDescriptor group, + required ExampleLoadingDescriptor one, + }) async { + final loader = defaultFactory.create( + descriptor: one, + exampleCache: _playgroundController!.exampleCache, + ); + + if (loader == null) { + // TODO: Log. + print('Cannot create example loader for $one'); + return; + } + + final example = await loader.future; + _playgroundController!.setExample( + example, + setCurrentSdk: + example.sdk == group.initialSdk || group.initialSdk == null, + ); + } +} diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/standard_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart similarity index 61% rename from playground/frontend/lib/pages/playground/states/example_loaders/standard_example_loader.dart rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart index 667f8f24e1dc..23c429ebab13 100644 --- a/playground/frontend/lib/pages/playground/states/example_loaders/standard_example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart @@ -18,11 +18,12 @@ import 'dart:async'; -import 'package:playground/modules/examples/models/example_loading_descriptors/standard_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/pages/playground/states/example_loaders/example_loader.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; +import '../../cache/example_cache.dart'; +import '../../models/example.dart'; +import '../../models/example_base.dart'; +import '../../models/example_loading_descriptors/standard_example_loading_descriptor.dart'; +import '../../models/sdk.dart'; +import 'example_loader.dart'; /// Loads a given example from the local cache, then adds info from network. /// @@ -30,20 +31,20 @@ import 'package:playground/pages/playground/states/examples_state.dart'; /// its cache. So it only completes if this is successful. class StandardExampleLoader extends ExampleLoader { final StandardExampleLoadingDescriptor descriptor; - final ExampleState exampleState; - final _completer = Completer(); + final ExampleCache exampleCache; + final _completer = Completer(); @override - Future get future => _completer.future; + Future get future => _completer.future; StandardExampleLoader({ required this.descriptor, - required this.exampleState, + required this.exampleCache, }) { _load(); } - void _load() async { + Future _load() async { final example = await _loadExampleWithoutInfo(); if (example == null) { @@ -52,23 +53,23 @@ class StandardExampleLoader extends ExampleLoader { } _completer.complete( - exampleState.loadExampleInfo(example), + exampleCache.loadExampleInfo(example), ); } - Future _loadExampleWithoutInfo() { - return exampleState.hasExampleCatalog - ? exampleState.getCatalogExampleByPath(descriptor.path) + Future _loadExampleWithoutInfo() { + return exampleCache.hasCatalog + ? exampleCache.getCatalogExampleByPath(descriptor.path) : _loadExampleFromRepository(); } - Future _loadExampleFromRepository() async { - final sdk = SDK.tryParseExamplePath(descriptor.path); + Future _loadExampleFromRepository() async { + final sdk = Sdk.tryParseExamplePath(descriptor.path); if (sdk == null) { return null; } - return exampleState.getExample(descriptor.path, sdk); + return exampleCache.getExample(descriptor.path, sdk); } } diff --git a/playground/frontend/lib/pages/playground/states/example_loaders/user_shared_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/user_shared_example_loader.dart similarity index 65% rename from playground/frontend/lib/pages/playground/states/example_loaders/user_shared_example_loader.dart rename to playground/frontend/playground_components/lib/src/controllers/example_loaders/user_shared_example_loader.dart index 0a658718a0ba..f256d5c56379 100644 --- a/playground/frontend/lib/pages/playground/states/example_loaders/user_shared_example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/user_shared_example_loader.dart @@ -16,21 +16,21 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/pages/playground/states/example_loaders/example_loader.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; +import '../../cache/example_cache.dart'; +import '../../models/example.dart'; +import '../../models/example_loading_descriptors/user_shared_example_loading_descriptor.dart'; +import 'example_loader.dart'; class UserSharedExampleLoader extends ExampleLoader { final UserSharedExampleLoadingDescriptor descriptor; - final ExampleState exampleState; + final ExampleCache exampleCache; UserSharedExampleLoader({ required this.descriptor, - required this.exampleState, + required this.exampleCache, }); @override - Future get future => - exampleState.loadSharedExample(descriptor.snippetId); + Future get future => + exampleCache.loadSharedExample(descriptor.snippetId); } diff --git a/playground/frontend/lib/pages/playground/states/playground_state.dart b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart similarity index 75% rename from playground/frontend/lib/pages/playground/states/playground_state.dart rename to playground/frontend/playground_components/lib/src/controllers/playground_controller.dart index 2080f2005f5c..d68a30eede37 100644 --- a/playground/frontend/lib/pages/playground/states/playground_state.dart +++ b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart @@ -19,21 +19,24 @@ import 'dart:async'; import 'dart:math'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:playground/modules/editor/controllers/snippet_editing_controller.dart'; -import 'package:playground/modules/editor/parsers/run_options_parser.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_repository.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_request.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_result.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/examples/models/outputs_model.dart'; -import 'package:playground/modules/examples/repositories/models/shared_file_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/pages/playground/states/example_loaders/examples_loader.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; +import 'package:flutter/services.dart'; + +import '../cache/example_cache.dart'; +import '../models/example.dart'; +import '../models/example_base.dart'; +import '../models/example_loading_descriptors/examples_loading_descriptor.dart'; +import '../models/intents.dart'; +import '../models/outputs.dart'; +import '../models/sdk.dart'; +import '../models/shortcut.dart'; +import '../repositories/code_repository.dart'; +import '../repositories/models/run_code_request.dart'; +import '../repositories/models/run_code_result.dart'; +import '../repositories/models/shared_file.dart'; +import '../util/pipeline_options.dart'; +import 'example_loaders/examples_loader.dart'; +import 'snippet_editing_controller.dart'; const kTitleLength = 15; const kExecutionTimeUpdate = 100; @@ -45,40 +48,33 @@ const kPipelineOptionsParseError = const kCachedResultsLog = 'The results of this example are taken from the Apache Beam Playground cache.\n'; -class PlaygroundState with ChangeNotifier { - final ExampleState exampleState; +class PlaygroundController with ChangeNotifier { + final ExampleCache exampleCache; final ExamplesLoader examplesLoader; - final ExamplesLoadingDescriptor examplesLoadingDescriptor; - final _snippetEditingControllers = {}; + final _snippetEditingControllers = {}; + + Sdk? _sdk; + final CodeRepository? _codeRepository; - SDK? _sdk; - CodeRepository? _codeRepository; RunCodeResult? _result; StreamSubscription? _runSubscription; StreamController? _executionTime; - OutputType? selectedOutputFilterType; + + // TODO(alexeyinkin): Extract along with run status, https://github.com/apache/beam/issues/23248 + OutputType selectedOutputFilterType = OutputType.all; String outputResult = ''; - PlaygroundState({ - required this.exampleState, + PlaygroundController({ + required this.exampleCache, required this.examplesLoader, CodeRepository? codeRepository, - }) : examplesLoadingDescriptor = - ExamplesLoadingDescriptorFactory.fromUriParts( - path: Uri.base.path, - params: Uri.base.queryParameters, - ) { - examplesLoader.setPlaygroundState(this); - examplesLoader.load(examplesLoadingDescriptor); - - _codeRepository = codeRepository; - selectedOutputFilterType = OutputType.all; - outputResult = ''; + }) : _codeRepository = codeRepository { + examplesLoader.setPlaygroundController(this); } SnippetEditingController _getOrCreateSnippetEditingController( - SDK sdk, { + Sdk sdk, { required bool loadDefaultIfNot, }) { final existing = _snippetEditingControllers[sdk]; @@ -90,30 +86,21 @@ class PlaygroundState with ChangeNotifier { _snippetEditingControllers[sdk] = result; if (loadDefaultIfNot) { - final descriptor = - examplesLoadingDescriptor.lazyLoadDescriptors[sdk]?.firstOrNull; - - if (descriptor != null) { - examplesLoader.loadOne( - group: examplesLoadingDescriptor, - one: descriptor, - ); - } + examplesLoader.loadDefaultIfAny(sdk); } return result; } - // TODO: Return full, then shorten. + // TODO(alexeyinkin): Return full, then shorten, https://github.com/apache/beam/issues/23250 String get examplesTitle { final name = snippetEditingController?.selectedExample?.name ?? kTitle; return name.substring(0, min(kTitleLength, name.length)); } - ExampleModel? get selectedExample => - snippetEditingController?.selectedExample; + Example? get selectedExample => snippetEditingController?.selectedExample; - SDK? get sdk => _sdk; + Sdk? get sdk => _sdk; SnippetEditingController? get snippetEditingController => _snippetEditingControllers[_sdk]; @@ -142,12 +129,13 @@ class PlaygroundState with ChangeNotifier { return snippetEditingController?.isChanged ?? false; } + // TODO(alexeyinkin): Single source of truth for whether graph is supported, https://github.com/apache/beam/issues/23251 bool get graphAvailable => selectedExample?.type != ExampleType.test && - [SDK.java, SDK.python].contains(sdk); + [Sdk.java, Sdk.python].contains(sdk); void setExample( - ExampleModel example, { + Example example, { required bool setCurrentSdk, }) { if (setCurrentSdk) { @@ -173,7 +161,7 @@ class PlaygroundState with ChangeNotifier { } void setSdk( - SDK sdk, { + Sdk sdk, { bool notify = true, }) { _sdk = sdk; @@ -233,7 +221,7 @@ class PlaygroundState with ChangeNotifier { final parsedPipelineOptions = parsePipelineOptions(controller.pipelineOptions); if (parsedPipelineOptions == null) { - _result = RunCodeResult( + _result = const RunCodeResult( status: RunCodeStatus.compileError, errorMessage: kPipelineOptionsParseError, ); @@ -245,7 +233,7 @@ class PlaygroundState with ChangeNotifier { if (!isExampleChanged && controller.selectedExample?.outputs != null) { _showPrecompiledResult(controller); } else { - final request = RunCodeRequestWrapper( + final request = RunCodeRequest( code: controller.codeController.text, sdk: controller.sdk, pipelineOptions: parsedPipelineOptions, @@ -265,28 +253,31 @@ class PlaygroundState with ChangeNotifier { } Future cancelRun() async { - _runSubscription?.cancel(); + await _runSubscription?.cancel(); final pipelineUuid = result?.pipelineUuid ?? ''; + if (pipelineUuid.isNotEmpty) { await _codeRepository?.cancelExecution(pipelineUuid); } + _result = RunCodeResult( status: RunCodeStatus.finished, output: _result?.output, log: (_result?.log ?? '') + kExecutionCancelledText, graph: _result?.graph, ); - String log = _result?.log ?? ''; - String output = _result?.output ?? ''; + + final log = _result?.log ?? ''; + final output = _result?.output ?? ''; setOutputResult(log + output); - _executionTime?.close(); + await _executionTime?.close(); notifyListeners(); } Future _showPrecompiledResult( SnippetEditingController snippetEditingController, ) async { - _result = RunCodeResult( + _result = const RunCodeResult( status: RunCodeStatus.preparation, ); final selectedExample = snippetEditingController.selectedExample!; @@ -303,8 +294,8 @@ class PlaygroundState with ChangeNotifier { graph: selectedExample.graph, ); - filterOutput(selectedOutputFilterType ?? OutputType.all); - _executionTime?.close(); + filterOutput(selectedOutputFilterType); + await _executionTime?.close(); notifyListeners(); } @@ -359,7 +350,7 @@ class PlaygroundState with ChangeNotifier { Future getSnippetId() { final controller = requireSnippetEditingController(); - return exampleState.getSnippetId( + return exampleCache.getSnippetId( files: [SharedFile(code: controller.codeController.text, isMain: true)], sdk: controller.sdk, pipelineOptions: controller.pipelineOptions, @@ -377,4 +368,32 @@ class PlaygroundState with ChangeNotifier { .toList(growable: false), ); } + + late BeamShortcut runShortcut = BeamShortcut( + shortcuts: LogicalKeySet( + LogicalKeyboardKey.meta, + LogicalKeyboardKey.enter, + ), + actionIntent: const RunIntent(), + createAction: (BuildContext context) => CallbackAction( + onInvoke: (_) => runCode(), + ), + ); + + late BeamShortcut resetShortcut = BeamShortcut( + shortcuts: LogicalKeySet( + LogicalKeyboardKey.meta, + LogicalKeyboardKey.shift, + LogicalKeyboardKey.keyE, + ), + actionIntent: const ResetIntent(), + createAction: (BuildContext context) => CallbackAction( + onInvoke: (_) => reset(), + ), + ); + + List get shortcuts => [ + runShortcut, + resetShortcut, + ]; } diff --git a/playground/frontend/lib/modules/editor/controllers/snippet_editing_controller.dart b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart similarity index 80% rename from playground/frontend/lib/modules/editor/controllers/snippet_editing_controller.dart rename to playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart index 92acbfc6d434..e687a16d451d 100644 --- a/playground/frontend/lib/modules/editor/controllers/snippet_editing_controller.dart +++ b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart @@ -18,20 +18,21 @@ import 'package:code_text_field/code_text_field.dart'; import 'package:flutter/widgets.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; + +import '../models/example.dart'; +import '../models/example_loading_descriptors/content_example_loading_descriptor.dart'; +import '../models/example_loading_descriptors/example_loading_descriptor.dart'; +import '../models/sdk.dart'; class SnippetEditingController extends ChangeNotifier { - final SDK sdk; + final Sdk sdk; final CodeController codeController; - ExampleModel? _selectedExample; + Example? _selectedExample; String _pipelineOptions; SnippetEditingController({ required this.sdk, - ExampleModel? selectedExample, + Example? selectedExample, String pipelineOptions = '', }) : codeController = CodeController( language: sdk.highlightMode, @@ -40,14 +41,14 @@ class SnippetEditingController extends ChangeNotifier { _selectedExample = selectedExample, _pipelineOptions = pipelineOptions; - set selectedExample(ExampleModel? value) { + set selectedExample(Example? value) { _selectedExample = value; codeController.text = _selectedExample?.source ?? ''; _pipelineOptions = _selectedExample?.pipelineOptions ?? ''; notifyListeners(); } - ExampleModel? get selectedExample => _selectedExample; + Example? get selectedExample => _selectedExample; set pipelineOptions(String value) { _pipelineOptions = value; @@ -77,7 +78,8 @@ class SnippetEditingController extends ChangeNotifier { /// current content. ExampleLoadingDescriptor getLoadingDescriptor() { // TODO: Return other classes for unchanged standard examples, - // user-shared examples, and an empty editor. + // user-shared examples, and an empty editor, + // https://github.com/apache/beam/issues/23252 return ContentExampleLoadingDescriptor( content: codeController.text, name: _selectedExample?.name, diff --git a/playground/frontend/lib/modules/examples/models/category_model.dart b/playground/frontend/playground_components/lib/src/models/category_with_examples.dart similarity index 56% rename from playground/frontend/lib/modules/examples/models/category_model.dart rename to playground/frontend/playground_components/lib/src/models/category_with_examples.dart index 20a224bb3a56..e9b1ba392537 100644 --- a/playground/frontend/lib/modules/examples/models/category_model.dart +++ b/playground/frontend/playground_components/lib/src/models/category_with_examples.dart @@ -16,23 +16,31 @@ * limitations under the License. */ -import 'package:playground/constants/params.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; +import 'example_base.dart'; -class CategoryModel with Comparable { - final String name; - final List examples; +const _pinnedTitle = 'quick start'; - const CategoryModel({required this.name, required this.examples}); +class CategoryWithExamples with Comparable { + // TODO(alexeyinkin): Sort on the backend instead, then make const constructor, https://github.com/apache/beam/issues/23083 + final bool isPinned; + final String title; + final List examples; + + CategoryWithExamples({ + required this.title, + required this.examples, + }) : isPinned = title.toLowerCase() == _pinnedTitle; @override - int compareTo(CategoryModel other) { - if (name.toLowerCase() == kQuickStartCategoryName) { + int compareTo(CategoryWithExamples other) { + if (isPinned && !other.isPinned) { return -1; } - if (other.name.toLowerCase() == kQuickStartCategoryName) { + + if (!isPinned && other.isPinned) { return 1; } - return name.toLowerCase().compareTo(other.name.toLowerCase()); + + return title.toLowerCase().compareTo(other.title.toLowerCase()); } } diff --git a/playground/frontend/playground_components/lib/src/models/example.dart b/playground/frontend/playground_components/lib/src/models/example.dart new file mode 100644 index 000000000000..e35856a6264f --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example.dart @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'example_base.dart'; + +class Example extends ExampleBase { + final String source; + final String? outputs; + final String? logs; + final String? graph; + + const Example({ + required super.sdk, + required super.type, + required super.name, + required super.path, + required super.description, + super.contextLine, + super.isMultiFile, + super.link, + required super.pipelineOptions, + required this.source, + this.outputs, + this.logs, + this.graph, + }); + + Example.fromBase( + ExampleBase example, { + required this.source, + required this.outputs, + required this.logs, + this.graph, + }) : super( + sdk: example.sdk, + name: example.name, + path: example.path, + description: example.description, + type: example.type, + contextLine: example.contextLine, + isMultiFile: example.isMultiFile, + link: example.link, + pipelineOptions: example.pipelineOptions, + ); +} diff --git a/playground/frontend/lib/modules/examples/models/example_model.dart b/playground/frontend/playground_components/lib/src/models/example_base.dart similarity index 60% rename from playground/frontend/lib/modules/examples/models/example_model.dart rename to playground/frontend/playground_components/lib/src/models/example_base.dart index 054dddb500a4..2a340e56ba2b 100644 --- a/playground/frontend/lib/modules/examples/models/example_model.dart +++ b/playground/frontend/playground_components/lib/src/models/example_base.dart @@ -16,7 +16,9 @@ * limitations under the License. */ -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:equatable/equatable.dart'; + +import 'sdk.dart'; enum ExampleType { all, @@ -40,22 +42,18 @@ extension ExampleTypeToString on ExampleType { } } -class ExampleModel with Comparable { - final SDK sdk; +class ExampleBase with Comparable, EquatableMixin { + final Sdk sdk; final ExampleType type; final String name; final String path; final String description; - int contextLine; - bool isMultiFile; - String? link; - String? source; - String? outputs; - String? logs; - String? pipelineOptions; - String? graph; + final int contextLine; + final bool isMultiFile; + final String? link; + final String pipelineOptions; - ExampleModel({ + const ExampleBase({ required this.sdk, required this.name, required this.path, @@ -64,47 +62,14 @@ class ExampleModel with Comparable { this.contextLine = 1, this.isMultiFile = false, this.link, - this.source, - this.outputs, - this.logs, - this.pipelineOptions, - this.graph, + required this.pipelineOptions, }); - setSource(String source) { - this.source = source; - } - - setOutputs(String outputs) { - this.outputs = outputs; - } - - setLogs(String logs) { - this.logs = logs; - } - - setGraph(String graph) { - this.graph = graph; - } - - setContextLine(int contextLine) { - this.contextLine = contextLine; - } - - bool isInfoFetched() { - // checking only source, because outputs/logs can be empty - return source?.isNotEmpty ?? false; - } - - @override - bool operator ==(Object other) => - identical(this, other) || (other is ExampleModel && path == other.path); - @override - int get hashCode => path.hashCode; + List get props => [path]; @override - int compareTo(ExampleModel other) { + int compareTo(ExampleBase other) { return name.toLowerCase().compareTo(other.name.toLowerCase()); } } diff --git a/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart new file mode 100644 index 000000000000..085ee1d9dc0c --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/catalog_default_example_loading_descriptor.dart @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../sdk.dart'; +import 'example_loading_descriptor.dart'; + +class CatalogDefaultExampleLoadingDescriptor extends ExampleLoadingDescriptor { + final Sdk sdk; + + const CatalogDefaultExampleLoadingDescriptor({ + required this.sdk, + }); + + @override + List get props => [sdk.id]; +} diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/content_example_loading_descriptor.dart similarity index 70% rename from playground/frontend/lib/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart rename to playground/frontend/playground_components/lib/src/models/example_loading_descriptors/content_example_loading_descriptor.dart index f981f8a9d018..13be92a506d0 100644 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/content_example_loading_descriptor.dart @@ -16,9 +16,8 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_origin.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import '../sdk.dart'; +import 'example_loading_descriptor.dart'; class ContentExampleLoadingDescriptor extends ExampleLoadingDescriptor { /// The source code. @@ -27,7 +26,7 @@ class ContentExampleLoadingDescriptor extends ExampleLoadingDescriptor { /// The name of the example, if any, to show in the dropdown. final String? name; - final SDK sdk; + final Sdk sdk; const ContentExampleLoadingDescriptor({ required this.content, @@ -61,35 +60,17 @@ class ContentExampleLoadingDescriptor extends ExampleLoadingDescriptor { return map['name']?.toString(); } - static SDK? _tryParseSdk(Map map) { - return SDK.tryParse(map['sdk']); + static Sdk? _tryParseSdk(Map map) { + return Sdk.tryParse(map['sdk']); } @override - int get hashCode => Object.hash( - content.hashCode, - sdk.hashCode, - ); - - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - - return other is ContentExampleLoadingDescriptor && - content == other.content && - name == other.name && - sdk == other.sdk; - } - - @override - ExampleOrigin get origin => ExampleOrigin.content; + List get props => [content, sdk.id]; @override Map toJson() => { 'content': content, 'name': name, - 'sdk': sdk.name, + 'sdk': sdk.id, }; } diff --git a/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/empty_example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/empty_example_loading_descriptor.dart new file mode 100644 index 000000000000..74721712b66a --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/empty_example_loading_descriptor.dart @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../sdk.dart'; +import 'example_loading_descriptor.dart'; + +class EmptyExampleLoadingDescriptor extends ExampleLoadingDescriptor { + final Sdk sdk; + + const EmptyExampleLoadingDescriptor({ + required this.sdk, + }); + + @override + List get props => [sdk]; +} diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/example_loading_descriptor.dart similarity index 79% rename from playground/frontend/lib/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart rename to playground/frontend/playground_components/lib/src/models/example_loading_descriptors/example_loading_descriptor.dart index 4867544e5056..35c3cf18100a 100644 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/example_loading_descriptor.dart @@ -16,15 +16,10 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_origin.dart'; +import 'package:equatable/equatable.dart'; -abstract class ExampleLoadingDescriptor { +abstract class ExampleLoadingDescriptor with EquatableMixin { const ExampleLoadingDescriptor(); - ExampleOrigin get origin; - - @override - String toString() => '$origin'; - - Map toJson(); + Map toJson() => throw UnimplementedError(); } diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/examples_loading_descriptor.dart similarity index 88% rename from playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart rename to playground/frontend/playground_components/lib/src/models/example_loading_descriptors/examples_loading_descriptor.dart index 0fce67a0d243..6a221376c3b8 100644 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/examples_loading_descriptor.dart @@ -17,21 +17,24 @@ */ import 'package:collection/collection.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:meta/meta.dart'; +import '../sdk.dart'; +import 'example_loading_descriptor.dart'; + +@immutable class ExamplesLoadingDescriptor { /// The descriptors to be loaded right away. final List descriptors; /// The descriptors to be loaded when an SDK is selected /// that has nothing loaded yet. - final Map> lazyLoadDescriptors; + final Map> lazyLoadDescriptors; /// If set, sets the SDK to this and does not change it when loading /// new examples. Otherwise sets the SDK to that of each loaded example /// of [descriptors]. - final SDK? initialSdk; + final Sdk? initialSdk; const ExamplesLoadingDescriptor({ required this.descriptors, @@ -46,7 +49,7 @@ class ExamplesLoadingDescriptor { buffer.write(descriptors.map((e) => e.toString()).join('_')); for (final descriptor in lazyLoadDescriptors.entries) { - buffer.write(', Lazy Load ${descriptor.key.name}: '); + buffer.write(', Lazy Load ${descriptor.key.id}: '); buffer.write(descriptor.value.map((e) => e.toString()).join('_')); } diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_response.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/standard_example_loading_descriptor.dart similarity index 75% rename from playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_response.dart rename to playground/frontend/playground_components/lib/src/models/example_loading_descriptors/standard_example_loading_descriptor.dart index 609c61df3ad0..8ac57c624a92 100644 --- a/playground/frontend/lib/modules/examples/repositories/models/get_list_of_examples_response.dart +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/standard_example_loading_descriptor.dart @@ -16,11 +16,15 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/category_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'example_loading_descriptor.dart'; -class GetListOfExampleResponse { - final Map> categories; +class StandardExampleLoadingDescriptor extends ExampleLoadingDescriptor { + final String path; - GetListOfExampleResponse(this.categories); + const StandardExampleLoadingDescriptor({ + required this.path, + }); + + @override + List get props => [path]; } diff --git a/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart new file mode 100644 index 000000000000..1bcbe0dc60af --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/user_shared_example_loading_descriptor.dart @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'example_loading_descriptor.dart'; + +class UserSharedExampleLoadingDescriptor extends ExampleLoadingDescriptor { + final String snippetId; + + const UserSharedExampleLoadingDescriptor({ + required this.snippetId, + }); + + @override + List get props => [snippetId]; +} diff --git a/playground/frontend/playground_components/lib/src/models/intents.dart b/playground/frontend/playground_components/lib/src/models/intents.dart new file mode 100644 index 000000000000..74ef7c4c557e --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/intents.dart @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/widgets.dart'; + +class BeamIntent extends Intent { + final String slug; + + const BeamIntent({ + required this.slug, + }); +} + +class ResetIntent extends BeamIntent { + const ResetIntent() : super(slug: 'intents.playground.reset'); +} + +class RunIntent extends BeamIntent { + const RunIntent() : super(slug: 'intents.playground.run'); +} diff --git a/playground/frontend/lib/modules/examples/models/outputs_model.dart b/playground/frontend/playground_components/lib/src/models/outputs.dart similarity index 89% rename from playground/frontend/lib/modules/examples/models/outputs_model.dart rename to playground/frontend/playground_components/lib/src/models/outputs.dart index 19f2feaa50bd..fa1a360063c0 100644 --- a/playground/frontend/lib/modules/examples/models/outputs_model.dart +++ b/playground/frontend/playground_components/lib/src/models/outputs.dart @@ -23,10 +23,14 @@ enum OutputType { graph, } -class OutputsModel { +class Outputs { final String output; final String graph; final String log; - OutputsModel(this.output, this.graph, this.log); + const Outputs({ + required this.output, + required this.graph, + required this.log, + }); } diff --git a/playground/frontend/playground_components/lib/src/models/sdk.dart b/playground/frontend/playground_components/lib/src/models/sdk.dart new file mode 100644 index 000000000000..ae29dc1a06f3 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/sdk.dart @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:highlight/highlight_core.dart'; +import 'package:highlight/languages/go.dart' as mode_go; +import 'package:highlight/languages/java.dart' as mode_java; +import 'package:highlight/languages/python.dart' as mode_python; +import 'package:highlight/languages/scala.dart' as mode_scala; + +class Sdk with EquatableMixin { + final String id; + final String title; + + const Sdk({ + required this.id, + required this.title, + }); + + static const java = Sdk(id: 'java', title: 'Java'); + static const go = Sdk(id: 'go', title: 'Go'); + static const python = Sdk(id: 'python', title: 'Python'); + static const scio = Sdk(id: 'scio', title: 'SCIO'); + + static const known = [ + java, + go, + python, + scio, + ]; + + @override + List get props => [ + id, + title, + ]; + + /// A temporary solution while we wait for the backend to add + /// sdk in example responses. + static Sdk? tryParseExamplePath(String? path) { + if (path == null) { + return null; + } + + if (path.startsWith('SDK_JAVA')) { + return java; + } + + if (path.startsWith('SDK_GO')) { + return go; + } + + if (path.startsWith('SDK_PYTHON')) { + return python; + } + + if (path.startsWith('SDK_SCIO')) { + return scio; + } + + return null; + } + + static Sdk? tryParse(Object? value) { + if (value is! String) { + return null; + } + + return known.firstWhereOrNull((e) => e.id == value); + } + + static final _idToHighlightMode = { + Sdk.java.id: mode_java.java, + Sdk.go.id: mode_go.go, + Sdk.python: mode_python.python, + Sdk.scio: mode_scala.scala, + }; + + Mode? get highlightMode => _idToHighlightMode[id]; +} diff --git a/playground/frontend/lib/components/horizontal_divider/horizontal_divider.dart b/playground/frontend/playground_components/lib/src/models/shortcut.dart similarity index 56% rename from playground/frontend/lib/components/horizontal_divider/horizontal_divider.dart rename to playground/frontend/playground_components/lib/src/models/shortcut.dart index f935eade1025..ff4ede80f91d 100644 --- a/playground/frontend/lib/components/horizontal_divider/horizontal_divider.dart +++ b/playground/frontend/playground_components/lib/src/models/shortcut.dart @@ -17,25 +17,34 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/sizes.dart'; +import 'package:flutter/services.dart'; -/// Replaces the Flutter's Divider which is buggy with HTML renderer, -/// see https://github.com/flutter/flutter/issues/46339 -class HorizontalDivider extends StatelessWidget { - final double? indent; +import 'intents.dart'; - const HorizontalDivider({ - super.key, - this.indent, +class BeamShortcut { + final LogicalKeySet shortcuts; + final BeamIntent actionIntent; + final CallbackAction Function(BuildContext) createAction; + + BeamShortcut({ + required this.shortcuts, + required this.actionIntent, + required this.createAction, }); - @override - Widget build(BuildContext context) { - return Container( - height: kDividerHeight, - margin: EdgeInsets.fromLTRB(indent ?? 0, 0, indent ?? 0, 0), - color: ThemeColors.of(context).divider, - ); + static const _metaKeyName = 'CMD/CTRL'; + static const _glue = ' + '; + + String get title { + return shortcuts.keys + .map(getKeyDisplayName) + .join(_glue); + } + + String getKeyDisplayName(LogicalKeyboardKey e) { + if (e.keyId == LogicalKeyboardKey.meta.keyId) { + return _metaKeyName; + } + return e.keyLabel; } } diff --git a/playground/frontend/lib/modules/notifications/components/base_notification.dart b/playground/frontend/playground_components/lib/src/notifications/base_notification.dart similarity index 82% rename from playground/frontend/lib/modules/notifications/components/base_notification.dart rename to playground/frontend/playground_components/lib/src/notifications/base_notification.dart index e115f8dc578d..fe23950959ca 100644 --- a/playground/frontend/lib/modules/notifications/components/base_notification.dart +++ b/playground/frontend/playground_components/lib/src/notifications/base_notification.dart @@ -18,8 +18,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground/constants/font_weight.dart'; -import 'package:playground/constants/sizes.dart'; + +import '../constants/sizes.dart'; const kNotificationBorderWidth = 4.0; const kMaxTextWidth = 300.0; @@ -31,12 +31,12 @@ class BaseNotification extends StatelessWidget { final String asset; const BaseNotification({ - Key? key, + super.key, required this.title, required this.notification, required this.color, required this.asset, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -58,8 +58,8 @@ class BaseNotification extends StatelessWidget { decoration: BoxDecoration( color: color, borderRadius: const BorderRadius.only( - topLeft: Radius.circular(kLgBorderRadius), - bottomLeft: Radius.circular(kLgBorderRadius), + topLeft: Radius.circular(BeamSizes.size8), + bottomLeft: Radius.circular(BeamSizes.size8), ), ), ), @@ -70,19 +70,18 @@ class BaseNotification extends StatelessWidget { final textTheme = Theme.of(context).textTheme.bodyText1; return Positioned( child: Padding( - padding: const EdgeInsets.all(kLgSpacing), + padding: const EdgeInsets.all(BeamSizes.size12), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ SvgPicture.asset(asset), - const SizedBox(width: kLgSpacing), + const SizedBox(width: BeamSizes.size12), Wrap( direction: Axis.vertical, - spacing: kSmSpacing, + spacing: BeamSizes.size4, children: [ Text( title, - style: textTheme?.copyWith(fontWeight: kBoldWeight), + style: textTheme?.copyWith(fontWeight: FontWeight.w600), ), SizedBox( width: kMaxTextWidth, diff --git a/playground/frontend/lib/modules/notifications/components/notification.dart b/playground/frontend/playground_components/lib/src/notifications/notification.dart similarity index 82% rename from playground/frontend/lib/modules/notifications/components/notification.dart rename to playground/frontend/playground_components/lib/src/notifications/notification.dart index 9fcb25e8af3b..264361174c56 100644 --- a/playground/frontend/lib/modules/notifications/components/notification.dart +++ b/playground/frontend/playground_components/lib/src/notifications/notification.dart @@ -18,9 +18,10 @@ import 'package:aligned_dialog/aligned_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:playground/constants/assets.dart'; -import 'package:playground/constants/colors.dart'; -import 'package:playground/modules/notifications/components/base_notification.dart'; + +import '../constants/colors.dart'; +import '../generated/assets.gen.dart'; +import 'base_notification.dart'; const kDialogOffset = Offset(0, 30); @@ -35,8 +36,8 @@ class NotificationManager { BaseNotification( title: title, notification: notification, - color: kErrorNotificationColor, - asset: kErrorNotificationIconAsset, + color: BeamNotificationColors.error, + asset: Assets.notificationIcons.error, ), ); } @@ -51,8 +52,8 @@ class NotificationManager { BaseNotification( title: title, notification: notification, - color: kInfoNotificationColor, - asset: kInfoNotificationIconAsset, + color: BeamNotificationColors.info, + asset: Assets.notificationIcons.info, ), ); } @@ -67,8 +68,8 @@ class NotificationManager { BaseNotification( title: title, notification: notification, - color: kWarningNotificationColor, - asset: kWarningNotificationIconAsset, + color: BeamNotificationColors.warning, + asset: Assets.notificationIcons.warning, ), ); } @@ -83,8 +84,8 @@ class NotificationManager { BaseNotification( title: title, notification: notification, - color: kSuccessNotificationColor, - asset: kSuccessNotificationIconAsset, + color: BeamNotificationColors.success, + asset: Assets.notificationIcons.success, ), ); } diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/code_client.dart b/playground/frontend/playground_components/lib/src/repositories/code_client/code_client.dart similarity index 66% rename from playground/frontend/lib/modules/editor/repository/code_repository/code_client/code_client.dart rename to playground/frontend/playground_components/lib/src/repositories/code_client/code_client.dart index 849429c120af..2024e0aa82b8 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/code_client.dart +++ b/playground/frontend/playground_components/lib/src/repositories/code_client/code_client.dart @@ -16,53 +16,45 @@ * limitations under the License. */ -import 'package:playground/modules/editor/repository/code_repository/code_client/check_status_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/run_code_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_request.dart'; +import '../models/check_status_response.dart'; +import '../models/output_response.dart'; +import '../models/run_code_request.dart'; +import '../models/run_code_response.dart'; abstract class CodeClient { - Future runCode(RunCodeRequestWrapper request); + Future runCode(RunCodeRequest request); Future cancelExecution(String pipelineUuid); Future checkStatus( String pipelineUuid, - RunCodeRequestWrapper request, ); Future getCompileOutput( String pipelineUuid, - RunCodeRequestWrapper request, ); Future getRunOutput( String pipelineUuid, - RunCodeRequestWrapper request, ); Future getLogOutput( String pipelineUuid, - RunCodeRequestWrapper request, ); Future getRunErrorOutput( String pipelineUuid, - RunCodeRequestWrapper request, ); Future getValidationErrorOutput( String pipelineUuid, - RunCodeRequestWrapper request, ); Future getPreparationErrorOutput( String pipelineUuid, - RunCodeRequestWrapper request, ); Future getGraphOutput( String pipelineUuid, - RunCodeRequestWrapper request, ); } diff --git a/playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart b/playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart new file mode 100644 index 000000000000..729ce4cd19f9 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/repositories/code_client/grpc_code_client.dart @@ -0,0 +1,256 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:grpc/grpc.dart'; + +import '../../api/iis_workaround_channel.dart'; +import '../../api/v1/api.pbgrpc.dart' as grpc; +import '../../models/sdk.dart'; +import '../../util/pipeline_options.dart'; +import '../../util/replace_incorrect_symbols.dart'; +import '../models/check_status_response.dart'; +import '../models/output_response.dart'; +import '../models/run_code_error.dart'; +import '../models/run_code_request.dart'; +import '../models/run_code_response.dart'; +import '../models/run_code_result.dart'; +import '../sdk_grpc_extension.dart'; +import 'code_client.dart'; + +const kGeneralError = 'Failed to execute code'; + +class GrpcCodeClient implements CodeClient { + final grpc.PlaygroundServiceClient _defaultClient; + final Map _runnerUrlsById; + + factory GrpcCodeClient({ + required String url, + required Map runnerUrlsById, + }) { + final channel = IisWorkaroundChannel.xhr( + Uri.parse(url), + ); + + return GrpcCodeClient._( + client: grpc.PlaygroundServiceClient(channel), + runnerUrlsById: runnerUrlsById, + ); + } + + GrpcCodeClient._({ + required grpc.PlaygroundServiceClient client, + required Map runnerUrlsById, + }) : _defaultClient = client, + _runnerUrlsById = runnerUrlsById; + + @override + Future runCode(RunCodeRequest request) async { + final client = _createRunCodeClient(request.sdk); + final response = await _runSafely( + () => client.runCode(_grpcRunCodeRequest(request)), + ); + + return RunCodeResponse( + pipelineUuid: response.pipelineUuid, + ); + } + + @override + Future cancelExecution(String pipelineUuid) { + return _runSafely(() => + _defaultClient.cancel(grpc.CancelRequest(pipelineUuid: pipelineUuid))); + } + + @override + Future checkStatus( + String pipelineUuid, + ) async { + final response = await _runSafely( + () => _defaultClient.checkStatus( + grpc.CheckStatusRequest(pipelineUuid: pipelineUuid), + ), + ); + + return CheckStatusResponse( + status: _toClientStatus(response.status), + ); + } + + @override + Future getCompileOutput( + String pipelineUuid, + ) async { + final response = await _runSafely( + () => _defaultClient.getCompileOutput( + grpc.GetCompileOutputRequest(pipelineUuid: pipelineUuid), + ), + ); + + return _toOutputResponse(response.output); + } + + @override + Future getRunOutput( + String pipelineUuid, + ) async { + try { + final response = await _runSafely( + () => _defaultClient.getRunOutput( + grpc.GetRunOutputRequest(pipelineUuid: pipelineUuid), + ), + ); + + return _toOutputResponse(response.output); + } catch (ex) { + print(ex); + return _toOutputResponse(''); + } + } + + @override + Future getLogOutput( + String pipelineUuid, + ) async { + try { + final response = await _defaultClient.getLogs( + grpc.GetLogsRequest(pipelineUuid: pipelineUuid), + ); + + return _toOutputResponse(response.output); + } catch (ex) { + print(ex); + return _toOutputResponse(''); + } + } + + @override + Future getRunErrorOutput( + String pipelineUuid, + ) async { + final response = await _defaultClient.getRunError( + grpc.GetRunErrorRequest(pipelineUuid: pipelineUuid), + ); + + return _toOutputResponse(response.output); + } + + @override + Future getValidationErrorOutput( + String pipelineUuid, + ) async { + final response = await _defaultClient.getValidationOutput( + grpc.GetValidationOutputRequest(pipelineUuid: pipelineUuid), + ); + + return _toOutputResponse(response.output); + } + + @override + Future getPreparationErrorOutput( + String pipelineUuid, + ) async { + final response = await _defaultClient.getPreparationOutput( + grpc.GetPreparationOutputRequest(pipelineUuid: pipelineUuid), + ); + + return _toOutputResponse(response.output); + } + + @override + Future getGraphOutput( + String pipelineUuid, + ) async { + try { + final response = await _defaultClient.getGraph( + grpc.GetGraphRequest(pipelineUuid: pipelineUuid), + ); + + return OutputResponse(output: response.graph); + } catch (ex) { + print(ex); + return _toOutputResponse(''); + } + } + + Future _runSafely(Future Function() invoke) async { + try { + return await invoke(); + } on GrpcError catch (error) { + throw RunCodeError(message: error.message); + } on Exception catch (_) { + throw const RunCodeError(); + } + } + + /// Run Code request should use different urls for each sdk + /// instead of the default one, because we need to code + /// sdk services for it + grpc.PlaygroundServiceClient _createRunCodeClient(Sdk sdk) { + final apiClientURL = _runnerUrlsById[sdk.id]; + + if (apiClientURL == null) { + throw Exception('Runner not found for ${sdk.id}'); + } + + final channel = IisWorkaroundChannel.xhr( + Uri.parse(apiClientURL), + ); + return grpc.PlaygroundServiceClient(channel); + } + + grpc.RunCodeRequest _grpcRunCodeRequest(RunCodeRequest request) { + return grpc.RunCodeRequest() + ..code = request.code + ..sdk = request.sdk.grpc + ..pipelineOptions = pipelineOptionsToString(request.pipelineOptions); + } + + RunCodeStatus _toClientStatus(grpc.Status status) { + switch (status) { + case grpc.Status.STATUS_UNSPECIFIED: + return RunCodeStatus.unspecified; + case grpc.Status.STATUS_VALIDATING: + case grpc.Status.STATUS_PREPARING: + return RunCodeStatus.preparation; + case grpc.Status.STATUS_COMPILING: + return RunCodeStatus.compiling; + case grpc.Status.STATUS_EXECUTING: + return RunCodeStatus.executing; + case grpc.Status.STATUS_CANCELED: + case grpc.Status.STATUS_FINISHED: + return RunCodeStatus.finished; + case grpc.Status.STATUS_COMPILE_ERROR: + return RunCodeStatus.compileError; + case grpc.Status.STATUS_RUN_TIMEOUT: + return RunCodeStatus.timeout; + case grpc.Status.STATUS_RUN_ERROR: + return RunCodeStatus.runError; + case grpc.Status.STATUS_VALIDATION_ERROR: + return RunCodeStatus.validationError; + case grpc.Status.STATUS_PREPARATION_ERROR: + return RunCodeStatus.preparationError; + case grpc.Status.STATUS_ERROR: + return RunCodeStatus.unknownError; + } + return RunCodeStatus.unspecified; + } + + OutputResponse _toOutputResponse(String response) { + return OutputResponse(output: replaceIncorrectSymbols(response)); + } +} diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_repository.dart b/playground/frontend/playground_components/lib/src/repositories/code_repository.dart similarity index 76% rename from playground/frontend/lib/modules/editor/repository/code_repository/code_repository.dart rename to playground/frontend/playground_components/lib/src/repositories/code_repository.dart index 37ad6ee0d1fe..c9efc3146c09 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/code_repository.dart +++ b/playground/frontend/playground_components/lib/src/repositories/code_repository.dart @@ -16,12 +16,12 @@ * limitations under the License. */ -import 'package:playground/modules/editor/repository/code_repository/code_client/code_client.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_error.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_request.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_result.dart'; -import 'package:playground/utils/run_with_retry.dart'; +import '../util/run_with_retry.dart'; +import 'code_client/code_client.dart'; +import 'models/output_response.dart'; +import 'models/run_code_error.dart'; +import 'models/run_code_request.dart'; +import 'models/run_code_result.dart'; const kPipelineCheckDelay = Duration(seconds: 1); const kTimeoutErrorText = @@ -33,25 +33,25 @@ const kUnknownErrorText = 'Something went wrong. Please try again later or create a GitHub issue'; const kProcessingStartedText = 'The processing has started\n'; +// TODO(alexeyinkin): Rename. This is not a repository but a higher level client. class CodeRepository { - late final CodeClient _client; + final CodeClient _client; - CodeRepository(CodeClient client) { - _client = client; - } + CodeRepository({required CodeClient client,}): _client = client; - Stream runCode(RunCodeRequestWrapper request) async* { + Stream runCode(RunCodeRequest request) async* { try { - final initResult = RunCodeResult( + const initResult = RunCodeResult( status: RunCodeStatus.preparation, log: kProcessingStartedText, ); yield initResult; - var runCodeResponse = await _client.runCode(request); + + final runCodeResponse = await _client.runCode(request); final pipelineUuid = runCodeResponse.pipelineUuid; + yield* _checkPipelineExecution( pipelineUuid, - request, prevResult: initResult, ); } on RunCodeError catch (error) { @@ -68,26 +68,23 @@ class CodeRepository { } Stream _checkPipelineExecution( - String pipelineUuid, - RunCodeRequestWrapper request, { + String pipelineUuid, { RunCodeResult? prevResult, }) async* { try { final statusResponse = await runWithRetry( - () => _client.checkStatus(pipelineUuid, request), + () => _client.checkStatus(pipelineUuid), ); final result = await _getPipelineResult( pipelineUuid, statusResponse.status, prevResult, - request, ); yield result; if (!result.isFinished) { await Future.delayed(kPipelineCheckDelay); yield* _checkPipelineExecution( pipelineUuid, - request, prevResult: result, ); } @@ -105,17 +102,14 @@ class CodeRepository { String pipelineUuid, RunCodeStatus status, RunCodeResult? prevResult, - RunCodeRequestWrapper request, ) async { final prevOutput = prevResult?.output ?? ''; final prevLog = prevResult?.log ?? ''; final prevGraph = prevResult?.graph ?? ''; + switch (status) { case RunCodeStatus.compileError: - final compileOutput = await _client.getCompileOutput( - pipelineUuid, - request, - ); + final compileOutput = await _client.getCompileOutput(pipelineUuid); return RunCodeResult( pipelineUuid: pipelineUuid, status: status, @@ -123,6 +117,7 @@ class CodeRepository { log: prevLog, graph: prevGraph, ); + case RunCodeStatus.timeout: return RunCodeResult( pipelineUuid: pipelineUuid, @@ -132,8 +127,9 @@ class CodeRepository { log: prevLog, graph: prevGraph, ); + case RunCodeStatus.runError: - final output = await _client.getRunErrorOutput(pipelineUuid, request); + final output = await _client.getRunErrorOutput(pipelineUuid); return RunCodeResult( pipelineUuid: pipelineUuid, status: status, @@ -141,24 +137,27 @@ class CodeRepository { log: prevLog, graph: prevGraph, ); + case RunCodeStatus.validationError: final output = - await _client.getValidationErrorOutput(pipelineUuid, request); + await _client.getValidationErrorOutput(pipelineUuid); return RunCodeResult( status: status, output: output.output, log: prevLog, graph: prevGraph, ); + case RunCodeStatus.preparationError: final output = - await _client.getPreparationErrorOutput(pipelineUuid, request); + await _client.getPreparationErrorOutput(pipelineUuid); return RunCodeResult( status: status, output: output.output, log: prevLog, graph: prevGraph, ); + case RunCodeStatus.unknownError: return RunCodeResult( pipelineUuid: pipelineUuid, @@ -168,13 +167,14 @@ class CodeRepository { log: prevLog, graph: prevGraph, ); + case RunCodeStatus.executing: final responses = await Future.wait([ - _client.getRunOutput(pipelineUuid, request), - _client.getLogOutput(pipelineUuid, request), + _client.getRunOutput(pipelineUuid), + _client.getLogOutput(pipelineUuid), prevGraph.isEmpty - ? _client.getGraphOutput(pipelineUuid, request) - : Future.value(OutputResponse(prevGraph)), + ? _client.getGraphOutput(pipelineUuid) + : Future.value(OutputResponse(output: prevGraph)), ]); final output = responses[0]; final log = responses[1]; @@ -186,14 +186,15 @@ class CodeRepository { log: prevLog + log.output, graph: graph.output, ); + case RunCodeStatus.finished: final responses = await Future.wait([ - _client.getRunOutput(pipelineUuid, request), - _client.getLogOutput(pipelineUuid, request), - _client.getRunErrorOutput(pipelineUuid, request), + _client.getRunOutput(pipelineUuid), + _client.getLogOutput(pipelineUuid), + _client.getRunErrorOutput(pipelineUuid), prevGraph.isEmpty - ? _client.getGraphOutput(pipelineUuid, request) - : Future.value(OutputResponse(prevGraph)), + ? _client.getGraphOutput(pipelineUuid) + : Future.value(OutputResponse(output: prevGraph)), ]); final output = responses[0]; final log = responses[1]; @@ -206,6 +207,7 @@ class CodeRepository { log: prevLog + log.output, graph: graph.output, ); + default: return RunCodeResult( pipelineUuid: pipelineUuid, diff --git a/playground/frontend/playground_components/lib/src/repositories/example_client/example_client.dart b/playground/frontend/playground_components/lib/src/repositories/example_client/example_client.dart new file mode 100644 index 000000000000..d2bc5a792a93 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/repositories/example_client/example_client.dart @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../models/get_default_precompiled_object_request.dart'; +import '../models/get_precompiled_object_code_response.dart'; +import '../models/get_precompiled_object_request.dart'; +import '../models/get_precompiled_object_response.dart'; +import '../models/get_precompiled_objects_request.dart'; +import '../models/get_precompiled_objects_response.dart'; +import '../models/get_snippet_request.dart'; +import '../models/get_snippet_response.dart'; +import '../models/output_response.dart'; +import '../models/save_snippet_request.dart'; +import '../models/save_snippet_response.dart'; + +abstract class ExampleClient { + Future getPrecompiledObjects( + GetPrecompiledObjectsRequest request, + ); + + Future getPrecompiledObjectCode( + GetPrecompiledObjectRequest request, + ); + + Future getDefaultPrecompiledObject( + GetDefaultPrecompiledObjectRequest request, + ); + + Future getPrecompiledObject( + GetPrecompiledObjectRequest request, + ); + + Future getPrecompiledObjectOutput( + GetPrecompiledObjectRequest request, + ); + + Future getPrecompiledObjectLogs( + GetPrecompiledObjectRequest request, + ); + + Future getPrecompiledObjectGraph( + GetPrecompiledObjectRequest request, + ); + + Future getSnippet( + GetSnippetRequest request, + ); + + Future saveSnippet( + SaveSnippetRequest request, + ); +} diff --git a/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart b/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart new file mode 100644 index 000000000000..ceaa08a9bdf5 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/repositories/example_client/grpc_example_client.dart @@ -0,0 +1,376 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:grpc/grpc.dart'; + +import '../../api/iis_workaround_channel.dart'; +import '../../api/v1/api.pbgrpc.dart' as grpc; +import '../../models/category_with_examples.dart'; +import '../../models/example_base.dart'; +import '../../models/sdk.dart'; +import '../../util/replace_incorrect_symbols.dart'; +import '../models/get_default_precompiled_object_request.dart'; +import '../models/get_precompiled_object_code_response.dart'; +import '../models/get_precompiled_object_request.dart'; +import '../models/get_precompiled_object_response.dart'; +import '../models/get_precompiled_objects_request.dart'; +import '../models/get_precompiled_objects_response.dart'; +import '../models/get_snippet_request.dart'; +import '../models/get_snippet_response.dart'; +import '../models/output_response.dart'; +import '../models/save_snippet_request.dart'; +import '../models/save_snippet_response.dart'; +import '../models/shared_file.dart'; +import '../sdk_grpc_extension.dart'; +import 'example_client.dart'; + +class GrpcExampleClient implements ExampleClient { + final grpc.PlaygroundServiceClient _defaultClient; + + factory GrpcExampleClient({ + required String url, + }) { + final channel = IisWorkaroundChannel.xhr( + Uri.parse(url), + ); + + return GrpcExampleClient._( + client: grpc.PlaygroundServiceClient(channel), + ); + } + + GrpcExampleClient._({ + required grpc.PlaygroundServiceClient client, + }) : _defaultClient = client; + + @override + Future getPrecompiledObjects( + GetPrecompiledObjectsRequest request, + ) async { + final response = await _runSafely( + () => _defaultClient.getPrecompiledObjects( + _grpcGetPrecompiledObjectsRequest(request), + ), + ); + return GetPrecompiledObjectsResponse( + categories: _toClientCategories(response.sdkCategories), + ); + } + + @override + Future getDefaultPrecompiledObject( + GetDefaultPrecompiledObjectRequest request, + ) async { + final response = await _runSafely( + () => _defaultClient.getDefaultPrecompiledObject( + _grpcGetDefaultPrecompiledObjectRequest(request), + ), + ); + + return GetPrecompiledObjectResponse( + example: _toExampleModel( + request.sdk, + response.precompiledObject, + ), + ); + } + + @override + Future getPrecompiledObject( + GetPrecompiledObjectRequest request, + ) async { + final response = await _runSafely( + () => _defaultClient.getPrecompiledObject( + grpc.GetPrecompiledObjectRequest()..cloudPath = request.path, + ), + ); + + return GetPrecompiledObjectResponse( + example: _toExampleModel( + request.sdk, + response.precompiledObject, + ), + ); + } + + @override + Future getPrecompiledObjectCode( + GetPrecompiledObjectRequest request, + ) async { + final response = await _runSafely( + () => _defaultClient.getPrecompiledObjectCode( + _grpcGetPrecompiledObjectRequest(request), + ), + ); + + return GetPrecompiledObjectCodeResponse( + code: replaceIncorrectSymbols(response.code), + ); + } + + @override + Future getPrecompiledObjectOutput( + GetPrecompiledObjectRequest request, + ) async { + try { + final response = await _runSafely( + () => _defaultClient.getPrecompiledObjectOutput( + _grpcGetPrecompiledObjectOutputRequest(request), + ), + ); + + return OutputResponse( + output: replaceIncorrectSymbols(response.output), + ); + } catch (ex) { + print(ex); + return OutputResponse( + output: '', + ); + } + } + + @override + Future getPrecompiledObjectLogs( + GetPrecompiledObjectRequest request, + ) async { + try { + final response = await _runSafely( + () => _defaultClient.getPrecompiledObjectLogs( + _grpcGetPrecompiledObjectLogRequest(request), + ), + ); + + return OutputResponse( + output: replaceIncorrectSymbols(response.output), + ); + } catch (ex) { + print(ex); + return OutputResponse( + output: '', + ); + } + } + + @override + Future getPrecompiledObjectGraph( + GetPrecompiledObjectRequest request, + ) async { + try { + final response = await _runSafely( + () => _defaultClient.getPrecompiledObjectGraph( + _grpcGetPrecompiledGraphRequest(request), + ), + ); + + return OutputResponse( + output: response.graph, + ); + } catch (ex) { + print(ex); + return OutputResponse( + output: '', + ); + } + } + + @override + Future getSnippet( + GetSnippetRequest request, + ) async { + final response = await _runSafely( + () => _defaultClient.getSnippet( + _grpcGetSnippetRequest(request), + ), + ); + + return GetSnippetResponse( + files: _convertToSharedFileList(response.files), + sdk: response.sdk.model, + pipelineOptions: response.pipelineOptions, + ); + } + + @override + Future saveSnippet( + SaveSnippetRequest request, + ) async { + final response = await _runSafely( + () => _defaultClient.saveSnippet( + _grpcSaveSnippetRequest(request), + ), + ); + + return SaveSnippetResponse( + id: response.id, + ); + } + + Future _runSafely(Future Function() invoke) async { + try { + return await invoke(); + } on GrpcError catch (error) { + throw Exception(error.message); + } + } + + grpc.GetPrecompiledObjectsRequest _grpcGetPrecompiledObjectsRequest( + GetPrecompiledObjectsRequest request, + ) { + return grpc.GetPrecompiledObjectsRequest() + ..category = request.category ?? '' + ..sdk = request.sdk?.grpc ?? grpc.Sdk.SDK_UNSPECIFIED; + } + + grpc.GetDefaultPrecompiledObjectRequest + _grpcGetDefaultPrecompiledObjectRequest( + GetDefaultPrecompiledObjectRequest request, + ) { + return grpc.GetDefaultPrecompiledObjectRequest()..sdk = request.sdk.grpc; + } + + grpc.GetPrecompiledObjectCodeRequest _grpcGetPrecompiledObjectRequest( + GetPrecompiledObjectRequest request, + ) { + return grpc.GetPrecompiledObjectCodeRequest()..cloudPath = request.path; + } + + grpc.GetPrecompiledObjectOutputRequest _grpcGetPrecompiledObjectOutputRequest( + GetPrecompiledObjectRequest request, + ) { + return grpc.GetPrecompiledObjectOutputRequest()..cloudPath = request.path; + } + + grpc.GetPrecompiledObjectLogsRequest _grpcGetPrecompiledObjectLogRequest( + GetPrecompiledObjectRequest request, + ) { + return grpc.GetPrecompiledObjectLogsRequest()..cloudPath = request.path; + } + + grpc.GetPrecompiledObjectGraphRequest _grpcGetPrecompiledGraphRequest( + GetPrecompiledObjectRequest request, + ) { + return grpc.GetPrecompiledObjectGraphRequest()..cloudPath = request.path; + } + + grpc.GetSnippetRequest _grpcGetSnippetRequest( + GetSnippetRequest request, + ) { + return grpc.GetSnippetRequest()..id = request.id; + } + + grpc.SaveSnippetRequest _grpcSaveSnippetRequest( + SaveSnippetRequest request, + ) { + return grpc.SaveSnippetRequest() + ..sdk = request.sdk.grpc + ..pipelineOptions = request.pipelineOptions + ..files.addAll(_convertToSnippetFileList(request.files)); + } + + ExampleType _exampleTypeFromString(grpc.PrecompiledObjectType type) { + switch (type) { + case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_EXAMPLE: + return ExampleType.example; + case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_KATA: + return ExampleType.kata; + case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_UNIT_TEST: + return ExampleType.test; + case grpc.PrecompiledObjectType.PRECOMPILED_OBJECT_TYPE_UNSPECIFIED: + return ExampleType.all; + } + + return ExampleType.example; + } + + Map> _toClientCategories( + List response, + ) { + final result = >{}; + + for (final sdkMap in response) { + final sdk = sdkMap.sdk.model; + final categoriesForSdk = []; + + for (final category in sdkMap.categories) { + final examples = category.precompiledObjects + .map((example) => _toExampleModel(sdk, example)) + .toList(growable: false) + ..sort(); + + categoriesForSdk.add( + CategoryWithExamples( + title: category.categoryName, + examples: examples, + ), + ); + } + + result[sdk] = categoriesForSdk..sort(); + } + + return result; + } + + ExampleBase _toExampleModel(Sdk sdk, grpc.PrecompiledObject example) { + return ExampleBase( + sdk: sdk, + name: example.name, + description: example.description, + type: _exampleTypeFromString(example.type), + path: example.cloudPath, + contextLine: example.contextLine, + pipelineOptions: example.pipelineOptions, + isMultiFile: example.multifile, + link: example.link, + ); + } + + List _convertToSharedFileList( + List snippetFileList, + ) { + final sharedFilesList = []; + + for (final item in snippetFileList) { + sharedFilesList.add(SharedFile( + code: item.content, + isMain: item.isMain, + name: item.name, + )); + } + + return sharedFilesList; + } + + List _convertToSnippetFileList( + List sharedFilesList, + ) { + final snippetFileList = []; + + for (final item in sharedFilesList) { + snippetFileList.add( + grpc.SnippetFile() + ..name = item.name + ..isMain = true + ..content = item.code, + ); + } + + return snippetFileList; + } +} diff --git a/playground/frontend/playground_components/lib/src/repositories/example_repository.dart b/playground/frontend/playground_components/lib/src/repositories/example_repository.dart new file mode 100644 index 000000000000..9cb624a3464b --- /dev/null +++ b/playground/frontend/playground_components/lib/src/repositories/example_repository.dart @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '../models/category_with_examples.dart'; +import '../models/example_base.dart'; +import '../models/sdk.dart'; +import 'example_client/example_client.dart'; +import 'models/get_default_precompiled_object_request.dart'; +import 'models/get_precompiled_object_request.dart'; +import 'models/get_precompiled_objects_request.dart'; +import 'models/get_snippet_request.dart'; +import 'models/get_snippet_response.dart'; +import 'models/save_snippet_request.dart'; + +class ExampleRepository { + final ExampleClient _client; + + ExampleRepository({ + required ExampleClient client, + }) : _client = client; + + Future>> getListOfExamples( + GetPrecompiledObjectsRequest request, + ) async { + final result = await _client.getPrecompiledObjects(request); + return result.categories; + } + + Future getDefaultExample( + GetDefaultPrecompiledObjectRequest request, + ) async { + final result = await _client.getDefaultPrecompiledObject(request); + return result.example; + } + + Future getExampleSource( + GetPrecompiledObjectRequest request, + ) async { + final result = await _client.getPrecompiledObjectCode(request); + return result.code; + } + + Future getExampleOutput( + GetPrecompiledObjectRequest request, + ) async { + final result = await _client.getPrecompiledObjectOutput(request); + return result.output; + } + + Future getExampleLogs( + GetPrecompiledObjectRequest request, + ) async { + final result = await _client.getPrecompiledObjectLogs(request); + return result.output; + } + + Future getExampleGraph( + GetPrecompiledObjectRequest request, + ) async { + final result = await _client.getPrecompiledObjectGraph(request); + return result.output; + } + + Future getExample( + GetPrecompiledObjectRequest request, + ) async { + final result = await _client.getPrecompiledObject(request); + return result.example; + } + + Future getSnippet( + GetSnippetRequest request, + ) async { + final result = await _client.getSnippet(request); + return result; + } + + Future saveSnippet( + SaveSnippetRequest request, + ) async { + final result = await _client.saveSnippet(request); + return result.id; + } +} diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/check_status_response.dart b/playground/frontend/playground_components/lib/src/repositories/models/check_status_response.dart similarity index 87% rename from playground/frontend/lib/modules/editor/repository/code_repository/code_client/check_status_response.dart rename to playground/frontend/playground_components/lib/src/repositories/models/check_status_response.dart index 7e77419305ce..089a9a340bd4 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/check_status_response.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/check_status_response.dart @@ -16,10 +16,12 @@ * limitations under the License. */ -import 'package:playground/modules/editor/repository/code_repository/run_code_result.dart'; +import 'run_code_result.dart'; class CheckStatusResponse { final RunCodeStatus status; - CheckStatusResponse(this.status); + const CheckStatusResponse({ + required this.status, + }); } diff --git a/playground/frontend/playground_components/lib/src/repositories/models/get_default_precompiled_object_request.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_default_precompiled_object_request.dart new file mode 100644 index 000000000000..4b42dee69518 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_default_precompiled_object_request.dart @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:equatable/equatable.dart'; + +import '../../models/sdk.dart'; + +class GetDefaultPrecompiledObjectRequest with EquatableMixin { + final Sdk sdk; + + const GetDefaultPrecompiledObjectRequest({ + required this.sdk, + }); + + @override + List get props => [ + sdk, + ]; +} diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_example_code_response.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart similarity index 87% rename from playground/frontend/lib/modules/examples/repositories/models/get_example_code_response.dart rename to playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart index c7a57e93f3f8..a7bbda2459d8 100644 --- a/playground/frontend/lib/modules/examples/repositories/models/get_example_code_response.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_code_response.dart @@ -16,8 +16,10 @@ * limitations under the License. */ -class GetExampleCodeResponse { +class GetPrecompiledObjectCodeResponse { final String code; - GetExampleCodeResponse(this.code); + const GetPrecompiledObjectCodeResponse({ + required this.code, + }); } diff --git a/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_request.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_request.dart new file mode 100644 index 000000000000..95516e905a15 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_request.dart @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:equatable/equatable.dart'; + +import '../../models/sdk.dart'; + +/// A data object for multiple requests querying separate fields +/// of a given example. +class GetPrecompiledObjectRequest with EquatableMixin { + final String path; + final Sdk sdk; + + const GetPrecompiledObjectRequest({ + required this.path, + required this.sdk, + }); + + @override + List get props => [ + path, + sdk, + ]; +} diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_example_response.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_response.dart similarity index 81% rename from playground/frontend/lib/modules/examples/repositories/models/get_example_response.dart rename to playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_response.dart index 002c737a1a0a..0c6ce3d5b59e 100644 --- a/playground/frontend/lib/modules/examples/repositories/models/get_example_response.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_object_response.dart @@ -16,10 +16,12 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_model.dart'; +import '../../models/example_base.dart'; -class GetExampleResponse { - final ExampleModel example; +class GetPrecompiledObjectResponse { + final ExampleBase example; - GetExampleResponse(this.example); + const GetPrecompiledObjectResponse({ + required this.example, + }); } diff --git a/playground/frontend/lib/modules/shortcuts/models/shortcut.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_objects_request.dart similarity index 70% rename from playground/frontend/lib/modules/shortcuts/models/shortcut.dart rename to playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_objects_request.dart index 0aafe060b052..c5c5b7246c31 100644 --- a/playground/frontend/lib/modules/shortcuts/models/shortcut.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_objects_request.dart @@ -16,18 +16,22 @@ * limitations under the License. */ -import 'package:flutter/material.dart'; +import 'package:equatable/equatable.dart'; -class Shortcut { - final String name; - final LogicalKeySet shortcuts; - final Intent actionIntent; - final CallbackAction Function(BuildContext) createAction; +import '../../models/sdk.dart'; - Shortcut({ - required this.name, - required this.shortcuts, - required this.actionIntent, - required this.createAction, +class GetPrecompiledObjectsRequest with EquatableMixin { + final Sdk? sdk; + final String? category; + + const GetPrecompiledObjectsRequest({ + required this.sdk, + required this.category, }); + + @override + List get props => [ + sdk, + category, + ]; } diff --git a/playground/frontend/lib/configure_web.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_objects_response.dart similarity index 76% rename from playground/frontend/lib/configure_web.dart rename to playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_objects_response.dart index 5fa7eb6de546..69901df75c66 100644 --- a/playground/frontend/lib/configure_web.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_precompiled_objects_response.dart @@ -16,8 +16,13 @@ * limitations under the License. */ -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import '../../models/category_with_examples.dart'; +import '../../models/sdk.dart'; -void configureApp() { - setUrlStrategy(PathUrlStrategy()); +class GetPrecompiledObjectsResponse { + final Map> categories; + + const GetPrecompiledObjectsResponse({ + required this.categories, + }); } diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_snippet_request.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_request.dart similarity index 92% rename from playground/frontend/lib/modules/examples/repositories/models/get_snippet_request.dart rename to playground/frontend/playground_components/lib/src/repositories/models/get_snippet_request.dart index 4319884981d9..610cf0d67b74 100644 --- a/playground/frontend/lib/modules/examples/repositories/models/get_snippet_request.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_request.dart @@ -16,10 +16,10 @@ * limitations under the License. */ -class GetSnippetRequestWrapper { +class GetSnippetRequest { final String id; - const GetSnippetRequestWrapper({ + const GetSnippetRequest({ required this.id, }); } diff --git a/playground/frontend/lib/modules/examples/repositories/models/get_snippet_response.dart b/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart similarity index 86% rename from playground/frontend/lib/modules/examples/repositories/models/get_snippet_response.dart rename to playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart index 167f725a5fb8..9cb9c8ec1280 100644 --- a/playground/frontend/lib/modules/examples/repositories/models/get_snippet_response.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/get_snippet_response.dart @@ -16,12 +16,12 @@ * limitations under the License. */ -import 'package:playground/modules/examples/repositories/models/shared_file_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import '../../models/sdk.dart'; +import 'shared_file.dart'; class GetSnippetResponse { final List files; - final SDK sdk; + final Sdk sdk; final String pipelineOptions; const GetSnippetResponse({ diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/output_response.dart b/playground/frontend/playground_components/lib/src/repositories/models/output_response.dart similarity index 87% rename from playground/frontend/lib/modules/editor/repository/code_repository/code_client/output_response.dart rename to playground/frontend/playground_components/lib/src/repositories/models/output_response.dart index c8835c707b23..d33292cf3333 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/output_response.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/output_response.dart @@ -16,8 +16,11 @@ * limitations under the License. */ +/// A common response for anything returning a single string. class OutputResponse { final String output; - OutputResponse(this.output); + const OutputResponse({ + required this.output, + }); } diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_error.dart b/playground/frontend/playground_components/lib/src/repositories/models/run_code_error.dart similarity index 94% rename from playground/frontend/lib/modules/editor/repository/code_repository/run_code_error.dart rename to playground/frontend/playground_components/lib/src/repositories/models/run_code_error.dart index b9339612ea58..68379a2473bb 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_error.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/run_code_error.dart @@ -19,5 +19,7 @@ class RunCodeError implements Exception { final String? message; - RunCodeError(this.message); + const RunCodeError({ + this.message, + }); } diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_request.dart b/playground/frontend/playground_components/lib/src/repositories/models/run_code_request.dart similarity index 88% rename from playground/frontend/lib/modules/editor/repository/code_repository/run_code_request.dart rename to playground/frontend/playground_components/lib/src/repositories/models/run_code_request.dart index aa09fcd0bffb..16a1e74df430 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_request.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/run_code_request.dart @@ -16,14 +16,14 @@ * limitations under the License. */ -import 'package:playground/modules/sdk/models/sdk.dart'; +import '../../models/sdk.dart'; -class RunCodeRequestWrapper { +class RunCodeRequest { final String code; - final SDK sdk; + final Sdk sdk; final Map pipelineOptions; - RunCodeRequestWrapper({ + const RunCodeRequest({ required this.code, required this.sdk, required this.pipelineOptions, diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/run_code_response.dart b/playground/frontend/playground_components/lib/src/repositories/models/run_code_response.dart similarity index 93% rename from playground/frontend/lib/modules/editor/repository/code_repository/code_client/run_code_response.dart rename to playground/frontend/playground_components/lib/src/repositories/models/run_code_response.dart index 2fe480828f67..20f1a147972d 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/code_client/run_code_response.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/run_code_response.dart @@ -19,5 +19,7 @@ class RunCodeResponse { final String pipelineUuid; - RunCodeResponse(this.pipelineUuid); + const RunCodeResponse({ + required this.pipelineUuid, + }); } diff --git a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_result.dart b/playground/frontend/playground_components/lib/src/repositories/models/run_code_result.dart similarity index 75% rename from playground/frontend/lib/modules/editor/repository/code_repository/run_code_result.dart rename to playground/frontend/playground_components/lib/src/repositories/models/run_code_result.dart index 30a686e73721..5a3bcd876916 100644 --- a/playground/frontend/lib/modules/editor/repository/code_repository/run_code_result.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/run_code_result.dart @@ -16,7 +16,7 @@ * limitations under the License. */ -import 'package:flutter/material.dart'; +import 'package:equatable/equatable.dart'; enum RunCodeStatus { unspecified, @@ -42,7 +42,7 @@ const kFinishedStatuses = [ RunCodeStatus.finished, ]; -class RunCodeResult { +class RunCodeResult with EquatableMixin { final RunCodeStatus status; final String? pipelineUuid; final String? output; @@ -50,7 +50,7 @@ class RunCodeResult { final String? graph; final String? errorMessage; - RunCodeResult({ + const RunCodeResult({ required this.status, this.pipelineUuid, this.output, @@ -64,20 +64,14 @@ class RunCodeResult { } @override - bool operator ==(Object other) => - identical(this, other) || - other is RunCodeResult && - runtimeType == other.runtimeType && - pipelineUuid == other.pipelineUuid && - status == other.status && - output == other.output && - log == other.log && - graph == other.graph && - errorMessage == other.errorMessage; - - @override - int get hashCode => - hashValues(pipelineUuid, status, output, log, errorMessage, graph); + List get props => [ + status, + pipelineUuid, + output, + log, + graph, + errorMessage, + ]; @override String toString() { diff --git a/playground/frontend/lib/modules/examples/repositories/models/save_snippet_request.dart b/playground/frontend/playground_components/lib/src/repositories/models/save_snippet_request.dart similarity index 80% rename from playground/frontend/lib/modules/examples/repositories/models/save_snippet_request.dart rename to playground/frontend/playground_components/lib/src/repositories/models/save_snippet_request.dart index c29ee4230f6a..4d64416efcc3 100644 --- a/playground/frontend/lib/modules/examples/repositories/models/save_snippet_request.dart +++ b/playground/frontend/playground_components/lib/src/repositories/models/save_snippet_request.dart @@ -16,15 +16,15 @@ * limitations under the License. */ -import 'package:playground/modules/examples/repositories/models/shared_file_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import '../../models/sdk.dart'; +import 'shared_file.dart'; -class SaveSnippetRequestWrapper { +class SaveSnippetRequest { final List files; - final SDK sdk; + final Sdk sdk; final String pipelineOptions; - const SaveSnippetRequestWrapper({ + const SaveSnippetRequest({ required this.files, required this.sdk, required this.pipelineOptions, diff --git a/playground/frontend/lib/modules/examples/repositories/models/save_snippet_response.dart b/playground/frontend/playground_components/lib/src/repositories/models/save_snippet_response.dart similarity index 100% rename from playground/frontend/lib/modules/examples/repositories/models/save_snippet_response.dart rename to playground/frontend/playground_components/lib/src/repositories/models/save_snippet_response.dart diff --git a/playground/frontend/lib/modules/examples/repositories/models/shared_file_model.dart b/playground/frontend/playground_components/lib/src/repositories/models/shared_file.dart similarity index 100% rename from playground/frontend/lib/modules/examples/repositories/models/shared_file_model.dart rename to playground/frontend/playground_components/lib/src/repositories/models/shared_file.dart diff --git a/playground/frontend/lib/modules/shortcuts/utils/shortcuts_display_name.dart b/playground/frontend/playground_components/lib/src/repositories/sdk_grpc_extension.dart similarity index 53% rename from playground/frontend/lib/modules/shortcuts/utils/shortcuts_display_name.dart rename to playground/frontend/playground_components/lib/src/repositories/sdk_grpc_extension.dart index eff27c1c446c..d9c1b5863303 100644 --- a/playground/frontend/lib/modules/shortcuts/utils/shortcuts_display_name.dart +++ b/playground/frontend/playground_components/lib/src/repositories/sdk_grpc_extension.dart @@ -16,21 +16,36 @@ * limitations under the License. */ -import 'package:flutter/services.dart'; -import 'package:playground/modules/shortcuts/models/shortcut.dart'; -const kMetaKeyName = 'CMD/CTRL'; -const kShortcutKeyJoinSymbol = ' + '; +import '../api/v1/api.pbgrpc.dart' as g; +import '../models/sdk.dart'; -String getShortcutDisplayName(Shortcut shortcut) { - return shortcut.shortcuts.keys - .map((e) => getKeyDisplayName(e)) - .join(kShortcutKeyJoinSymbol); +extension SdkExtension on Sdk { + static final _idToGrpcEnum = { + Sdk.java.id: g.Sdk.SDK_JAVA, + Sdk.go.id: g.Sdk.SDK_GO, + Sdk.python.id: g.Sdk.SDK_PYTHON, + Sdk.scio.id: g.Sdk.SDK_SCIO, + }; + + g.Sdk get grpc => + _idToGrpcEnum[id] ?? + (throw Exception('SDK not supported for GRPS: $id')); } -String getKeyDisplayName(LogicalKeyboardKey e) { - if (e.keyId == LogicalKeyboardKey.meta.keyId) { - return kMetaKeyName; +extension GrpcSdkExtension on g.Sdk { + Sdk get model { + switch (this) { + case g.Sdk.SDK_JAVA: + return Sdk.java; + case g.Sdk.SDK_GO: + return Sdk.go; + case g.Sdk.SDK_PYTHON: + return Sdk.python; + case g.Sdk.SDK_SCIO: + return Sdk.scio; + } + + return Sdk(id: '$value', title: name); } - return e.keyLabel; } diff --git a/playground/frontend/playground_components/lib/src/theme/color_provider.dart b/playground/frontend/playground_components/lib/src/theme/color_provider.dart deleted file mode 100644 index c158229a53db..000000000000 --- a/playground/frontend/playground_components/lib/src/theme/color_provider.dart +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../constants/colors.dart'; - -class ThemeColorsProvider extends StatelessWidget { - final ThemeColors data; - final Widget child; - - const ThemeColorsProvider({ - super.key, - required this.data, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return Provider.value( - value: data, - child: child, - ); - } -} - -class ThemeColors { - final Color? _background; - final bool isDark; - - ThemeColors({ - required this.isDark, - Color? background, - }) : _background = background; - - static ThemeColors of(BuildContext context, {bool listen = true}) { - return Provider.of(context, listen: listen); - } - - const ThemeColors.fromBrightness({ - required this.isDark, - }) : _background = null; - - Color get divider => - isDark ? BeamDarkThemeColors.grey : BeamLightThemeColors.grey; - - Color get primary => - isDark ? BeamDarkThemeColors.primary : BeamLightThemeColors.primary; - - Color get primaryBackgroundTextColor => BeamColors.white; - - Color get lightGreyBackgroundTextColor => BeamColors.black; - - Color get secondaryBackground => isDark - ? BeamDarkThemeColors.secondaryBackground - : BeamLightThemeColors.secondaryBackground; - - Color get background => - _background ?? - (isDark - ? BeamDarkThemeColors.primaryBackground - : BeamLightThemeColors.primaryBackground); - - Color get textColor => - isDark ? BeamDarkThemeColors.text : BeamLightThemeColors.text; -} diff --git a/playground/frontend/playground_components/lib/src/theme/switch_notifier.dart b/playground/frontend/playground_components/lib/src/theme/switch_notifier.dart index 06f7bcad5725..28fe46c58ded 100644 --- a/playground/frontend/playground_components/lib/src/theme/switch_notifier.dart +++ b/playground/frontend/playground_components/lib/src/theme/switch_notifier.dart @@ -20,23 +20,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'color_provider.dart'; - const kThemeMode = 'theme_mode'; class ThemeSwitchNotifier extends ChangeNotifier { ThemeMode themeMode = ThemeMode.light; - static const _darkThemeColors = ThemeColors.fromBrightness(isDark: true); - static const _lightThemeColors = ThemeColors.fromBrightness(isDark: false); - - ThemeColors get themeColors { - if (themeMode == ThemeMode.dark) { - return _darkThemeColors; - } - return _lightThemeColors; - } - void init() { _setPreferences(); } @@ -73,12 +61,7 @@ class ThemeSwitchNotifierProvider extends StatelessWidget { Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => ThemeSwitchNotifier()..init(), - child: Consumer( - builder: (context, themeSwitchNotifier, _) => ThemeColorsProvider( - data: themeSwitchNotifier.themeColors, - child: child, - ), - ), + child: child, ); } } diff --git a/playground/frontend/playground_components/lib/src/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart index 88c644a0604c..9f6625316b94 100644 --- a/playground/frontend/playground_components/lib/src/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -16,43 +16,226 @@ * limitations under the License. */ +import 'package:code_text_field/code_text_field.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../playground_components.dart'; +const codeFontSize = 14.0; + +class BeamThemeExtension extends ThemeExtension { + final Color borderColor; + final Color fieldBackgroundColor; + final Color iconColor; + final Color primaryBackgroundTextColor; + final Color lightGreyBackgroundTextColor; + final Color secondaryBackgroundColor; + + final TextStyle codeRootStyle; + final CodeThemeData codeTheme; + + const BeamThemeExtension({ + required this.borderColor, + required this.fieldBackgroundColor, + required this.iconColor, + required this.primaryBackgroundTextColor, + required this.lightGreyBackgroundTextColor, + required this.secondaryBackgroundColor, + required this.codeRootStyle, + required this.codeTheme, + }); + + @override + ThemeExtension copyWith({ + Color? borderColor, + Color? fieldBackgroundColor, + Color? iconColor, + Color? primaryBackgroundTextColor, + Color? lightGreyBackgroundTextColor, + Color? secondaryBackgroundColor, + TextStyle? codeRootStyle, + CodeThemeData? codeTheme, + }) { + return BeamThemeExtension( + borderColor: borderColor ?? this.borderColor, + fieldBackgroundColor: fieldBackgroundColor ?? this.fieldBackgroundColor, + iconColor: iconColor ?? this.iconColor, + primaryBackgroundTextColor: + primaryBackgroundTextColor ?? this.primaryBackgroundTextColor, + lightGreyBackgroundTextColor: + lightGreyBackgroundTextColor ?? this.lightGreyBackgroundTextColor, + secondaryBackgroundColor: + secondaryBackgroundColor ?? this.secondaryBackgroundColor, + codeRootStyle: codeRootStyle ?? this.codeRootStyle, + codeTheme: codeTheme ?? this.codeTheme, + ); + } + + @override + ThemeExtension lerp( + covariant BeamThemeExtension? other, + double t, + ) { + return BeamThemeExtension( + borderColor: Color.lerp(borderColor, other?.borderColor, t)!, + fieldBackgroundColor: + Color.lerp(fieldBackgroundColor, other?.fieldBackgroundColor, t)!, + iconColor: Color.lerp(iconColor, other?.iconColor, t)!, + primaryBackgroundTextColor: Color.lerp( + primaryBackgroundTextColor, other?.primaryBackgroundTextColor, t)!, + lightGreyBackgroundTextColor: Color.lerp(lightGreyBackgroundTextColor, + other?.lightGreyBackgroundTextColor, t)!, + secondaryBackgroundColor: Color.lerp( + secondaryBackgroundColor, other?.secondaryBackgroundColor, t)!, + codeRootStyle: TextStyle.lerp(codeRootStyle, other?.codeRootStyle, t)!, + codeTheme: t == 0.0 ? codeTheme : other?.codeTheme ?? codeTheme, + ); + } +} + final kLightTheme = ThemeData( brightness: Brightness.light, - primaryColor: BeamLightThemeColors.primary, + appBarTheme: _getAppBarTheme(BeamLightThemeColors.secondaryBackground), + backgroundColor: BeamLightThemeColors.primaryBackground, canvasColor: BeamLightThemeColors.primaryBackground, dividerColor: BeamLightThemeColors.grey, - scaffoldBackgroundColor: BeamLightThemeColors.secondaryBackground, - backgroundColor: BeamLightThemeColors.primaryBackground, - textTheme: _getTextTheme(BeamLightThemeColors.text), - textButtonTheme: _getTextButtonTheme(BeamLightThemeColors.text), + elevatedButtonTheme: _getElevatedButtonTheme(BeamLightThemeColors.primary), outlinedButtonTheme: _getOutlineButtonTheme( BeamLightThemeColors.text, BeamLightThemeColors.primary, ), - elevatedButtonTheme: _getElevatedButtonTheme(BeamLightThemeColors.primary), - appBarTheme: _getAppBarTheme(BeamLightThemeColors.secondaryBackground), + primaryColor: BeamLightThemeColors.primary, + scaffoldBackgroundColor: BeamLightThemeColors.secondaryBackground, + tabBarTheme: _getTabBarTheme( + textColor: BeamLightThemeColors.text, + indicatorColor: BeamLightThemeColors.primary, + ), + textButtonTheme: _getTextButtonTheme(BeamLightThemeColors.text), + textTheme: _getTextTheme(BeamLightThemeColors.text), + extensions: { + BeamThemeExtension( + borderColor: BeamLightThemeColors.border, + fieldBackgroundColor: BeamLightThemeColors.grey, + iconColor: BeamLightThemeColors.icon, + primaryBackgroundTextColor: BeamColors.white, + lightGreyBackgroundTextColor: BeamColors.black, + secondaryBackgroundColor: BeamLightThemeColors.secondaryBackground, + codeRootStyle: GoogleFonts.sourceCodePro( + backgroundColor: BeamLightThemeColors.primaryBackground, + color: BeamLightThemeColors.text, + fontSize: codeFontSize, + ), + codeTheme: CodeThemeData( + styles: { + 'root': TextStyle( + backgroundColor: BeamLightThemeColors.primaryBackground, + color: BeamLightThemeColors.text, + ), + 'comment': TextStyle(color: BeamLightThemeColors.codeComment), + 'quote': TextStyle(color: BeamLightThemeColors.code2), + 'variable': TextStyle(color: BeamLightThemeColors.code2), + 'keyword': TextStyle(color: BeamLightThemeColors.code2), + 'selector-tag': TextStyle(color: BeamLightThemeColors.code2), + 'built_in': TextStyle(color: BeamLightThemeColors.code2), + 'name': TextStyle(color: BeamLightThemeColors.code2), + 'tag': TextStyle(color: BeamLightThemeColors.code2), + 'string': TextStyle(color: BeamLightThemeColors.code1), + 'title': TextStyle(color: BeamLightThemeColors.code1), + 'section': TextStyle(color: BeamLightThemeColors.code1), + 'attribute': TextStyle(color: BeamLightThemeColors.code1), + 'literal': TextStyle(color: BeamLightThemeColors.code1), + 'template-tag': TextStyle(color: BeamLightThemeColors.code1), + 'template-variable': TextStyle(color: BeamLightThemeColors.code1), + 'type': TextStyle(color: BeamLightThemeColors.code1), + 'addition': TextStyle(color: BeamLightThemeColors.code1), + 'deletion': TextStyle(color: BeamLightThemeColors.code2), + 'selector-attr': TextStyle(color: BeamLightThemeColors.code2), + 'selector-pseudo': TextStyle(color: BeamLightThemeColors.code2), + 'meta': TextStyle(color: BeamLightThemeColors.code2), + 'doctag': TextStyle(color: BeamLightThemeColors.codeComment), + 'attr': TextStyle(color: BeamLightThemeColors.primary), + 'symbol': TextStyle(color: BeamLightThemeColors.code2), + 'bullet': TextStyle(color: BeamLightThemeColors.code2), + 'link': TextStyle(color: BeamLightThemeColors.code2), + 'emphasis': const TextStyle(fontStyle: FontStyle.italic), + 'strong': const TextStyle(fontWeight: FontWeight.bold), + }, + ), + ), + }, ); final kDarkTheme = ThemeData( brightness: Brightness.dark, - primaryColor: BeamDarkThemeColors.primary, + appBarTheme: _getAppBarTheme(BeamDarkThemeColors.secondaryBackground), + backgroundColor: BeamDarkThemeColors.primaryBackground, canvasColor: BeamDarkThemeColors.primaryBackground, dividerColor: BeamDarkThemeColors.grey, - scaffoldBackgroundColor: BeamDarkThemeColors.secondaryBackground, - backgroundColor: BeamDarkThemeColors.primaryBackground, - textTheme: _getTextTheme(BeamDarkThemeColors.text), - textButtonTheme: _getTextButtonTheme(BeamDarkThemeColors.text), + elevatedButtonTheme: _getElevatedButtonTheme(BeamDarkThemeColors.primary), outlinedButtonTheme: _getOutlineButtonTheme( BeamDarkThemeColors.text, BeamDarkThemeColors.primary, ), - elevatedButtonTheme: _getElevatedButtonTheme(BeamDarkThemeColors.primary), - appBarTheme: _getAppBarTheme(BeamDarkThemeColors.secondaryBackground), + primaryColor: BeamDarkThemeColors.primary, + scaffoldBackgroundColor: BeamDarkThemeColors.secondaryBackground, + tabBarTheme: _getTabBarTheme( + textColor: BeamDarkThemeColors.text, + indicatorColor: BeamDarkThemeColors.primary, + ), + textButtonTheme: _getTextButtonTheme(BeamDarkThemeColors.text), + textTheme: _getTextTheme(BeamDarkThemeColors.text), + extensions: { + BeamThemeExtension( + borderColor: BeamDarkThemeColors.border, + fieldBackgroundColor: BeamDarkThemeColors.grey, + iconColor: BeamDarkThemeColors.icon, + primaryBackgroundTextColor: BeamColors.white, + lightGreyBackgroundTextColor: BeamColors.black, + secondaryBackgroundColor: BeamDarkThemeColors.secondaryBackground, + codeRootStyle: GoogleFonts.sourceCodePro( + backgroundColor: BeamDarkThemeColors.primaryBackground, + color: BeamDarkThemeColors.text, + fontSize: codeFontSize, + ), + codeTheme: CodeThemeData( + styles: { + 'root': TextStyle( + backgroundColor: BeamDarkThemeColors.primaryBackground, + color: BeamDarkThemeColors.text, + ), + 'comment': TextStyle(color: BeamDarkThemeColors.codeComment), + 'quote': TextStyle(color: BeamDarkThemeColors.code2), + 'variable': TextStyle(color: BeamDarkThemeColors.code2), + 'keyword': TextStyle(color: BeamDarkThemeColors.code2), + 'selector-tag': TextStyle(color: BeamDarkThemeColors.code2), + 'built_in': TextStyle(color: BeamDarkThemeColors.code2), + 'name': TextStyle(color: BeamDarkThemeColors.code2), + 'tag': TextStyle(color: BeamDarkThemeColors.code2), + 'string': TextStyle(color: BeamDarkThemeColors.code1), + 'title': TextStyle(color: BeamDarkThemeColors.code1), + 'section': TextStyle(color: BeamDarkThemeColors.code1), + 'attribute': TextStyle(color: BeamDarkThemeColors.code1), + 'literal': TextStyle(color: BeamDarkThemeColors.code1), + 'template-tag': TextStyle(color: BeamDarkThemeColors.code1), + 'template-variable': TextStyle(color: BeamDarkThemeColors.code1), + 'type': TextStyle(color: BeamDarkThemeColors.code1), + 'addition': TextStyle(color: BeamDarkThemeColors.code1), + 'deletion': TextStyle(color: BeamDarkThemeColors.code2), + 'selector-attr': TextStyle(color: BeamDarkThemeColors.code2), + 'selector-pseudo': TextStyle(color: BeamDarkThemeColors.code2), + 'meta': TextStyle(color: BeamDarkThemeColors.code2), + 'doctag': TextStyle(color: BeamDarkThemeColors.codeComment), + 'attr': TextStyle(color: BeamDarkThemeColors.primary), + 'symbol': TextStyle(color: BeamDarkThemeColors.code2), + 'bullet': TextStyle(color: BeamDarkThemeColors.code2), + 'link': TextStyle(color: BeamDarkThemeColors.code2), + 'emphasis': const TextStyle(fontStyle: FontStyle.italic), + 'strong': const TextStyle(fontWeight: FontWeight.bold), + }, + ), + ), + }, ); TextTheme _getTextTheme(Color textColor) { @@ -87,7 +270,7 @@ TextTheme _getTextTheme(Color textColor) { titleMedium: _emptyTextStyle, titleSmall: _emptyTextStyle, labelLarge: TextStyle( - fontSize: 16, + fontSize: 14, fontWeight: FontWeight.w600, ), labelMedium: _emptyTextStyle, @@ -139,8 +322,22 @@ ElevatedButtonThemeData _getElevatedButtonTheme(Color color) { style: ElevatedButton.styleFrom( foregroundColor: BeamColors.white, backgroundColor: color, - padding: _buttonPadding, - elevation: BeamSizes.size0, + ), + ); +} + +TabBarTheme _getTabBarTheme({ + required Color textColor, + required Color indicatorColor, +}) { + const labelStyle = TextStyle(fontWeight: FontWeight.w600); + return TabBarTheme( + unselectedLabelColor: textColor, + labelColor: textColor, + labelStyle: labelStyle, + unselectedLabelStyle: labelStyle, + indicator: UnderlineTabIndicator( + borderSide: BorderSide(width: 2.0, color: indicatorColor), ), ); } diff --git a/playground/frontend/lib/modules/editor/parsers/run_options_parser.dart b/playground/frontend/playground_components/lib/src/util/pipeline_options.dart similarity index 100% rename from playground/frontend/lib/modules/editor/parsers/run_options_parser.dart rename to playground/frontend/playground_components/lib/src/util/pipeline_options.dart diff --git a/playground/frontend/lib/utils/replace_incorrect_symbols.dart b/playground/frontend/playground_components/lib/src/util/replace_incorrect_symbols.dart similarity index 87% rename from playground/frontend/lib/utils/replace_incorrect_symbols.dart rename to playground/frontend/playground_components/lib/src/util/replace_incorrect_symbols.dart index 08d75ee7dc55..69a0bb684009 100644 --- a/playground/frontend/lib/utils/replace_incorrect_symbols.dart +++ b/playground/frontend/playground_components/lib/src/util/replace_incorrect_symbols.dart @@ -16,11 +16,9 @@ * limitations under the License. */ -const kIncorrectTabSymbol = ' '; -const kTabSymbolReplacement = ' '; - +// TODO(alexeyinkin): Move to the editor, https://github.com/apache/beam/issues/23079 /// sometimes code contains incorrect symbols (like tab which doesn't look properly), /// replace it with correct ones String replaceIncorrectSymbols(String output) { - return output.replaceAll(kIncorrectTabSymbol, kTabSymbolReplacement); + return output.replaceAll('\t', ' '); } diff --git a/playground/frontend/lib/utils/run_with_retry.dart b/playground/frontend/playground_components/lib/src/util/run_with_retry.dart similarity index 100% rename from playground/frontend/lib/utils/run_with_retry.dart rename to playground/frontend/playground_components/lib/src/util/run_with_retry.dart diff --git a/playground/frontend/playground_components/lib/src/widgets/bubble.dart b/playground/frontend/playground_components/lib/src/widgets/bubble.dart new file mode 100644 index 000000000000..1f6e46a1848a --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/bubble.dart @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../constants/sizes.dart'; + +class BubbleWidget extends StatelessWidget { + final bool isSelected; + final VoidCallback onTap; + final String title; + + const BubbleWidget({ + super.key, + required this.isSelected, + required this.onTap, + required this.title, + }); + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + final ext = themeData.extension()!; + + return MouseRegion( + cursor: SystemMouseCursors.click, + child: Padding( + padding: const EdgeInsets.only(right: BeamSizes.size8), + child: GestureDetector( + onTap: onTap, + child: Container( + height: BeamSizes.buttonHeight, + padding: const EdgeInsets.symmetric(horizontal: BeamSizes.size16), + decoration: BoxDecoration( + color: isSelected ? themeData.primaryColor : ext.borderColor, + borderRadius: BorderRadius.circular(BeamBorderRadius.infinite), + ), + child: Center( + child: Text( + title, + style: TextStyle( + color: isSelected + ? ext.primaryBackgroundTextColor + : ext.lightGreyBackgroundTextColor, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/widgets/drag_indicator.dart b/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart similarity index 69% rename from playground/frontend/playground_components/lib/src/widgets/drag_indicator.dart rename to playground/frontend/playground_components/lib/src/widgets/drag_handle.dart index 768b513b9e33..7dfed2c589ae 100644 --- a/playground/frontend/playground_components/lib/src/widgets/drag_indicator.dart +++ b/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart @@ -19,17 +19,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import '../constants/names.dart'; +import '../constants/playground_components.dart'; import '../generated/assets.gen.dart'; -class DragIndicator extends StatelessWidget { - const DragIndicator(); +class DragHandle extends StatelessWidget { + final Axis direction; + + const DragHandle({ + required this.direction, + }); @override Widget build(BuildContext context) { + // TODO: Use a single file and just rotate it if needed. + // Currently it gets blurred in HTML renderer. Find a fix. return SvgPicture.asset( - Assets.svg.drag, - package: BeamNames.package, + direction == Axis.horizontal + ? Assets.svg.dragHorizontal + : Assets.svg.dragVertical, + package: PlaygroundComponents.packageName, ); } } diff --git a/playground/frontend/lib/modules/editor/components/editor_textarea.dart b/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart similarity index 80% rename from playground/frontend/lib/modules/editor/components/editor_textarea.dart rename to playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart index d3e7d3b6bda8..05231d0ee8b7 100644 --- a/playground/frontend/lib/modules/editor/components/editor_textarea.dart +++ b/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart @@ -16,14 +16,14 @@ * limitations under the License. */ +// TODO(alexeyinkin): Refactor this, merge into snippet_editor.dart + import 'package:code_text_field/code_text_field.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/fonts.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; + +import '../models/example.dart'; +import '../models/sdk.dart'; +import '../theme/theme.dart'; const kJavaRegExp = r'import\s[A-z.0-9]*\;\n\n[(\/\*\*)|(public)|(class)]'; const kPythonRegExp = r'[^\S\r\n](import|as)[^\S\r\n][A-z]*\n\n'; @@ -36,21 +36,21 @@ const kAdditionalLinesForScrolling = 4; class EditorTextArea extends StatefulWidget { final CodeController codeController; - final SDK sdk; - final ExampleModel? example; + final Sdk sdk; + final Example? example; final bool enabled; final bool isEditable; - final bool isEmbedded; + final bool goToContextLine; const EditorTextArea({ - Key? key, + super.key, required this.codeController, required this.sdk, this.example, required this.enabled, required this.isEditable, - this.isEmbedded = false, - }) : super(key: key); + required this.goToContextLine, + }); @override State createState() => _EditorTextAreaState(); @@ -68,32 +68,30 @@ class _EditorTextAreaState extends State { @override Widget build(BuildContext context) { - if (!widget.isEmbedded) { + if (widget.goToContextLine) { WidgetsBinding.instance.addPostFrameCallback((_) => _setTextScrolling()); } + final ext = Theme.of(context).extension()!; + return Semantics( container: true, textField: true, multiline: true, enabled: widget.enabled, readOnly: widget.enabled, - label: AppLocalizations.of(context)!.codeTextArea, + label: 'widgets.codeEditor.label', child: FocusScope( node: FocusScopeNode(canRequestFocus: widget.isEditable), - child: CodeField( - key: codeFieldKey, - focusNode: focusNode, - enabled: widget.enabled, - controller: widget.codeController, - textStyle: getCodeFontStyle( - textStyle: const TextStyle(fontSize: kCodeFontSize), - ), - expands: true, - lineNumberStyle: LineNumberStyle( - textStyle: TextStyle( - color: ThemeColors.of(context).grey1Color, - ), + child: CodeTheme( + data: ext.codeTheme, + child: CodeField( + key: codeFieldKey, + focusNode: focusNode, + enabled: widget.enabled, + controller: widget.codeController, + textStyle: ext.codeRootStyle, + expands: true, ), ), ), @@ -143,7 +141,7 @@ class _EditorTextAreaState extends State { codeFieldKey.currentContext?.findRenderObject() as RenderBox; double height = rBox.size.height * .75; - return height ~/ kCodeFontSize; + return height ~/ codeFontSize; } int _getIndexOfContextLine() { diff --git a/playground/frontend/lib/modules/actions/components/header_icon_button.dart b/playground/frontend/playground_components/lib/src/widgets/header_icon_button.dart similarity index 91% rename from playground/frontend/lib/modules/actions/components/header_icon_button.dart rename to playground/frontend/playground_components/lib/src/widgets/header_icon_button.dart index 09d42e8aa187..01e4a62de1c8 100644 --- a/playground/frontend/lib/modules/actions/components/header_icon_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/header_icon_button.dart @@ -17,7 +17,8 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/constants/sizes.dart'; + +import '../constants/sizes.dart'; class HeaderIconButton extends StatelessWidget { final VoidCallback onPressed; @@ -25,16 +26,16 @@ class HeaderIconButton extends StatelessWidget { final Widget icon; const HeaderIconButton({ - Key? key, + super.key, required this.onPressed, required this.label, required this.icon, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return SizedBox( - height: kHeaderButtonHeight, + height: BeamSizes.headerButtonHeight, child: TextButton.icon( icon: icon, label: Text(label), diff --git a/playground/frontend/lib/components/loading_indicator/loading_indicator.dart b/playground/frontend/playground_components/lib/src/widgets/loading_indicator.dart similarity index 83% rename from playground/frontend/lib/components/loading_indicator/loading_indicator.dart rename to playground/frontend/playground_components/lib/src/widgets/loading_indicator.dart index 8d7fb142a28d..60b696ed880f 100644 --- a/playground/frontend/lib/components/loading_indicator/loading_indicator.dart +++ b/playground/frontend/playground_components/lib/src/widgets/loading_indicator.dart @@ -17,12 +17,16 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/constants/colors.dart'; + +import '../constants/sizes.dart'; class LoadingIndicator extends StatelessWidget { final double size; - const LoadingIndicator({Key? key, required this.size}) : super(key: key); + const LoadingIndicator({ + super.key, + this.size = BeamSizes.loadingIndicator, + }); @override Widget build(BuildContext context) { @@ -30,8 +34,8 @@ class LoadingIndicator extends StatelessWidget { child: SizedBox( height: size, width: size, - child: const CircularProgressIndicator( - color: kLightPrimary, + child: CircularProgressIndicator( + color: Theme.of(context).primaryColor, ), ), ); diff --git a/playground/frontend/playground_components/lib/src/widgets/logo.dart b/playground/frontend/playground_components/lib/src/widgets/logo.dart index 61741807e3c6..2a0bdad9519b 100644 --- a/playground/frontend/playground_components/lib/src/widgets/logo.dart +++ b/playground/frontend/playground_components/lib/src/widgets/logo.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; -import '../constants/names.dart'; +import '../constants/playground_components.dart'; import '../constants/sizes.dart'; import '../generated/assets.gen.dart'; @@ -30,7 +30,7 @@ class BeamLogo extends StatelessWidget { return Image.asset( Assets.png.beamLogo.path, height: BeamIconSizes.large, - package: BeamNames.package, + package: PlaygroundComponents.packageName, ); } } diff --git a/playground/frontend/lib/modules/output/components/graph.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph.dart similarity index 83% rename from playground/frontend/lib/modules/output/components/graph.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph.dart index 01fcfbd4b08e..6f5e95c01d24 100644 --- a/playground/frontend/lib/modules/output/components/graph.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph.dart @@ -17,11 +17,11 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/graph/graph_builder/canvas_drawer.dart'; -import 'package:playground/modules/graph/graph_builder/graph_builder.dart'; -import 'package:playground/modules/graph/graph_builder/painters/graph_painter.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; + +import 'graph_builder/canvas_drawer.dart'; +import 'graph_builder/graph_builder.dart'; +import 'graph_builder/painters/graph_painter.dart'; class GraphCustomPainter extends CustomPainter { final GraphPainter graph; @@ -41,15 +41,15 @@ class GraphCustomPainter extends CustomPainter { class GraphTab extends StatefulWidget { final String graph; - final SDK sdk; - final GraphDirection direction; + final Sdk sdk; + final Axis direction; const GraphTab({ - Key? key, + super.key, required this.graph, required this.sdk, required this.direction, - }) : super(key: key); + }); @override State createState() => _GraphTabState(); @@ -88,9 +88,8 @@ class _GraphTabState extends State { return Container(); } return Padding( - padding: const EdgeInsets.all(kXlSpacing), + padding: const EdgeInsets.all(BeamSizes.size16), child: SingleChildScrollView( - scrollDirection: Axis.vertical, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: ClipRRect( diff --git a/playground/frontend/lib/modules/graph/graph_builder/canvas_drawer.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/canvas_drawer.dart similarity index 93% rename from playground/frontend/lib/modules/graph/graph_builder/canvas_drawer.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/canvas_drawer.dart index 59cbda7bb848..65a95919021e 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/canvas_drawer.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/canvas_drawer.dart @@ -20,7 +20,8 @@ import 'dart:ui'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:playground/constants/colors.dart'; + +import '../../../../constants/colors.dart'; const kDashSize = 4; const kArrowSize = 4; @@ -32,13 +33,13 @@ class CanvasDrawer { CanvasDrawer(this.canvas); final borderPaint = Paint() - ..color = kLightGrey2 + ..color = BeamGraphColors.border ..strokeWidth = 1 ..isAntiAlias = true ..style = PaintingStyle.fill; final linePaint = Paint() - ..color = kLightPrimary + ..color = BeamGraphColors.edge ..strokeWidth = 2 ..isAntiAlias = true ..style = PaintingStyle.fill; @@ -71,7 +72,7 @@ class CanvasDrawer { createParagraph( text, width, - color: kLightGrey1, + color: BeamGraphColors.node, ), offset, ); @@ -82,7 +83,6 @@ class CanvasDrawer { } drawDashedLine(double x1, double y1, double x2, double y2) { - double startX = x1; double startY = y1; @@ -123,12 +123,12 @@ class CanvasDrawer { } drawRect( - double left, - double top, - double width, - double height, - double radius, - ) { + double left, + double top, + double width, + double height, + double radius, + ) { final borderRadius = Radius.circular(radius); final rect = Rect.fromLTWH(left, top, width, height); final rRect = RRect.fromRectAndRadius(rect, borderRadius); diff --git a/playground/frontend/lib/modules/graph/graph_builder/extractors/edge_extractor.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/edge_extractor.dart similarity index 93% rename from playground/frontend/lib/modules/graph/graph_builder/extractors/edge_extractor.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/edge_extractor.dart index 1c7685d45cb3..27c62de3177b 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/extractors/edge_extractor.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/edge_extractor.dart @@ -16,8 +16,8 @@ * limitations under the License. */ -import 'package:playground/modules/graph/graph_builder/extractors/extractors.dart'; -import 'package:playground/modules/graph/models/graph.dart'; +import '../../models/graph.dart'; +import 'extractors.dart'; final RegExp kEdgeRegExp = RegExp(r'''.+ -> .+'''); const kPrimaryEdgeStyle = 'solid'; diff --git a/playground/frontend/lib/modules/graph/graph_builder/extractors/element_extractor.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/element_extractor.dart similarity index 90% rename from playground/frontend/lib/modules/graph/graph_builder/extractors/element_extractor.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/element_extractor.dart index d533e2d230fe..e4af2ec9bab9 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/extractors/element_extractor.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/element_extractor.dart @@ -16,9 +16,9 @@ * limitations under the License. */ -import 'package:playground/modules/graph/graph_builder/extractors/extractor_utils.dart'; -import 'package:playground/modules/graph/graph_builder/extractors/extractors.dart'; -import 'package:playground/modules/graph/models/graph.dart'; +import '../../models/graph.dart'; +import 'extractor_utils.dart'; +import 'extractors.dart'; final RegExp kGraphElementRegExp = RegExp(r'''subgraph cluster_0'''); final RegExp kSubgraphElementRegExp = RegExp(r'''subgraph cluster_\d+'''); diff --git a/playground/frontend/lib/modules/graph/graph_builder/extractors/extractor_utils.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/extractor_utils.dart similarity index 100% rename from playground/frontend/lib/modules/graph/graph_builder/extractors/extractor_utils.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/extractor_utils.dart diff --git a/playground/frontend/lib/modules/graph/graph_builder/extractors/extractors.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/extractors.dart similarity index 100% rename from playground/frontend/lib/modules/graph/graph_builder/extractors/extractors.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/extractors.dart diff --git a/playground/frontend/lib/modules/graph/graph_builder/extractors/label_extractor.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/label_extractor.dart similarity index 93% rename from playground/frontend/lib/modules/graph/graph_builder/extractors/label_extractor.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/label_extractor.dart index 1c04558c594a..1c9ce981388e 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/extractors/label_extractor.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/extractors/label_extractor.dart @@ -16,7 +16,7 @@ * limitations under the License. */ -import 'package:playground/modules/graph/graph_builder/extractors/extractors.dart'; +import 'extractors.dart'; final RegExp kLabelRegExp = RegExp(r'''label = ".*"'''); const kLabelStart = 'label = "'; diff --git a/playground/frontend/lib/modules/graph/graph_builder/graph_builder.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/graph_builder.dart similarity index 85% rename from playground/frontend/lib/modules/graph/graph_builder/graph_builder.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/graph_builder.dart index 53901aaa9500..9549a76fa601 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/graph_builder.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/graph_builder.dart @@ -18,15 +18,17 @@ import 'dart:convert'; -import 'package:playground/modules/graph/graph_builder/extractors/edge_extractor.dart'; -import 'package:playground/modules/graph/graph_builder/extractors/element_extractor.dart'; -import 'package:playground/modules/graph/graph_builder/extractors/label_extractor.dart'; -import 'package:playground/modules/graph/graph_builder/painters/edge_painter.dart'; -import 'package:playground/modules/graph/graph_builder/painters/graph_painter.dart'; -import 'package:playground/modules/graph/graph_builder/painters/node_painter.dart'; -import 'package:playground/modules/graph/models/graph.dart'; -import 'package:playground/modules/graph/models/table_cell.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:flutter/widgets.dart' as widgets; +import 'package:playground_components/playground_components.dart'; + +import '../models/graph.dart'; +import '../models/table_cell.dart'; +import 'extractors/edge_extractor.dart'; +import 'extractors/element_extractor.dart'; +import 'extractors/label_extractor.dart'; +import 'painters/edge_painter.dart'; +import 'painters/graph_painter.dart'; +import 'painters/node_painter.dart'; final kGraphElementExtractor = GraphElementExtractor(); final kLabelExtractor = LabelExtractor(); @@ -37,20 +39,23 @@ abstract class GraphBuilder { final List edges = []; final Map elementsMap = {}; - static GraphBuilder? parseDot(String dot, SDK sdk) { + // TODO(alexeyinkin): Use this as the source of truth + // of whether a graph is available for an SDK, + // https://github.com/apache/beam/issues/23251 + static final _graphBuilderFactoriesBySdk = { + Sdk.java: JavaGraphBuilder.new, + Sdk.python: PythonGraphBuilder.new, + }; + + static GraphBuilder? parseDot(String dot, Sdk sdk) { LineSplitter ls = const LineSplitter(); List lines = ls.convert(dot); - GraphBuilder builder; - switch (sdk) { - case SDK.java: - builder = JavaGraphBuilder(); - break; - case SDK.python: - builder = PythonGraphBuilder(); - break; - default: - return null; + final builder = _graphBuilderFactoriesBySdk[sdk]?.call(); + + if (builder == null) { + return null; } + for (var line in lines) { builder.parseNextLine(line); } @@ -60,7 +65,7 @@ abstract class GraphBuilder { void parseNextLine(String line); - GraphPainter getPainter(GraphDirection direction) { + GraphPainter getPainter(widgets.Axis direction) { final List nodeElements = elements .where((element) => element.type == NodeType.node) .toList() @@ -86,9 +91,9 @@ abstract class GraphBuilder { .map((element) { final cell = nodeToCellMap[element.name]!; final row = - direction == GraphDirection.horizontal ? cell.row : cell.column; + direction == widgets.Axis.horizontal ? cell.row : cell.column; final column = - direction == GraphDirection.horizontal ? cell.column : cell.row; + direction == widgets.Axis.horizontal ? cell.column : cell.row; return NodeElementPainter( element: element as Node, row: row, diff --git a/playground/frontend/lib/modules/graph/graph_builder/painters/edge_painter.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/edge_painter.dart similarity index 90% rename from playground/frontend/lib/modules/graph/graph_builder/painters/edge_painter.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/edge_painter.dart index d67610530104..93a3d15d9108 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/painters/edge_painter.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/edge_painter.dart @@ -18,13 +18,14 @@ import 'dart:math'; import 'package:collection/collection.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/graph/graph_builder/canvas_drawer.dart'; -import 'package:playground/modules/graph/graph_builder/painters/graph_painter.dart'; -import 'package:playground/modules/graph/graph_builder/painters/node_painter.dart'; -import 'package:playground/modules/graph/models/graph.dart'; +import 'package:flutter/widgets.dart'; -const kEdgeSpacing = 2 * kXlSpacing; +import '../../../../../constants/sizes.dart'; +import '../../models/graph.dart'; +import '../canvas_drawer.dart'; +import 'node_painter.dart'; + +const kEdgeSpacing = 2 * BeamSizes.size16; class EdgePainter { final Edge edge; @@ -38,9 +39,9 @@ class EdgePainter { Map columnStarts, Map rowSizes, Map columnSizes, - GraphDirection direction, + Axis direction, ) { - if (direction == GraphDirection.vertical) { + if (direction == Axis.vertical) { _drawVertical( drawer, elementsMap, rowStarts, columnStarts, rowSizes, columnSizes); } else { @@ -109,7 +110,7 @@ class EdgePainter { }); drawer.drawRightArrow( - optimizedMovePoints[0].x + kXlSpacing, optimizedMovePoints[0].y); + optimizedMovePoints[0].x + BeamSizes.size16, optimizedMovePoints[0].y); _drawLine(drawer, optimizedMovePoints); } @@ -174,7 +175,7 @@ class EdgePainter { }); drawer.drawBottomArrow( - optimizedMovePoints[0].x, optimizedMovePoints[0].y + kXlSpacing); + optimizedMovePoints[0].x, optimizedMovePoints[0].y + BeamSizes.size16); _drawLine(drawer, optimizedMovePoints); } diff --git a/playground/frontend/lib/modules/graph/graph_builder/painters/graph_painter.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/graph_painter.dart similarity index 83% rename from playground/frontend/lib/modules/graph/graph_builder/painters/graph_painter.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/graph_painter.dart index 81d563531630..8c2c4c356c02 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/painters/graph_painter.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/graph_painter.dart @@ -19,18 +19,17 @@ import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/graph/graph_builder/canvas_drawer.dart'; -import 'package:playground/modules/graph/graph_builder/painters/edge_painter.dart'; -import 'package:playground/modules/graph/graph_builder/painters/node_painter.dart'; -import 'package:playground/modules/graph/models/graph.dart'; -enum GraphDirection { vertical, horizontal } +import '../../../../../constants/sizes.dart'; +import '../../models/graph.dart'; +import '../canvas_drawer.dart'; +import 'edge_painter.dart'; +import 'node_painter.dart'; class GraphPainter { final List elementsPainter; final List edges; - final GraphDirection direction; + final Axis direction; final Map elementsMap = {}; final Map rowSizes = {}; final Map columnSizes = {}; @@ -41,8 +40,8 @@ class GraphPainter { final lastColumn = columnStarts.length - 1; final lastRow = rowStarts.length - 1; final width = - columnStarts[lastColumn]! + columnSizes[lastColumn]! + 4 * kXlSpacing; - final height = rowStarts[lastRow]! + rowSizes[lastRow]! + 4 * kXlSpacing; + columnStarts[lastColumn]! + columnSizes[lastColumn]! + 4 * BeamSizes.size16; + final height = rowStarts[lastRow]! + rowSizes[lastRow]! + 4 * BeamSizes.size16; return Size(width, height); } @@ -70,11 +69,11 @@ class GraphPainter { var top = 0.0; for (var r = 0; r < rowSizes.length; r++) { rowStarts[r] = top; - top = top + rowSizes[r]! + 4 * kXlSpacing; + top = top + rowSizes[r]! + 4 * BeamSizes.size16; } for (var c = 0; c < columnSizes.length; c++) { columnStarts[c] = left; - left = left + columnSizes[c]! + 4 * kXlSpacing; + left = left + columnSizes[c]! + 4 * BeamSizes.size16; } } diff --git a/playground/frontend/lib/modules/graph/graph_builder/painters/node_painter.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/node_painter.dart similarity index 82% rename from playground/frontend/lib/modules/graph/graph_builder/painters/node_painter.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/node_painter.dart index 6e04f880bddb..f0b78632d99a 100644 --- a/playground/frontend/lib/modules/graph/graph_builder/painters/node_painter.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/graph/graph_builder/painters/node_painter.dart @@ -15,12 +15,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/graph/graph_builder/canvas_drawer.dart'; -import 'package:playground/modules/graph/models/graph.dart'; + +import '../../../../../constants/sizes.dart'; +import '../../models/graph.dart'; +import '../canvas_drawer.dart'; class NodeElementPainter { int row; @@ -47,18 +49,18 @@ class NodeElementPainter { drawer.drawText( parentLabel, maxTextWidth, - Offset(left! + kXlSpacing, top! + kLgSpacing), + Offset(left! + BeamSizes.size16, top! + BeamSizes.size12), ); drawer.drawSecondaryText( element.label, maxTextWidth, - Offset(left! + kXlSpacing, top! + kLgSpacing + kMdSpacing + 10.0), + Offset(left! + BeamSizes.size16, top! + BeamSizes.size12 + BeamSizes.size8 + 10.0), ); } else { drawer.drawText( element.label, maxTextWidth, - Offset(left! + kXlSpacing, top! + (56 / 2 - 5)), + Offset(left! + BeamSizes.size16, top! + (56 / 2 - 5)), ); } } @@ -75,8 +77,8 @@ class NodeElementPainter { if (size != null) { return size!; } - final fullWidth = maxTextWidth + kXlSpacing * 2; - size = Size(fullWidth, kLgSpacing * 2 + kMdSpacing + 10.0 * 2); + final fullWidth = maxTextWidth + BeamSizes.size16 * 2; + size = Size(fullWidth, BeamSizes.size12 * 2 + BeamSizes.size8 + 10.0 * 2); return size!; } diff --git a/playground/frontend/lib/modules/graph/models/graph.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/models/graph.dart similarity index 100% rename from playground/frontend/lib/modules/graph/models/graph.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/models/graph.dart diff --git a/playground/frontend/lib/modules/graph/models/table_cell.dart b/playground/frontend/playground_components/lib/src/widgets/output/graph/models/table_cell.dart similarity index 100% rename from playground/frontend/lib/modules/graph/models/table_cell.dart rename to playground/frontend/playground_components/lib/src/widgets/output/graph/models/table_cell.dart diff --git a/playground/frontend/lib/modules/output/components/output.dart b/playground/frontend/playground_components/lib/src/widgets/output/output.dart similarity index 64% rename from playground/frontend/lib/modules/output/components/output.dart rename to playground/frontend/playground_components/lib/src/widgets/output/output.dart index b99692749cbc..194b5d754937 100644 --- a/playground/frontend/lib/modules/output/components/output.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/output.dart @@ -17,39 +17,43 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/modules/output/components/output_area.dart'; -import 'package:playground/modules/output/components/output_header/output_placements.dart'; -import 'package:playground/modules/output/components/output_header/output_tabs.dart'; -import 'package:playground/modules/output/components/output_header/tab_header.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; + +import '../../controllers/playground_controller.dart'; +import '../tab_header.dart'; +import 'output_area.dart'; +import 'output_tabs.dart'; const kTabsCount = 2; -class Output extends StatefulWidget { - final bool isEmbedded; - final bool showGraph; +class OutputWidget extends StatefulWidget { + final PlaygroundController playgroundController; + final Widget? trailing; + final Axis graphDirection; - Output({ - required this.isEmbedded, - required PlaygroundState playgroundState, - }) : showGraph = playgroundState.graphAvailable, - super( + OutputWidget({ + required this.playgroundController, + required this.graphDirection, + this.trailing, + }) : super( key: ValueKey( - '${playgroundState.sdk}_${playgroundState.selectedExample?.path}', + '${playgroundController.sdk}_${playgroundController.selectedExample?.path}', ), ); @override - State createState() => _OutputState(); + State createState() => _OutputWidgetState(); } -class _OutputState extends State with SingleTickerProviderStateMixin { +class _OutputWidgetState extends State + with SingleTickerProviderStateMixin { late final TabController tabController; int selectedTab = 0; @override void initState() { - final tabsCount = widget.showGraph ? kTabsCount : kTabsCount - 1; + final tabsCount = widget.playgroundController.graphAvailable + ? kTabsCount + : kTabsCount - 1; tabController = TabController(vsync: this, length: tabsCount); tabController.addListener(_onTabChange); super.initState(); @@ -78,17 +82,18 @@ class _OutputState extends State with SingleTickerProviderStateMixin { TabHeader( tabController: tabController, tabsWidget: OutputTabs( + playgroundController: widget.playgroundController, tabController: tabController, - showGraph: widget.showGraph, ), ), - const OutputPlacements(), + if (widget.trailing != null) widget.trailing!, ], ), Expanded( child: OutputArea( + playgroundController: widget.playgroundController, tabController: tabController, - showGraph: widget.showGraph, + graphDirection: widget.graphDirection, ), ), ], diff --git a/playground/frontend/playground_components/lib/src/widgets/output/output_area.dart b/playground/frontend/playground_components/lib/src/widgets/output/output_area.dart new file mode 100644 index 000000000000..f1bf48b2c260 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/output/output_area.dart @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; +import 'package:playground_components/playground_components.dart'; + +import 'graph/graph.dart'; +import 'output_result.dart'; + +class OutputArea extends StatelessWidget { + final PlaygroundController playgroundController; + final TabController tabController; + final Axis graphDirection; + + const OutputArea({ + Key? key, + required this.playgroundController, + required this.tabController, + required this.graphDirection, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final sdk = playgroundController.sdk; + + return Container( + color: Theme.of(context).backgroundColor, + child: TabBarView( + controller: tabController, + physics: const NeverScrollableScrollPhysics(), + children: [ + OutputResult( + text: playgroundController.outputResult, + isSelected: tabController.index == 0, + ), + if (playgroundController.graphAvailable) + sdk == null + ? Container() + : GraphTab( + graph: playgroundController.result?.graph ?? '', + sdk: sdk, + direction: graphDirection, + ), + ], + ), + ); + } +} diff --git a/playground/frontend/lib/modules/output/components/output_result.dart b/playground/frontend/playground_components/lib/src/widgets/output/output_result.dart similarity index 86% rename from playground/frontend/lib/modules/output/components/output_result.dart rename to playground/frontend/playground_components/lib/src/widgets/output/output_result.dart index 63c90b2770a8..236a4856ff02 100644 --- a/playground/frontend/lib/modules/output/components/output_result.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/output_result.dart @@ -17,8 +17,9 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/constants/fonts.dart'; -import 'package:playground/constants/sizes.dart'; + +import '../../constants/sizes.dart'; +import '../../theme/theme.dart'; class OutputResult extends StatefulWidget { final String text; @@ -48,6 +49,7 @@ class _OutputResultState extends State { @override Widget build(BuildContext context) { + final ext = Theme.of(context).extension()!; return SingleChildScrollView( controller: _scrollController, child: Scrollbar( @@ -55,8 +57,11 @@ class _OutputResultState extends State { trackVisibility: true, controller: _scrollController, child: Padding( - padding: const EdgeInsets.all(kXlSpacing), - child: SelectableText(widget.text, style: getCodeFontStyle()), + padding: const EdgeInsets.all(BeamSizes.size16), + child: SelectableText( + widget.text, + style: ext.codeRootStyle, + ), ), ), ); diff --git a/playground/frontend/lib/modules/output/components/output_header/output_tab.dart b/playground/frontend/playground_components/lib/src/widgets/output/output_tab.dart similarity index 78% rename from playground/frontend/lib/modules/output/components/output_header/output_tab.dart rename to playground/frontend/playground_components/lib/src/widgets/output/output_tab.dart index 4b6daed2a08f..326b1c72448f 100644 --- a/playground/frontend/lib/modules/output/components/output_header/output_tab.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/output_tab.dart @@ -18,23 +18,26 @@ import 'package:aligned_dialog/aligned_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/output/components/output_header/result_filter_popover.dart'; + +import '../../constants/sizes.dart'; +import '../../controllers/playground_controller.dart'; +import 'result_filter_popover.dart'; class OutputTab extends StatefulWidget { + final PlaygroundController playgroundController; final String name; final bool isSelected; final String value; final bool hasFilter; const OutputTab({ - Key? key, + super.key, + required this.playgroundController, required this.name, required this.isSelected, required this.value, this.hasFilter = false, - }) : super(key: key); + }); @override State createState() => _OutputTabState(); @@ -61,11 +64,13 @@ class _OutputTabState extends State { @override Widget build(BuildContext context) { + final themeData = Theme.of(context); + return Tab( child: Wrap( direction: Axis.horizontal, alignment: WrapAlignment.center, - spacing: kMdSpacing, + spacing: BeamSizes.size8, children: [ Text(widget.name), widget.hasFilter @@ -73,7 +78,9 @@ class _OutputTabState extends State { onTap: () { showAlignedDialog( context: context, - builder: (dialogContext) => const ResultFilterPopover(), + builder: (dialogContext) => ResultFilterPopover( + playgroundController: widget.playgroundController, + ), followerAnchor: Alignment.topLeft, targetAnchor: Alignment.topLeft, barrierColor: Colors.transparent, @@ -81,17 +88,17 @@ class _OutputTabState extends State { }, child: Icon( Icons.filter_alt_outlined, - size: kIconSizeSm, - color: ThemeColors.of(context).primary, + size: BeamIconSizes.small, + color: themeData.primaryColor, ), ) : const SizedBox(), if (hasNewContent) Container( - width: kIconSizeXs, - height: kIconSizeXs, + width: BeamIconSizes.xs, + height: BeamIconSizes.xs, decoration: BoxDecoration( - color: ThemeColors.of(context).primary, + color: themeData.primaryColor, shape: BoxShape.circle, ), ), diff --git a/playground/frontend/lib/modules/output/components/output_header/output_tabs.dart b/playground/frontend/playground_components/lib/src/widgets/output/output_tabs.dart similarity index 51% rename from playground/frontend/lib/modules/output/components/output_header/output_tabs.dart rename to playground/frontend/playground_components/lib/src/widgets/output/output_tabs.dart index 6107769a5298..80839296fbfa 100644 --- a/playground/frontend/lib/modules/output/components/output_header/output_tabs.dart +++ b/playground/frontend/playground_components/lib/src/widgets/output/output_tabs.dart @@ -16,46 +16,45 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/modules/output/components/output_header/output_tab.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; -import 'package:provider/provider.dart'; + +import '../../controllers/playground_controller.dart'; +import 'output_tab.dart'; class OutputTabs extends StatelessWidget { + final PlaygroundController playgroundController; final TabController tabController; - final bool showGraph; const OutputTabs({ - Key? key, + super.key, + required this.playgroundController, required this.tabController, - required this.showGraph, - }) : super(key: key); + }); @override Widget build(BuildContext context) { - AppLocalizations appLocale = AppLocalizations.of(context)!; - return Consumer(builder: (context, state, child) { - return SizedBox( - width: 300, - child: TabBar( - controller: tabController, - tabs: [ + return SizedBox( + width: 300, + child: TabBar( + controller: tabController, + tabs: [ + OutputTab( + playgroundController: playgroundController, + name: 'widgets.output.result'.tr(), + isSelected: tabController.index == 0, + value: playgroundController.outputResult, + hasFilter: true, + ), + if (playgroundController.graphAvailable) OutputTab( - name: appLocale.result, - isSelected: tabController.index == 0, - value: state.outputResult, - hasFilter: true, + playgroundController: playgroundController, + name: 'widgets.output.graph'.tr(), + isSelected: tabController.index == 2, + value: playgroundController.result?.graph ?? '', ), - if (showGraph) - OutputTab( - name: appLocale.graph, - isSelected: tabController.index == 2, - value: state.result?.graph ?? '', - ), - ], - ), - ); - }); + ], + ), + ); } } diff --git a/playground/frontend/playground_components/lib/src/widgets/output/result_filter_bubble.dart b/playground/frontend/playground_components/lib/src/widgets/output/result_filter_bubble.dart new file mode 100644 index 000000000000..a18ddb01d81a --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/output/result_filter_bubble.dart @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/material.dart'; + +import '../../controllers/playground_controller.dart'; +import '../../models/outputs.dart'; +import '../bubble.dart'; + +class ResultFilterBubble extends StatelessWidget { + final PlaygroundController playgroundController; + final OutputType type; + final String name; + + const ResultFilterBubble({ + super.key, + required this.playgroundController, + required this.type, + required this.name, + }); + + @override + Widget build(BuildContext context) { + final isSelected = type == playgroundController.selectedOutputFilterType; + + return BubbleWidget( + isSelected: isSelected, + onTap: () { + if (!isSelected) { + playgroundController.setSelectedOutputFilterType(type); + playgroundController.filterOutput(type); + } + }, + title: name, + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/widgets/output/result_filter_popover.dart b/playground/frontend/playground_components/lib/src/widgets/output/result_filter_popover.dart new file mode 100644 index 000000000000..a74c67e2fa17 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/output/result_filter_popover.dart @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import 'package:playground_components/playground_components.dart'; + +import 'result_filter_bubble.dart'; + +const kPopoverWidth = 240.0; +const kPopoverPadding = 50.0; + +class ResultFilterPopover extends StatelessWidget { + final PlaygroundController playgroundController; + + const ResultFilterPopover({ + required this.playgroundController, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: kPopoverPadding), + child: SizedBox( + width: kPopoverWidth, + child: Card( + child: Padding( + padding: const EdgeInsets.all(BeamSizes.size8), + child: Wrap( + runSpacing: BeamSizes.size8, + children: [ + const Text('widgets.output.filterTitle').tr(), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: BeamSizes.size4, + vertical: BeamSizes.size4, + ), + child: AnimatedBuilder( + animation: playgroundController, + builder: (context, child) => Row( + children: [ + ResultFilterBubble( + playgroundController: playgroundController, + type: OutputType.all, + name: 'widgets.output.filter.all'.tr(), + ), + ResultFilterBubble( + playgroundController: playgroundController, + type: OutputType.log, + name: 'widgets.output.filter.log'.tr(), + ), + ResultFilterBubble( + playgroundController: playgroundController, + type: OutputType.output, + name: 'widgets.output.filter.output'.tr(), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/widgets/reset_button.dart b/playground/frontend/playground_components/lib/src/widgets/reset_button.dart new file mode 100644 index 000000000000..079587dddedb --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/reset_button.dart @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../constants/playground_components.dart'; +import '../controllers/playground_controller.dart'; +import '../generated/assets.gen.dart'; +import '../theme/theme.dart'; +import 'header_icon_button.dart'; +import 'shortcut_tooltip.dart'; + +class ResetButton extends StatelessWidget { + final PlaygroundController playgroundController; + final VoidCallback? beforeReset; + + const ResetButton({ + required this.playgroundController, + this.beforeReset, + }); + + @override + Widget build(BuildContext context) { + return ShortcutTooltip( + shortcut: playgroundController.resetShortcut, + child: HeaderIconButton( + icon: SvgPicture.asset( + Assets.buttons.reset, + color: Theme.of(context).extension()?.iconColor, + package: PlaygroundComponents.packageName, + ), + label: 'widgets.resetButton.label'.tr(), + onPressed: () { + beforeReset?.call(); + playgroundController.reset(); + }, + ), + ); + } +} diff --git a/playground/frontend/lib/modules/editor/components/run_button.dart b/playground/frontend/playground_components/lib/src/widgets/run_button.dart similarity index 69% rename from playground/frontend/lib/modules/editor/components/run_button.dart rename to playground/frontend/playground_components/lib/src/widgets/run_button.dart index d31e36d21351..9aabaeb6b1f4 100644 --- a/playground/frontend/lib/modules/editor/components/run_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/run_button.dart @@ -16,55 +16,58 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/sizes.dart'; -import 'package:playground/modules/shortcuts/components/shortcut_tooltip.dart'; -import 'package:playground/modules/shortcuts/constants/global_shortcuts.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; -import 'package:provider/provider.dart'; + +import '../constants/sizes.dart'; +import '../controllers/playground_controller.dart'; +import '../theme/theme.dart'; +import 'shortcut_tooltip.dart'; const kMsToSec = 1000; const kSecondsFractions = 1; +const _width = 150.0; + class RunButton extends StatelessWidget { + final PlaygroundController playgroundController; final bool isRunning; final VoidCallback runCode; final VoidCallback cancelRun; final bool disabled; const RunButton({ - Key? key, + super.key, + required this.playgroundController, required this.isRunning, required this.runCode, required this.cancelRun, this.disabled = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return SizedBox( - width: kRunButtonWidth, - height: kButtonHeight, + width: _width, + height: BeamSizes.buttonHeight, child: ShortcutTooltip( - shortcut: kRunShortcut, + shortcut: playgroundController.runShortcut, child: ElevatedButton.icon( icon: isRunning ? SizedBox( - width: kIconSizeSm, - height: kIconSizeSm, + width: BeamIconSizes.small, + height: BeamIconSizes.small, child: CircularProgressIndicator( - color: ThemeColors.of(context).primaryBackgroundTextColor, + color: Theme.of(context).extension()!.primaryBackgroundTextColor, ), ) : const Icon(Icons.play_arrow), label: StreamBuilder( - stream: Provider.of(context).executionTime, + stream: playgroundController.executionTime, builder: (context, AsyncSnapshot state) { final seconds = (state.data ?? 0) / kMsToSec; - final runText = AppLocalizations.of(context)!.run; - final cancelText = AppLocalizations.of(context)!.cancel; + final runText = 'widgets.runOrCancelButton.titles.run'.tr(); + final cancelText = 'widgets.runOrCancelButton.titles.cancel'.tr(); final buttonText = isRunning ? cancelText : runText; if (seconds > 0) { return Text( diff --git a/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart b/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart new file mode 100644 index 000000000000..7bbd204fcb2c --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/run_or_cancel_button.dart @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/widgets.dart'; + +import '../controllers/playground_controller.dart'; +import '../notifications/notification.dart'; +import 'run_button.dart'; + +class RunOrCancelButton extends StatelessWidget { + final VoidCallback? beforeCancel; + final VoidCallback? onComplete; + final VoidCallback? beforeRun; + final PlaygroundController playgroundController; + + const RunOrCancelButton({ + required this.playgroundController, + this.beforeCancel, + this.onComplete, + this.beforeRun, + }); + + @override + Widget build(BuildContext context) { + return RunButton( + playgroundController: playgroundController, + disabled: playgroundController.selectedExample?.isMultiFile ?? false, + isRunning: playgroundController.isCodeRunning, + cancelRun: () { + beforeCancel?.call(); + playgroundController.cancelRun().catchError( + (_) => NotificationManager.showError( + context, + 'widgets.runOrCancelButton.notificationTitles.runCode'.tr(), + 'widgets.runOrCancelButton.notificationTitles.cancelExecution'.tr(), + ), + ); + }, + runCode: () { + beforeRun?.call(); + playgroundController.runCode( + onFinish: onComplete, + ); + }, + ); + } +} diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcut_tooltip.dart b/playground/frontend/playground_components/lib/src/widgets/shortcut_tooltip.dart similarity index 81% rename from playground/frontend/lib/modules/shortcuts/components/shortcut_tooltip.dart rename to playground/frontend/playground_components/lib/src/widgets/shortcut_tooltip.dart index 94f14677c14b..80e75b9e0788 100644 --- a/playground/frontend/lib/modules/shortcuts/components/shortcut_tooltip.dart +++ b/playground/frontend/playground_components/lib/src/widgets/shortcut_tooltip.dart @@ -17,24 +17,24 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/modules/shortcuts/models/shortcut.dart'; -import 'package:playground/modules/shortcuts/utils/shortcuts_display_name.dart'; + +import '../models/shortcut.dart'; class ShortcutTooltip extends StatelessWidget { - final Shortcut shortcut; + final BeamShortcut shortcut; final Widget child; const ShortcutTooltip({ - Key? key, + super.key, required this.shortcut, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { return Tooltip( excludeFromSemantics: true, - message: getShortcutDisplayName(shortcut), + message: shortcut.title, child: child, ); } diff --git a/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart b/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart new file mode 100644 index 000000000000..fe7ecc4e6037 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter/widgets.dart'; + +import '../controllers/snippet_editing_controller.dart'; +import 'editor_textarea.dart'; + +class SnippetEditor extends StatelessWidget { + final SnippetEditingController controller; + final bool isEditable; + final bool goToContextLine; + + const SnippetEditor({ + required this.controller, + required this.isEditable, + required this.goToContextLine, + }); + + @override + Widget build(BuildContext context) { + return EditorTextArea( + codeController: controller.codeController, + sdk: controller.sdk, + enabled: !(controller.selectedExample?.isMultiFile ?? false), + example: controller.selectedExample, + isEditable: isEditable, + goToContextLine: goToContextLine, + ); + } +} diff --git a/playground/frontend/lib/components/split_view/split_view.dart b/playground/frontend/playground_components/lib/src/widgets/split_view.dart similarity index 79% rename from playground/frontend/lib/components/split_view/split_view.dart rename to playground/frontend/playground_components/lib/src/widgets/split_view.dart index 06403d6759a7..937685618dd3 100644 --- a/playground/frontend/lib/components/split_view/split_view.dart +++ b/playground/frontend/playground_components/lib/src/widgets/split_view.dart @@ -17,14 +17,9 @@ */ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/assets.dart'; -enum SplitViewDirection { - vertical, - horizontal, -} +import '../constants/sizes.dart'; +import 'drag_handle.dart'; const minRatio = 0.3; const maxRatio = 0.7; @@ -33,18 +28,16 @@ const defaultRatio = 0.5; class SplitView extends StatefulWidget { final Widget first; final Widget second; - final double dividerSize; - final SplitViewDirection direction; + final Axis direction; final double ratio; const SplitView({ - Key? key, + super.key, required this.first, required this.second, - required this.dividerSize, required this.direction, this.ratio = defaultRatio, - }) : super(key: key); + }); @override State createState() => _SplitViewState(); @@ -59,9 +52,9 @@ class _SplitViewState extends State { get _sizeSecond => (1 - _ratio) * _maxSize; - get _isHorizontal => widget.direction == SplitViewDirection.horizontal; + get _isHorizontal => widget.direction == Axis.horizontal; - get _isVertical => widget.direction == SplitViewDirection.vertical; + get _isVertical => widget.direction == Axis.vertical; @override void initState() { @@ -79,7 +72,8 @@ class _SplitViewState extends State { }); } - _buildHorizontalLayout(BuildContext context, BoxConstraints constraints) { + Widget _buildHorizontalLayout( + BuildContext context, BoxConstraints constraints) { return SizedBox( width: constraints.maxWidth, child: Row( @@ -126,14 +120,13 @@ class _SplitViewState extends State { child: GestureDetector( behavior: HitTestBehavior.translucent, child: Container( - width: _isHorizontal ? widget.dividerSize : double.infinity, - height: _isVertical ? widget.dividerSize : double.infinity, - color: ThemeColors.of(context).divider, - child: Center( - child: SvgPicture.asset(_isHorizontal - ? kDragHorizontalIconAsset - : kDragVerticalIconAsset), - )), + width: _isHorizontal ? BeamSizes.splitViewSeparator : double.infinity, + height: _isVertical ? BeamSizes.splitViewSeparator : double.infinity, + color: Theme.of(context).dividerColor, + child: Center( + child: DragHandle(direction: widget.direction), + ), + ), onPanUpdate: (DragUpdateDetails details) { setState(() { _updateRatio(details); @@ -171,7 +164,7 @@ class _SplitViewState extends State { void _calculateMaxSize(double maxSize) { if (_maxSize != maxSize) { - _maxSize = maxSize - widget.dividerSize; + _maxSize = maxSize - BeamSizes.splitViewSeparator; } } } diff --git a/playground/frontend/lib/modules/output/components/output_header/tab_header.dart b/playground/frontend/playground_components/lib/src/widgets/tab_header.dart similarity index 91% rename from playground/frontend/lib/modules/output/components/output_header/tab_header.dart rename to playground/frontend/playground_components/lib/src/widgets/tab_header.dart index dbf60b30f8a4..714f025814e0 100644 --- a/playground/frontend/lib/modules/output/components/output_header/tab_header.dart +++ b/playground/frontend/playground_components/lib/src/widgets/tab_header.dart @@ -17,7 +17,8 @@ */ import 'package:flutter/material.dart'; -import 'package:playground/constants/sizes.dart'; + +import '../constants/sizes.dart'; const kHeaderHeight = 50.0; @@ -37,8 +38,7 @@ class TabHeader extends StatelessWidget { height: 50, child: Padding( padding: const EdgeInsets.symmetric( - horizontal: kXlSpacing, - vertical: kZeroSpacing, + horizontal: BeamSizes.size16, ), child: tabsWidget, ), diff --git a/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart index abd0b82edd56..927bda590ebe 100644 --- a/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_button.dart @@ -21,6 +21,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; +import '../constants/playground_components.dart'; import '../generated/assets.gen.dart'; import '../theme/switch_notifier.dart'; @@ -35,7 +36,10 @@ class ToggleThemeButton extends StatelessWidget { notifier.isDarkMode ? 'ui.lightMode'.tr() : 'ui.darkMode'.tr(); return TextButton.icon( - icon: SvgPicture.asset(Assets.svg.themeMode), + icon: SvgPicture.asset( + Assets.buttons.themeMode, + package: PlaygroundComponents.packageName, + ), label: Text(text), onPressed: () { notifier.toggleTheme(); diff --git a/playground/frontend/lib/components/toggle_theme_button/toggle_theme_icon_button.dart b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_icon_button.dart similarity index 73% rename from playground/frontend/lib/components/toggle_theme_button/toggle_theme_icon_button.dart rename to playground/frontend/playground_components/lib/src/widgets/toggle_theme_icon_button.dart index f82270826fea..982aa39f80e6 100644 --- a/playground/frontend/lib/components/toggle_theme_button/toggle_theme_icon_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/toggle_theme_icon_button.dart @@ -18,21 +18,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:playground/config/theme.dart'; -import 'package:playground/constants/assets.dart'; -import 'package:playground/constants/sizes.dart'; import 'package:provider/provider.dart'; +import '../constants/playground_components.dart'; +import '../constants/sizes.dart'; +import '../generated/assets.gen.dart'; +import '../theme/switch_notifier.dart'; + class ToggleThemeIconButton extends StatelessWidget { - const ToggleThemeIconButton({Key? key}) : super(key: key); + const ToggleThemeIconButton({super.key}); @override Widget build(BuildContext context) { return Consumer(builder: (context, notifier, child) { return IconButton( - iconSize: kIconSizeLg, - splashRadius: kIconButtonSplashRadius, - icon: SvgPicture.asset(kThemeIconAsset), + iconSize: BeamIconSizes.large, + splashRadius: BeamIconSizes.largeSplashRadius, + icon: SvgPicture.asset( + Assets.buttons.themeMode, + package: PlaygroundComponents.packageName, + ), onPressed: notifier.toggleTheme, ); }); diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index d489229cb822..1321bee934cc 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -24,25 +24,40 @@ environment: flutter: '>=1.17.0' dependencies: + aligned_dialog: ^0.0.6 + code_text_field: + git: + url: https://github.com/BertrandBev/code_field.git + ref: 9e2c9fe52a69481f038f4b6609e8a0a776429437 + collection: ^1.16.0 easy_localization: ^3.0.1 + easy_localization_ext: ^0.1.1 easy_localization_loader: ^1.0.0 + equatable: ^2.0.5 flutter: { sdk: flutter } flutter_svg: ^1.0.3 google_fonts: ^3.0.1 + grpc: ^3.0.2 + highlight: ^0.7.0 + meta: ^1.7.0 provider: ^6.0.3 + protobuf: ^2.1.0 shared_preferences: ^2.0.15 dev_dependencies: build_runner: ^2.2.0 flutter_gen_runner: ^4.3.0 flutter_test: { sdk: flutter } + mockito: 5.2.0 total_lints: ^2.17.4 flutter: uses-material-design: true assets: - - assets/svg/ + - assets/buttons/ + - assets/notification_icons/ - assets/png/ + - assets/svg/ - assets/translations/en.yaml flutter_gen: diff --git a/playground/frontend/test/pages/playground/states/examples_state_test.dart b/playground/frontend/playground_components/test/src/cache/example_cache_test.dart similarity index 52% rename from playground/frontend/test/pages/playground/states/examples_state_test.dart rename to playground/frontend/playground_components/test/src/cache/example_cache_test.dart index 169dce298a2e..4b461ba69e9c 100644 --- a/playground/frontend/test/pages/playground/states/examples_state_test.dart +++ b/playground/frontend/playground_components/test/src/cache/example_cache_test.dart @@ -16,35 +16,37 @@ * limitations under the License. */ +import 'dart:collection'; + import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; - -import 'mocks/categories_mock.dart'; -import 'mocks/example_mock.dart'; -import 'mocks/example_repository_mock.dart'; -import 'mocks/example_repository_mock.mocks.dart'; -import 'mocks/request_mock.dart'; - -final kDefaultExamplesMapMock = { - SDK.java: exampleWithAllAdditionsMock, - SDK.go: exampleWithAllAdditionsMock, - SDK.python: exampleWithAllAdditionsMock, - SDK.scio: exampleWithAllAdditionsMock, -}; +import 'package:playground_components/src/cache/example_cache.dart'; +import 'package:playground_components/src/models/sdk.dart'; + +import '../common/categories.dart'; +import '../common/example_repository_mock.dart'; +import '../common/example_repository_mock.mocks.dart'; +import '../common/examples.dart'; +import '../common/requests.dart'; + +final kDefaultExamplesMapMock = UnmodifiableMapView({ + Sdk.java: exampleWithAllAdditionsMock, + Sdk.go: exampleWithAllAdditionsMock, + Sdk.python: exampleWithAllAdditionsMock, + Sdk.scio: exampleWithAllAdditionsMock, +}); void main() { - late ExampleState state; + late ExampleCache state; late MockExampleRepository mockRepo; setUp(() { mockRepo = getMockExampleRepository(); - state = ExampleState(mockRepo); + state = ExampleCache(exampleRepository: mockRepo, hasCatalog: true); }); - test('Initial value of defaultExamplesMap should be an empty map', () { - expect(state.defaultExamplesMap, {}); + test('Initial value of defaultExamplesBySdk should be an empty map', () { + expect(state.defaultExamplesBySdk, {}); }); test('Initial value of isSelectorOpened should be false', () { @@ -52,20 +54,20 @@ void main() { }); test( - 'Example state init should initiate loading of sdkCategories from server', + 'Example state init should initiate loading of categoryListsBySdk from server', () async { - when(mockRepo.getListOfExamples(kGetListOfExamplesRequestMock)) - .thenAnswer((_) async => kGetListOfExamplesResponseMock.categories); + when(mockRepo.getListOfExamples(kGetPrecompiledObjectsRequest)) + .thenAnswer((_) async => kGetPrecompiledObjectsResponse.categories); await state.init(); - expect(state.sdkCategories, sdkCategoriesFromServerMock); + expect(state.categoryListsBySdk, sdkCategoriesFromServerMock); }, ); test( - 'Example state should notify all listeners about sdkCategories is set', + 'Example state should notify all listeners about categoryListsBySdk is set', () { state.addListener(() { - expect(state.sdkCategories, sdkCategoriesFromServerMock); + expect(state.categoryListsBySdk, sdkCategoriesFromServerMock); }); state.setSdkCategories(sdkCategoriesFromServerMock); }, @@ -85,21 +87,24 @@ void main() { 'Example state getCategories should get the categories list for each SDK', () { state.setSdkCategories(sdkCategoriesFromServerMock); - expect(state.getCategories(SDK.java), categoriesMock); - expect(state.getCategories(SDK.go), categoriesMock); - expect(state.getCategories(SDK.python), categoriesMock); - expect(state.getCategories(SDK.scio), categoriesMock); + expect(state.getCategories(Sdk.java), categoriesMock); + expect(state.getCategories(Sdk.go), categoriesMock); + expect(state.getCategories(Sdk.python), categoriesMock); + expect(state.getCategories(Sdk.scio), categoriesMock); }, ); test( 'Example state getExampleOutput should return output for example', () async { - when(mockRepo.getExampleOutput(kGetExampleRequestMock)) - .thenAnswer((_) async => kOutputResponseMock.output); + when(mockRepo.getExampleOutput(kRequestForExampleInfo)) + .thenAnswer((_) async => kOutputResponse.output); expect( - await state.getExampleOutput('', SDK.java), - kOutputResponseMock.output, + await state.getExampleOutput( + kRequestForExampleInfo.path, + kRequestForExampleInfo.sdk, + ), + kOutputResponse.output, ); }, ); @@ -107,11 +112,14 @@ void main() { test( 'Example state getExampleSource should return source code for example', () async { - when(mockRepo.getExampleSource(kGetExampleRequestMock)) - .thenAnswer((_) async => kOutputResponseMock.output); + when(mockRepo.getExampleSource(kRequestForExampleInfo)) + .thenAnswer((_) async => kOutputResponse.output); expect( - await state.getExampleSource('', SDK.java), - kOutputResponseMock.output, + await state.getExampleSource( + kRequestForExampleInfo.path, + kRequestForExampleInfo.sdk, + ), + kOutputResponse.output, ); }, ); @@ -119,11 +127,14 @@ void main() { test( 'Example state getExampleLogs should return logs for example', () async { - when(mockRepo.getExampleLogs(kGetExampleRequestMock)) - .thenAnswer((_) async => kOutputResponseMock.output); + when(mockRepo.getExampleLogs(kRequestForExampleInfo)) + .thenAnswer((_) async => kOutputResponse.output); expect( - await state.getExampleLogs('', SDK.java), - kOutputResponseMock.output, + await state.getExampleLogs( + kRequestForExampleInfo.path, + kRequestForExampleInfo.sdk, + ), + kOutputResponse.output, ); }, ); @@ -131,18 +142,21 @@ void main() { test( 'Example state getExampleGraph should return output for example', () async { - when(mockRepo.getExampleGraph(kGetExampleRequestMock)) - .thenAnswer((_) async => kOutputResponseMock.output); + when(mockRepo.getExampleGraph(kRequestForExampleInfo)) + .thenAnswer((_) async => kOutputResponse.output); expect( - await state.getExampleGraph('', SDK.java), - kOutputResponseMock.output, + await state.getExampleGraph( + kRequestForExampleInfo.path, + kRequestForExampleInfo.sdk, + ), + kOutputResponse.output, ); }, ); group('loadExampleInfo tests', () { test( - 'If example info is fetched (source is not empty),' + 'If example info is fetched (source is not empty), ' 'then loadExampleInfo should return example immediately', () async { expect( @@ -165,31 +179,31 @@ void main() { group('loadDefaultExamples tests', () { test( - 'If defaultExamplesMap is not empty, then loadDefaultExamples should not change it', + 'If defaultExamplesBySdk is not empty, then loadDefaultExamples should not change it', () async { - state.defaultExamplesMap = kDefaultExamplesMapMock; + state.defaultExamplesBySdk.addAll(kDefaultExamplesMapMock); await state.loadDefaultExamples(); - expect(state.defaultExamplesMap, kDefaultExamplesMapMock); + expect(state.defaultExamplesBySdk, kDefaultExamplesMapMock); }, ); test( - 'Example state loadDefaultExamples should load default example' + 'Example state loadDefaultExamples should load default example ' 'with all additions for every Sdk', () async { // stubs when(mockRepo.getExampleOutput(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); when(mockRepo.getExampleSource(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); when(mockRepo.getExampleLogs(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); when(mockRepo.getExampleGraph(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); // test assertion await state.loadDefaultExamples(); expect( - state.defaultExamplesMap, + state.defaultExamplesBySdk, kDefaultExamplesMapMock, ); }, diff --git a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart b/playground/frontend/playground_components/test/src/common/categories.dart similarity index 51% rename from playground/frontend/lib/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart rename to playground/frontend/playground_components/test/src/common/categories.dart index aec65ba2f246..39c0f4c38bcb 100644 --- a/playground/frontend/lib/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart +++ b/playground/frontend/playground_components/test/src/common/categories.dart @@ -16,29 +16,31 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_origin.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'dart:collection'; -class EmptyExampleLoadingDescriptor extends ExampleLoadingDescriptor { - final SDK sdk; +import 'package:playground_components/src/models/category_with_examples.dart'; +import 'package:playground_components/src/models/sdk.dart'; - const EmptyExampleLoadingDescriptor({ - required this.sdk, - }); +import 'examples.dart'; - @override - ExampleOrigin get origin => ExampleOrigin.empty; +final categoriesMock = [ + CategoryWithExamples(title: 'Sorted', examples: [exampleMock1]), + CategoryWithExamples(title: 'Unsorted', examples: [exampleMock2]), +]; - @override - int get hashCode => sdk.hashCode; +final sortedCategories = [ + CategoryWithExamples(title: 'Sorted', examples: [exampleMock1]), +]; - @override - bool operator ==(Object other) { - return other is EmptyExampleLoadingDescriptor && sdk == other.sdk; - } +const unsortedExamples = [exampleMock1, exampleMock2]; - // Only ContentExampleLoadingDescriptor is serialized now. - @override - Map toJson() => throw UnimplementedError(); -} +const examplesSortedByTypeMock = [exampleMock2]; + +const examplesSortedByNameMock = [exampleMock1]; + +final sdkCategoriesFromServerMock = UnmodifiableMapView({ + Sdk.java: categoriesMock, + Sdk.python: categoriesMock, + Sdk.go: categoriesMock, + Sdk.scio: categoriesMock, +}); diff --git a/playground/frontend/test/pages/playground/states/mocks/example_repository_mock.dart b/playground/frontend/playground_components/test/src/common/example_repository_mock.dart similarity index 82% rename from playground/frontend/test/pages/playground/states/mocks/example_repository_mock.dart rename to playground/frontend/playground_components/test/src/common/example_repository_mock.dart index e44c5f03b4e6..108d9b1195a1 100644 --- a/playground/frontend/test/pages/playground/states/mocks/example_repository_mock.dart +++ b/playground/frontend/playground_components/test/src/common/example_repository_mock.dart @@ -18,11 +18,11 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:playground/modules/examples/repositories/example_repository.dart'; +import 'package:playground_components/src/repositories/example_repository.dart'; import 'example_repository_mock.mocks.dart'; -import 'example_mock.dart'; -import 'request_mock.dart'; +import 'examples.dart'; +import 'requests.dart'; @GenerateMocks([ExampleRepository]) MockExampleRepository getMockExampleRepository() { @@ -39,13 +39,13 @@ MockExampleRepository getMockExampleRepository() { .thenAnswer((_) async => exampleWithoutSourceMock); when(m.getExampleOutput(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); when(m.getExampleSource(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); when(m.getExampleLogs(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); when(m.getExampleGraph(kRequestForExampleInfo)) - .thenAnswer((_) async => kOutputResponseMock.output); + .thenAnswer((_) async => kOutputResponse.output); return m; } diff --git a/playground/frontend/test/pages/playground/states/mocks/example_mock.dart b/playground/frontend/playground_components/test/src/common/examples.dart similarity index 72% rename from playground/frontend/test/pages/playground/states/mocks/example_mock.dart rename to playground/frontend/playground_components/test/src/common/examples.dart index f0833e8afb48..a522668827de 100644 --- a/playground/frontend/test/pages/playground/states/mocks/example_mock.dart +++ b/playground/frontend/playground_components/test/src/common/examples.dart @@ -16,37 +16,41 @@ * limitations under the License. */ -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/src/models/example.dart'; +import 'package:playground_components/src/models/example_base.dart'; +import 'package:playground_components/src/models/sdk.dart'; -final ExampleModel exampleMock1 = ExampleModel( - sdk: SDK.python, +const exampleMock1 = Example( + sdk: Sdk.python, source: 'ex1', name: 'Example', type: ExampleType.example, description: 'description', path: 'SDK_PYTHON/Category/Name', + pipelineOptions: '', ); -final ExampleModel exampleMock2 = ExampleModel( - sdk: SDK.python, +const exampleMock2 = Example( + sdk: Sdk.python, source: 'ex2', name: 'Kata', type: ExampleType.kata, description: 'description', path: 'SDK_PYTHON/Category/Name', + pipelineOptions: '', ); -final ExampleModel exampleWithoutSourceMock = ExampleModel( - sdk: SDK.python, +const exampleWithoutSourceMock = ExampleBase( + sdk: Sdk.python, name: 'Test example', type: ExampleType.example, description: 'description', path: 'SDK_PYTHON/Category/Name', + pipelineOptions: '', ); -final ExampleModel exampleWithAllAdditionsMock = ExampleModel( - sdk: SDK.python, +const exampleWithAllAdditionsMock = Example( + sdk: Sdk.python, name: 'Test example', type: ExampleType.example, description: 'description', @@ -55,13 +59,15 @@ final ExampleModel exampleWithAllAdditionsMock = ExampleModel( outputs: 'test outputs', logs: 'test outputs', graph: 'test outputs', + pipelineOptions: '', ); -final ExampleModel exampleMockGo = ExampleModel( - sdk: SDK.go, +const exampleMockGo = Example( + sdk: Sdk.go, source: 'ex1', name: 'Example', type: ExampleType.example, description: 'description', path: 'SDK_GO/Category/Name', + pipelineOptions: '', ); diff --git a/playground/frontend/playground_components/test/src/common/requests.dart b/playground/frontend/playground_components/test/src/common/requests.dart new file mode 100644 index 000000000000..84d015884df1 --- /dev/null +++ b/playground/frontend/playground_components/test/src/common/requests.dart @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:playground_components/src/models/sdk.dart'; +import 'package:playground_components/src/repositories/models/get_default_precompiled_object_request.dart'; +import 'package:playground_components/src/repositories/models/get_precompiled_object_code_response.dart'; +import 'package:playground_components/src/repositories/models/get_precompiled_object_request.dart'; +import 'package:playground_components/src/repositories/models/get_precompiled_object_response.dart'; +import 'package:playground_components/src/repositories/models/get_precompiled_objects_request.dart'; +import 'package:playground_components/src/repositories/models/get_precompiled_objects_response.dart'; +import 'package:playground_components/src/repositories/models/output_response.dart'; + +import 'categories.dart'; +import 'examples.dart'; + +const kGetPrecompiledObjectsRequest = GetPrecompiledObjectsRequest( + sdk: null, + category: null, +); +final kGetPrecompiledObjectsResponse = GetPrecompiledObjectsResponse( + categories: sdkCategoriesFromServerMock, +); + +const kGetDefaultPrecompiledObjectRequest = GetDefaultPrecompiledObjectRequest( + sdk: Sdk.java, +); +const kGetDefaultPrecompiledObjectResponse = GetPrecompiledObjectResponse( + example: exampleMock1, +); + +const kGetPrecompiledObjectCodeResponse = GetPrecompiledObjectCodeResponse( + code: 'test source', +); +const kOutputResponse = OutputResponse(output: 'test outputs'); + +const kRequestForExampleInfo = GetPrecompiledObjectRequest( + path: 'SDK_PYTHON/Category/Name', + sdk: Sdk.python, +); +const kRequestDefaultExampleForJava = GetDefaultPrecompiledObjectRequest( + sdk: Sdk.java, +); +const kRequestDefaultExampleForGo = GetDefaultPrecompiledObjectRequest( + sdk: Sdk.go, +); +const kRequestDefaultExampleForPython = GetDefaultPrecompiledObjectRequest( + sdk: Sdk.python, +); +const kRequestDefaultExampleForScio = GetDefaultPrecompiledObjectRequest( + sdk: Sdk.scio, +); diff --git a/playground/frontend/test/pages/playground/states/playground_state_test.dart b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart similarity index 79% rename from playground/frontend/test/pages/playground/states/playground_state_test.dart rename to playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart index 411a935bc2a2..506102cce5cc 100644 --- a/playground/frontend/test/pages/playground/states/playground_state_test.dart +++ b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart @@ -19,32 +19,32 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; -import 'package:playground/pages/playground/states/example_loaders/examples_loader.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/src/cache/example_cache.dart'; +import 'package:playground_components/src/controllers/example_loaders/examples_loader.dart'; +import 'package:playground_components/src/controllers/playground_controller.dart'; +import 'package:playground_components/src/models/sdk.dart'; -import 'mocks/example_mock.dart'; -import 'playground_state_test.mocks.dart'; +import '../common/examples.dart'; +import 'playground_controller_test.mocks.dart'; -@GenerateMocks([ExamplesLoader, ExampleState]) +@GenerateMocks([ExamplesLoader, ExampleCache]) void main() { - late PlaygroundState state; + late PlaygroundController state; final mockExamplesLoader = MockExamplesLoader(); when(mockExamplesLoader.load(any)).thenAnswer((_) async => 1); setUp(() { - state = PlaygroundState( + state = PlaygroundController( examplesLoader: MockExamplesLoader(), - exampleState: MockExampleState(), + exampleCache: MockExampleCache(), ); }); test('Initial value of SDK field should be null', () { expect(state.sdk, null); - state.setSdk(SDK.go); - expect(state.sdk, SDK.go); + state.setSdk(Sdk.go); + expect(state.sdk, Sdk.go); }); test('Initial value of examplesTitle should be equal to kTitle', () { @@ -57,13 +57,13 @@ void main() { test('Initial value of pipelineOptions should be empty string', () { expect(state.pipelineOptions, null); - state.setSdk(SDK.go); + state.setSdk(Sdk.go); expect(state.pipelineOptions, ''); }); test('Initial value of source should be empty string', () { expect(state.source, null); - state.setSdk(SDK.go); + state.setSdk(Sdk.go); expect(state.source, ''); }); @@ -73,7 +73,10 @@ void main() { () { state.setExample(exampleMock1, setCurrentSdk: true); expect(state.isExampleChanged, false); - state.selectedExample!.setSource('test'); + // 'test' in this line hits a bug fixed here: + // https://github.com/akvelon/flutter-code-editor/commit/c74ce566bf873dc76a5269ce6fe7b02df9c148e0 + // TODO(alexeyinkin): revert from 'test1' to 'test' when Akvelon's editor is integrated. + state.setSource('test1'); expect(state.isExampleChanged, true); }, ); @@ -101,7 +104,7 @@ void main() { 'Playground state setExample should update source and example and notify all listeners', () { state.addListener(() { - expect(state.sdk, SDK.go); + expect(state.sdk, Sdk.go); expect(state.source, exampleMockGo.source); expect(state.selectedExample, exampleMockGo); }); @@ -111,9 +114,9 @@ void main() { test('Playground state should notify all listeners about sdk change', () { state.addListener(() { - expect(state.sdk, SDK.go); + expect(state.sdk, Sdk.go); }); - state.setSdk(SDK.go); + state.setSdk(Sdk.go); }); test( @@ -138,7 +141,7 @@ void main() { test( 'Playground state should notify all listeners about pipeline options change', () { - state.setSdk(SDK.go); + state.setSdk(Sdk.go); state.addListener(() { expect(state.pipelineOptions, 'test options'); }); diff --git a/playground/frontend/test/modules/editor/repository/code_repository/code_repository_test.dart b/playground/frontend/playground_components/test/src/repositories/code_repository_test.dart similarity index 65% rename from playground/frontend/test/modules/editor/repository/code_repository/code_repository_test.dart rename to playground/frontend/playground_components/test/src/repositories/code_repository_test.dart index 344840d29d80..757e4f57ba3f 100644 --- a/playground/frontend/test/modules/editor/repository/code_repository/code_repository_test.dart +++ b/playground/frontend/playground_components/test/src/repositories/code_repository_test.dart @@ -19,20 +19,20 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/check_status_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/code_client.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_client/run_code_response.dart'; -import 'package:playground/modules/editor/repository/code_repository/code_repository.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_request.dart'; -import 'package:playground/modules/editor/repository/code_repository/run_code_result.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/src/models/sdk.dart'; +import 'package:playground_components/src/repositories/code_client/code_client.dart'; +import 'package:playground_components/src/repositories/code_repository.dart'; +import 'package:playground_components/src/repositories/models/check_status_response.dart'; +import 'package:playground_components/src/repositories/models/output_response.dart'; +import 'package:playground_components/src/repositories/models/run_code_request.dart'; +import 'package:playground_components/src/repositories/models/run_code_response.dart'; +import 'package:playground_components/src/repositories/models/run_code_result.dart'; import 'code_repository_test.mocks.dart'; -final kRequestMock = RunCodeRequestWrapper( +const kRequestMock = RunCodeRequest( code: 'code', - sdk: SDK.java, + sdk: Sdk.java, pipelineOptions: {}, ); @@ -45,25 +45,25 @@ const kRunErrorOutput = 'RunErrorOutput'; const kPreparationErrorOutput = 'PreparationErrorOutput'; const kValidationErrorOutput = 'ValidationErrorOutput'; -final kRunCodeResponse = RunCodeResponse(kPipelineUuid); -final kFinishedStatusResponse = CheckStatusResponse(RunCodeStatus.finished); -final kErrorStatusResponse = CheckStatusResponse(RunCodeStatus.unknownError); -final kRunErrorStatusResponse = CheckStatusResponse(RunCodeStatus.runError); -final kExecutingStatusResponse = CheckStatusResponse(RunCodeStatus.executing); -final kCompileErrorStatusResponse = - CheckStatusResponse(RunCodeStatus.compileError); -final kValidationErrorStatusResponse = - CheckStatusResponse(RunCodeStatus.validationError); -final kPreparationErrorStatusResponse = - CheckStatusResponse(RunCodeStatus.preparationError); +const kRunCodeResponse = RunCodeResponse(pipelineUuid: kPipelineUuid); +const kFinishedStatusResponse = CheckStatusResponse(status: RunCodeStatus.finished,); +const kErrorStatusResponse = CheckStatusResponse(status: RunCodeStatus.unknownError,); +const kRunErrorStatusResponse = CheckStatusResponse(status: RunCodeStatus.runError,); +const kExecutingStatusResponse = CheckStatusResponse(status: RunCodeStatus.executing,); +const kCompileErrorStatusResponse = + CheckStatusResponse(status: RunCodeStatus.compileError,); +const kValidationErrorStatusResponse = + CheckStatusResponse(status: RunCodeStatus.validationError,); +const kPreparationErrorStatusResponse = + CheckStatusResponse(status: RunCodeStatus.preparationError,); -final kRunOutputResponse = OutputResponse(kRunOutput); -final kLogOutputResponse = OutputResponse(kLogOutput); -final kCompileOutputResponse = OutputResponse(kCompileOutput); -final kRunErrorOutputResponse = OutputResponse(kRunErrorOutput); -final kGraphResponse = OutputResponse(kGraphOutput); -final kValidationErrorOutputResponse = OutputResponse(kValidationErrorOutput); -final kPreparationErrorOutputResponse = OutputResponse(kPreparationErrorOutput); +const kRunOutputResponse = OutputResponse(output: kRunOutput); +const kLogOutputResponse = OutputResponse(output: kLogOutput); +const kCompileOutputResponse = OutputResponse(output: kCompileOutput); +const kRunErrorOutputResponse = OutputResponse(output: kRunErrorOutput); +const kGraphResponse = OutputResponse(output: kGraphOutput); +const kValidationErrorOutputResponse = OutputResponse(output: kValidationErrorOutput); +const kPreparationErrorOutputResponse = OutputResponse(output: kPreparationErrorOutput); @GenerateMocks([CodeClient]) void main() { @@ -74,27 +74,27 @@ void main() { when(client.runCode(kRequestMock)).thenAnswer( (_) async => kRunCodeResponse, ); - when(client.checkStatus(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.checkStatus(kPipelineUuid)).thenAnswer( (_) async => kFinishedStatusResponse, ); - when(client.getRunOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getRunOutput(kPipelineUuid)).thenAnswer( (_) async => kRunOutputResponse, ); - when(client.getCompileOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getCompileOutput(kPipelineUuid)).thenAnswer( (_) async => kCompileOutputResponse, ); - when(client.getRunErrorOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getRunErrorOutput(kPipelineUuid)).thenAnswer( (_) async => kRunErrorOutputResponse, ); - when(client.getLogOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getLogOutput(kPipelineUuid)).thenAnswer( (_) async => kLogOutputResponse, ); - when(client.getGraphOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getGraphOutput(kPipelineUuid)).thenAnswer( (_) async => kGraphResponse, ); // test variables - final repository = CodeRepository(client); + final repository = CodeRepository(client: client); final stream = repository.runCode(kRequestMock); // test assertion @@ -115,7 +115,7 @@ void main() { ]), ); // compile output should not be called - verifyNever(client.getCompileOutput(kPipelineUuid, kRequestMock)); + verifyNever(client.getCompileOutput(kPipelineUuid)); }); test('should return output from compilation if failed', () async { @@ -124,24 +124,24 @@ void main() { when(client.runCode(kRequestMock)).thenAnswer( (_) async => kRunCodeResponse, ); - when(client.checkStatus(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.checkStatus(kPipelineUuid)).thenAnswer( (_) async => kCompileErrorStatusResponse, ); - when(client.getCompileOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getCompileOutput(kPipelineUuid)).thenAnswer( (_) async => kCompileOutputResponse, ); - when(client.getRunOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getRunOutput(kPipelineUuid)).thenAnswer( (_) async => kRunOutputResponse, ); - when(client.getLogOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getLogOutput(kPipelineUuid)).thenAnswer( (_) async => kLogOutputResponse, ); - when(client.getGraphOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getGraphOutput(kPipelineUuid)).thenAnswer( (_) async => kGraphResponse, ); // test variables - final repository = CodeRepository(client); + final repository = CodeRepository(client: client); final stream = repository.runCode(kRequestMock); // test assertion @@ -170,19 +170,19 @@ void main() { when(client.runCode(kRequestMock)).thenAnswer( (_) async => kRunCodeResponse, ); - when(client.checkStatus(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.checkStatus(kPipelineUuid)).thenAnswer( (_) async => kValidationErrorStatusResponse, ); - when(client.getValidationErrorOutput(kPipelineUuid, kRequestMock)) + when(client.getValidationErrorOutput(kPipelineUuid)) .thenAnswer( (_) async => kValidationErrorOutputResponse, ); - when(client.getGraphOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getGraphOutput(kPipelineUuid)).thenAnswer( (_) async => kGraphResponse, ); // test variables - final repository = CodeRepository(client); + final repository = CodeRepository(client: client); final stream = repository.runCode(kRequestMock); // test assertion @@ -210,19 +210,19 @@ void main() { when(client.runCode(kRequestMock)).thenAnswer( (_) async => kRunCodeResponse, ); - when(client.checkStatus(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.checkStatus(kPipelineUuid)).thenAnswer( (_) async => kPreparationErrorStatusResponse, ); - when(client.getPreparationErrorOutput(kPipelineUuid, kRequestMock)) + when(client.getPreparationErrorOutput(kPipelineUuid)) .thenAnswer( (_) async => kPreparationErrorOutputResponse, ); - when(client.getGraphOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getGraphOutput(kPipelineUuid)).thenAnswer( (_) async => kGraphResponse, ); // test variables - final repository = CodeRepository(client); + final repository = CodeRepository(client: client); final stream = repository.runCode(kRequestMock); // test assertion @@ -250,27 +250,27 @@ void main() { when(client.runCode(kRequestMock)).thenAnswer( (_) async => kRunCodeResponse, ); - when(client.checkStatus(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.checkStatus(kPipelineUuid)).thenAnswer( (_) async => kRunErrorStatusResponse, ); - when(client.getCompileOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getCompileOutput(kPipelineUuid)).thenAnswer( (_) async => kCompileOutputResponse, ); - when(client.getRunOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getRunOutput(kPipelineUuid)).thenAnswer( (_) async => kRunOutputResponse, ); - when(client.getRunErrorOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getRunErrorOutput(kPipelineUuid)).thenAnswer( (_) async => kRunErrorOutputResponse, ); - when(client.getLogOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getLogOutput(kPipelineUuid)).thenAnswer( (_) async => kLogOutputResponse, ); - when(client.getGraphOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getGraphOutput(kPipelineUuid)).thenAnswer( (_) async => kGraphResponse, ); // test variables - final repository = CodeRepository(client); + final repository = CodeRepository(client: client); final stream = repository.runCode(kRequestMock); // test assertion @@ -305,24 +305,24 @@ void main() { kExecutingStatusResponse, kFinishedStatusResponse ]; - when(client.checkStatus(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.checkStatus(kPipelineUuid)).thenAnswer( (_) async => answers.removeAt(0), ); - when(client.getRunOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getRunOutput(kPipelineUuid)).thenAnswer( (_) async => kRunOutputResponse, ); - when(client.getRunErrorOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getRunErrorOutput(kPipelineUuid)).thenAnswer( (_) async => kRunErrorOutputResponse, ); - when(client.getLogOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getLogOutput(kPipelineUuid)).thenAnswer( (_) async => kLogOutputResponse, ); - when(client.getGraphOutput(kPipelineUuid, kRequestMock)).thenAnswer( + when(client.getGraphOutput(kPipelineUuid)).thenAnswer( (_) async => kGraphResponse, ); // test variables - final repository = CodeRepository(client); + final repository = CodeRepository(client: client); final stream = repository.runCode(kRequestMock); // test assertion diff --git a/playground/frontend/playground_components/test/src/repositories/example_repository_test.dart b/playground/frontend/playground_components/test/src/repositories/example_repository_test.dart new file mode 100644 index 000000000000..7046f1e553c6 --- /dev/null +++ b/playground/frontend/playground_components/test/src/repositories/example_repository_test.dart @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:playground_components/src/repositories/example_client/example_client.dart'; +import 'package:playground_components/src/repositories/example_repository.dart'; + +import '../common/requests.dart'; +import 'example_repository_test.mocks.dart'; + +@GenerateMocks([ExampleClient]) +void main() { + late ExampleRepository repo; + late ExampleClient client; + + setUp( + () { + client = MockExampleClient(); + repo = ExampleRepository(client: client); + }, + ); + + test( + 'Example repository getListOfExamples should return response with categories', + () async { + when(client.getPrecompiledObjects(kGetPrecompiledObjectsRequest)) + .thenAnswer((_) async => kGetPrecompiledObjectsResponse); + expect( + await repo.getListOfExamples(kGetPrecompiledObjectsRequest), + kGetPrecompiledObjectsResponse.categories, + ); + verify(client.getPrecompiledObjects(kGetPrecompiledObjectsRequest)).called(1); + }, + ); + + test( + 'Example repository getDefaultExample should return defaultExample for chosen Sdk', + () async { + when(client.getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest)) + .thenAnswer((_) async => kGetDefaultPrecompiledObjectResponse); + expect( + await repo.getDefaultExample(kGetDefaultPrecompiledObjectRequest), + kGetDefaultPrecompiledObjectResponse.example, + ); + verify(client.getDefaultPrecompiledObject(kGetDefaultPrecompiledObjectRequest)).called(1); + }, + ); + + test( + 'Example repository getExampleSource should return source code for example', + () async { + when(client.getPrecompiledObjectCode(kRequestForExampleInfo)) + .thenAnswer((_) async => kGetPrecompiledObjectCodeResponse); + expect( + await repo.getExampleSource(kRequestForExampleInfo), + kGetPrecompiledObjectCodeResponse.code, + ); + verify(client.getPrecompiledObjectCode(kRequestForExampleInfo)).called(1); + }, + ); + + test( + 'Example repository getExampleOutput should return output for example', + () async { + when(client.getPrecompiledObjectOutput(kRequestForExampleInfo)) + .thenAnswer((_) async => kOutputResponse); + expect( + await repo.getExampleOutput(kRequestForExampleInfo), + kOutputResponse.output, + ); + verify(client.getPrecompiledObjectOutput(kRequestForExampleInfo)).called(1); + }, + ); + + test( + 'Example repository getExampleLogs should return logs for example', + () async { + when(client.getPrecompiledObjectLogs(kRequestForExampleInfo)) + .thenAnswer((_) async => kOutputResponse); + expect( + await repo.getExampleLogs(kRequestForExampleInfo), + kOutputResponse.output, + ); + verify(client.getPrecompiledObjectLogs(kRequestForExampleInfo)).called(1); + }, + ); + + test( + 'Example repository getExampleGraph should return logs for example', + () async { + when(client.getPrecompiledObjectGraph(kRequestForExampleInfo)) + .thenAnswer((_) async => kOutputResponse); + expect( + await repo.getExampleGraph(kRequestForExampleInfo), + kOutputResponse.output, + ); + verify(client.getPrecompiledObjectGraph(kRequestForExampleInfo)).called(1); + }, + ); + + test( + 'Example repository getExample should return ExampleModel', + () async { + when(client.getPrecompiledObject(kRequestForExampleInfo)) + .thenAnswer((_) async => kGetDefaultPrecompiledObjectResponse); + expect( + await repo.getExample(kRequestForExampleInfo), + kGetDefaultPrecompiledObjectResponse.example, + ); + verify(client.getPrecompiledObject(kRequestForExampleInfo)).called(1); + }, + ); +} diff --git a/playground/frontend/test/modules/editor/parsers/run_options_parser_test.dart b/playground/frontend/playground_components/test/src/util/pipeline_options_test.dart similarity index 96% rename from playground/frontend/test/modules/editor/parsers/run_options_parser_test.dart rename to playground/frontend/playground_components/test/src/util/pipeline_options_test.dart index 26657af6cc6c..cb15e5b046e6 100644 --- a/playground/frontend/test/modules/editor/parsers/run_options_parser_test.dart +++ b/playground/frontend/playground_components/test/src/util/pipeline_options_test.dart @@ -17,7 +17,7 @@ */ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/modules/editor/parsers/run_options_parser.dart'; +import 'package:playground_components/src/util/pipeline_options.dart'; void main() { group('PipelineOptions parser', () { diff --git a/playground/frontend/test/utils/run_with_retry_test.dart b/playground/frontend/playground_components/test/src/util/run_with_retry_test.dart similarity index 97% rename from playground/frontend/test/utils/run_with_retry_test.dart rename to playground/frontend/playground_components/test/src/util/run_with_retry_test.dart index 39d7a9ddb6b6..5c655a01f3c4 100644 --- a/playground/frontend/test/utils/run_with_retry_test.dart +++ b/playground/frontend/playground_components/test/src/util/run_with_retry_test.dart @@ -17,7 +17,7 @@ */ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/utils/run_with_retry.dart'; +import 'package:playground_components/src/util/run_with_retry.dart'; class ExecutionTime { final int time; diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock index eb1df6618a43..757308ffe2c9 100644 --- a/playground/frontend/pubspec.lock +++ b/playground/frontend/pubspec.lock @@ -35,7 +35,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.3.1" args: dependency: transitive description: @@ -49,7 +49,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.2" + version: "2.9.0" boolean_selector: dependency: transitive description: @@ -119,7 +119,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" charcode: dependency: transitive description: @@ -140,7 +140,7 @@ packages: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: @@ -149,7 +149,7 @@ packages: source: hosted version: "4.1.0" code_text_field: - dependency: "direct main" + dependency: transitive description: path: "." ref: "9e2c9fe52a69481f038f4b6609e8a0a776429437" @@ -185,6 +185,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.2" + csv: + dependency: transitive + description: + name: csv + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.1" dart_style: dependency: transitive description: @@ -192,6 +199,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.3" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + easy_localization_ext: + dependency: "direct main" + description: + name: easy_localization_ext + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" + easy_localization_loader: + dependency: "direct main" + description: + name: easy_localization_loader + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + easy_logger: + dependency: transitive + description: + name: easy_logger + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" + equatable: + dependency: transitive + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" expansion_widget: dependency: "direct main" description: @@ -205,7 +247,7 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: @@ -257,7 +299,7 @@ packages: name: flutter_svg url: "https://pub.dartlang.org" source: hosted - version: "0.22.0" + version: "1.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -283,12 +325,12 @@ packages: source: hosted version: "2.0.2" google_fonts: - dependency: "direct main" + dependency: transitive description: name: google_fonts url: "https://pub.dartlang.org" source: hosted - version: "2.3.3" + version: "3.0.1" googleapis_auth: dependency: transitive description: @@ -304,14 +346,14 @@ packages: source: hosted version: "2.1.0" grpc: - dependency: "direct main" + dependency: transitive description: name: grpc url: "https://pub.dartlang.org" source: hosted version: "3.0.2" highlight: - dependency: "direct main" + dependency: transitive description: name: highlight url: "https://pub.dartlang.org" @@ -407,21 +449,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.11" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.5" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" mime: dependency: transitive description: @@ -463,21 +505,21 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing url: "https://pub.dartlang.org" source: hosted - version: "0.5.1+1" + version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing url: "https://pub.dartlang.org" source: hosted - version: "0.2.1" + version: "1.0.1" path_provider: dependency: transitive description: @@ -541,6 +583,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + playground_components: + dependency: "direct main" + description: + path: playground_components + relative: true + source: path + version: "0.0.1" plugin_platform_interface: dependency: transitive description: @@ -568,7 +617,7 @@ packages: name: protobuf url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.1.0" provider: dependency: "direct main" description: @@ -678,7 +727,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.2" + version: "1.9.0" stack_trace: dependency: transitive description: @@ -706,21 +755,21 @@ packages: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.12" timing: dependency: transitive description: @@ -805,6 +854,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + url_strategy: + dependency: "direct main" + description: + name: url_strategy + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" usage: dependency: "direct main" description: @@ -869,5 +925,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.17.6 <3.0.0" + flutter: ">=3.3.1" diff --git a/playground/frontend/pubspec.yaml b/playground/frontend/pubspec.yaml index 91027ecd99fc..b989f7fe4443 100644 --- a/playground/frontend/pubspec.yaml +++ b/playground/frontend/pubspec.yaml @@ -22,28 +22,28 @@ version: 1.0.0+1 environment: sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.3.1" dependencies: akvelon_flutter_issue_106664_workaround: ^0.1.2 aligned_dialog: ^0.0.6 - code_text_field: - git: - url: https://github.com/BertrandBev/code_field.git - ref: 9e2c9fe52a69481f038f4b6609e8a0a776429437 collection: ^1.15.0 + easy_localization: ^3.0.1 + easy_localization_ext: ^0.1.1 + easy_localization_loader: ^1.0.0 expansion_widget: ^0.0.2 flutter: { sdk: flutter } flutter_localizations: { sdk: flutter } - flutter_svg: ^0.22.0 + flutter_svg: ^1.0.3 flutter_web_plugins: { sdk: flutter } - google_fonts: ^2.3.1 - grpc: ^3.0.0 - highlight: ^0.7.0 + google_fonts: ^3.0.1 intl: ^0.17.0 onmessage: ^0.2.0 + playground_components: { path: playground_components } provider: ^6.0.0 shared_preferences: ^2.0.12 url_launcher: ^6.0.12 + url_strategy: ^0.2.0 usage: ^4.0.2 dev_dependencies: @@ -56,6 +56,7 @@ dev_dependencies: flutter: assets: - assets/ + - assets/translations/en.yaml generate: true uses-material-design: true diff --git a/playground/frontend/test/modules/editor/repository/code_repository/code_repository_test.mocks.dart b/playground/frontend/test/modules/editor/repository/code_repository/code_repository_test.mocks.dart deleted file mode 100644 index b2429f18816c..000000000000 --- a/playground/frontend/test/modules/editor/repository/code_repository/code_repository_test.mocks.dart +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// Mocks generated by Mockito 5.0.16 from annotations -// in playground/test/modules/editor/repository/code_repository/code_repository_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i6; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:playground/modules/editor/repository/code_repository/code_client/check_status_response.dart' - as _i3; -import 'package:playground/modules/editor/repository/code_repository/code_client/code_client.dart' - as _i5; -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart' - as _i4; -import 'package:playground/modules/editor/repository/code_repository/code_client/run_code_response.dart' - as _i2; -import 'package:playground/modules/editor/repository/code_repository/run_code_request.dart' - as _i7; - -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -class _FakeRunCodeResponse_0 extends _i1.Fake implements _i2.RunCodeResponse {} - -class _FakeCheckStatusResponse_1 extends _i1.Fake - implements _i3.CheckStatusResponse {} - -class _FakeOutputResponse_2 extends _i1.Fake implements _i4.OutputResponse {} - -/// A class which mocks [CodeClient]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockCodeClient extends _i1.Mock implements _i5.CodeClient { - MockCodeClient() { - _i1.throwOnMissingStub(this); - } - - @override - _i6.Future<_i2.RunCodeResponse> runCode(_i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#runCode, [request]), - returnValue: - Future<_i2.RunCodeResponse>.value(_FakeRunCodeResponse_0())) - as _i6.Future<_i2.RunCodeResponse>); - @override - _i6.Future cancelExecution(String? pipelineUuid) => - (super.noSuchMethod(Invocation.method(#cancelExecution, [pipelineUuid]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i6.Future); - @override - _i6.Future<_i3.CheckStatusResponse> checkStatus( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method(#checkStatus, [pipelineUuid, request]), - returnValue: Future<_i3.CheckStatusResponse>.value( - _FakeCheckStatusResponse_1())) - as _i6.Future<_i3.CheckStatusResponse>); - @override - _i6.Future<_i4.OutputResponse> getCompileOutput( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method(#getCompileOutput, [pipelineUuid, request]), - returnValue: - Future<_i4.OutputResponse>.value(_FakeOutputResponse_2())) - as _i6.Future<_i4.OutputResponse>); - @override - _i6.Future<_i4.OutputResponse> getRunOutput( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method(#getRunOutput, [pipelineUuid, request]), - returnValue: - Future<_i4.OutputResponse>.value(_FakeOutputResponse_2())) - as _i6.Future<_i4.OutputResponse>); - @override - _i6.Future<_i4.OutputResponse> getLogOutput( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method(#getLogOutput, [pipelineUuid, request]), - returnValue: - Future<_i4.OutputResponse>.value(_FakeOutputResponse_2())) - as _i6.Future<_i4.OutputResponse>); - @override - _i6.Future<_i4.OutputResponse> getRunErrorOutput( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method(#getRunErrorOutput, [pipelineUuid, request]), - returnValue: - Future<_i4.OutputResponse>.value(_FakeOutputResponse_2())) - as _i6.Future<_i4.OutputResponse>); - @override - _i6.Future<_i4.OutputResponse> getValidationErrorOutput( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method(#getValidationErrorOutput, [pipelineUuid, request]), - returnValue: - Future<_i4.OutputResponse>.value(_FakeOutputResponse_2())) as _i6 - .Future<_i4.OutputResponse>); - @override - _i6.Future<_i4.OutputResponse> getPreparationErrorOutput( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method( - #getPreparationErrorOutput, [pipelineUuid, request]), - returnValue: - Future<_i4.OutputResponse>.value(_FakeOutputResponse_2())) - as _i6.Future<_i4.OutputResponse>); - @override - _i6.Future<_i4.OutputResponse> getGraphOutput( - String? pipelineUuid, _i7.RunCodeRequestWrapper? request) => - (super.noSuchMethod( - Invocation.method(#getGraphOutput, [pipelineUuid, request]), - returnValue: - Future<_i4.OutputResponse>.value(_FakeOutputResponse_2())) - as _i6.Future<_i4.OutputResponse>); - @override - String toString() => super.toString(); -} diff --git a/playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.dart b/playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.dart deleted file mode 100644 index 5e08b3773847..000000000000 --- a/playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.dart +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:playground/modules/examples/repositories/example_client/example_client.dart'; -import 'package:playground/modules/examples/repositories/example_repository.dart'; - -import '../../../../pages/playground/states/mocks/request_mock.dart'; -import 'example_repository_test.mocks.dart'; - -@GenerateMocks([ExampleClient]) -void main() { - late ExampleRepository repo; - late ExampleClient client; - - setUp( - () { - client = MockExampleClient(); - repo = ExampleRepository(client); - }, - ); - - test( - 'Example repository getListOfExamples should return response with categories', - () async { - when(client.getListOfExamples(kGetListOfExamplesRequestMock)) - .thenAnswer((_) async => kGetListOfExamplesResponseMock); - expect( - await repo.getListOfExamples(kGetListOfExamplesRequestMock), - kGetListOfExamplesResponseMock.categories, - ); - verify(client.getListOfExamples(kGetListOfExamplesRequestMock)).called(1); - }, - ); - - test( - 'Example repository getDefaultExample should return defaultExample for chosen Sdk', - () async { - when(client.getDefaultExample(kGetExampleRequestMock)) - .thenAnswer((_) async => kGetExampleResponseMock); - expect( - await repo.getDefaultExample(kGetExampleRequestMock), - kGetExampleResponseMock.example, - ); - verify(client.getDefaultExample(kGetExampleRequestMock)).called(1); - }, - ); - - test( - 'Example repository getExampleSource should return source code for example', - () async { - when(client.getExampleSource(kGetExampleRequestMock)) - .thenAnswer((_) async => kGetExampleCodeResponseMock); - expect( - await repo.getExampleSource(kGetExampleRequestMock), - kGetExampleCodeResponseMock.code, - ); - verify(client.getExampleSource(kGetExampleRequestMock)).called(1); - }, - ); - - test( - 'Example repository getExampleOutput should return output for example', - () async { - when(client.getExampleOutput(kGetExampleRequestMock)) - .thenAnswer((_) async => kOutputResponseMock); - expect( - await repo.getExampleOutput(kGetExampleRequestMock), - kOutputResponseMock.output, - ); - verify(client.getExampleOutput(kGetExampleRequestMock)).called(1); - }, - ); - - test( - 'Example repository getExampleLogs should return logs for example', - () async { - when(client.getExampleLogs(kGetExampleRequestMock)) - .thenAnswer((_) async => kOutputResponseMock); - expect( - await repo.getExampleLogs(kGetExampleRequestMock), - kOutputResponseMock.output, - ); - verify(client.getExampleLogs(kGetExampleRequestMock)).called(1); - }, - ); - - test( - 'Example repository getExampleLogs should return logs for example', - () async { - when(client.getExampleGraph(kGetExampleRequestMock)) - .thenAnswer((_) async => kOutputResponseMock); - expect( - await repo.getExampleGraph(kGetExampleRequestMock), - kOutputResponseMock.output, - ); - verify(client.getExampleGraph(kGetExampleRequestMock)).called(1); - }, - ); - - test( - 'Example repository getExample should return ExampleModel', - () async { - when(client.getExample(kGetExampleRequestMock)) - .thenAnswer((_) async => kGetExampleResponseMock); - expect( - await repo.getExample(kGetExampleRequestMock), - kGetExampleResponseMock.example, - ); - verify(client.getExample(kGetExampleRequestMock)).called(1); - }, - ); -} diff --git a/playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.mocks.dart b/playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.mocks.dart deleted file mode 100644 index 92bcb2546a5a..000000000000 --- a/playground/frontend/test/modules/editor/repository/example_repository/example_repository_test.mocks.dart +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Mocks generated by Mockito 5.1.0 from annotations -// in playground/test/modules/editor/repository/example_repository/example_repository_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i9; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart' - as _i5; -import 'package:playground/modules/examples/repositories/example_client/example_client.dart' - as _i8; -import 'package:playground/modules/examples/repositories/models/get_example_code_response.dart' - as _i3; -import 'package:playground/modules/examples/repositories/models/get_example_request.dart' - as _i11; -import 'package:playground/modules/examples/repositories/models/get_example_response.dart' - as _i4; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart' - as _i10; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_response.dart' - as _i2; -import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart' - as _i12; -import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart' - as _i6; -import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart' - as _i13; -import 'package:playground/modules/examples/repositories/models/save_snippet_response.dart' - as _i7; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -class _FakeGetListOfExampleResponse_0 extends _i1.Fake - implements _i2.GetListOfExampleResponse {} - -class _FakeGetExampleCodeResponse_1 extends _i1.Fake - implements _i3.GetExampleCodeResponse {} - -class _FakeGetExampleResponse_2 extends _i1.Fake - implements _i4.GetExampleResponse {} - -class _FakeOutputResponse_3 extends _i1.Fake implements _i5.OutputResponse {} - -class _FakeGetSnippetResponse_4 extends _i1.Fake - implements _i6.GetSnippetResponse {} - -class _FakeSaveSnippetResponse_5 extends _i1.Fake - implements _i7.SaveSnippetResponse {} - -/// A class which mocks [ExampleClient]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockExampleClient extends _i1.Mock implements _i8.ExampleClient { - MockExampleClient() { - _i1.throwOnMissingStub(this); - } - - @override - _i9.Future<_i2.GetListOfExampleResponse> getListOfExamples( - _i10.GetListOfExamplesRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getListOfExamples, [request]), - returnValue: Future<_i2.GetListOfExampleResponse>.value( - _FakeGetListOfExampleResponse_0())) - as _i9.Future<_i2.GetListOfExampleResponse>); - @override - _i9.Future<_i3.GetExampleCodeResponse> getExampleSource( - _i11.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleSource, [request]), - returnValue: Future<_i3.GetExampleCodeResponse>.value( - _FakeGetExampleCodeResponse_1())) - as _i9.Future<_i3.GetExampleCodeResponse>); - @override - _i9.Future<_i4.GetExampleResponse> getDefaultExample( - _i11.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getDefaultExample, [request]), - returnValue: Future<_i4.GetExampleResponse>.value( - _FakeGetExampleResponse_2())) - as _i9.Future<_i4.GetExampleResponse>); - @override - _i9.Future<_i4.GetExampleResponse> getExample( - _i11.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExample, [request]), - returnValue: Future<_i4.GetExampleResponse>.value( - _FakeGetExampleResponse_2())) - as _i9.Future<_i4.GetExampleResponse>); - @override - _i9.Future<_i5.OutputResponse> getExampleOutput( - _i11.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleOutput, [request]), - returnValue: - Future<_i5.OutputResponse>.value(_FakeOutputResponse_3())) - as _i9.Future<_i5.OutputResponse>); - @override - _i9.Future<_i5.OutputResponse> getExampleLogs( - _i11.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleLogs, [request]), - returnValue: - Future<_i5.OutputResponse>.value(_FakeOutputResponse_3())) - as _i9.Future<_i5.OutputResponse>); - @override - _i9.Future<_i5.OutputResponse> getExampleGraph( - _i11.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleGraph, [request]), - returnValue: - Future<_i5.OutputResponse>.value(_FakeOutputResponse_3())) - as _i9.Future<_i5.OutputResponse>); - @override - _i9.Future<_i6.GetSnippetResponse> getSnippet( - _i12.GetSnippetRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getSnippet, [request]), - returnValue: Future<_i6.GetSnippetResponse>.value( - _FakeGetSnippetResponse_4())) - as _i9.Future<_i6.GetSnippetResponse>); - @override - _i9.Future<_i7.SaveSnippetResponse> saveSnippet( - _i13.SaveSnippetRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#saveSnippet, [request]), - returnValue: Future<_i7.SaveSnippetResponse>.value( - _FakeSaveSnippetResponse_5())) - as _i9.Future<_i7.SaveSnippetResponse>); -} diff --git a/playground/frontend/test/modules/messages/handlers/messages_debouncer_test.dart b/playground/frontend/test/modules/messages/handlers/messages_debouncer_test.dart index df5266c23dd5..7999493bea33 100644 --- a/playground/frontend/test/modules/messages/handlers/messages_debouncer_test.dart +++ b/playground/frontend/test/modules/messages/handlers/messages_debouncer_test.dart @@ -24,7 +24,7 @@ import 'package:playground/modules/messages/handlers/abstract_message_handler.da import 'package:playground/modules/messages/handlers/messages_debouncer.dart'; import 'package:playground/modules/messages/models/abstract_message.dart'; import 'package:playground/modules/messages/models/set_sdk_message.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; void main() { group('MessagesDebouncer', () { @@ -38,13 +38,13 @@ void main() { test('drops sequential calls, no time limit', () { fakeAsync((async) { - debouncer.handle(SetSdkMessage(sdk: SDK.java)); - debouncer.handle(SetSdkMessage(sdk: SDK.java)); + debouncer.handle(SetSdkMessage(sdk: Sdk.java)); + debouncer.handle(SetSdkMessage(sdk: Sdk.java)); async.elapse(const Duration(days: 36500)); - debouncer.handle(SetSdkMessage(sdk: SDK.java)); + debouncer.handle(SetSdkMessage(sdk: Sdk.java)); }); - expect(recorder.messages, [SetSdkMessage(sdk: SDK.java)]); + expect(recorder.messages, [SetSdkMessage(sdk: Sdk.java)]); }); test('returns the last result on debouncing', () { diff --git a/playground/frontend/test/modules/messages/models/set_content_message_test.dart b/playground/frontend/test/modules/messages/models/set_content_message_test.dart index c472a8269e44..a552501b84a9 100644 --- a/playground/frontend/test/modules/messages/models/set_content_message_test.dart +++ b/playground/frontend/test/modules/messages/models/set_content_message_test.dart @@ -17,14 +17,11 @@ */ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/content_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart'; import 'package:playground/modules/messages/models/set_content_message.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; const _content = 'my_code'; -const _sdk = SDK.python; +const _sdk = Sdk.python; void main() { group('SetContentMessage.tryParse', () { @@ -50,7 +47,7 @@ void main() { parsed, const SetContentMessage( descriptor: ExamplesLoadingDescriptor( - descriptors: [EmptyExampleLoadingDescriptor(sdk: SDK.java)], + descriptors: [EmptyExampleLoadingDescriptor(sdk: Sdk.java)], ), ), ); @@ -71,7 +68,7 @@ void main() { parsed, const SetContentMessage( descriptor: ExamplesLoadingDescriptor( - descriptors: [EmptyExampleLoadingDescriptor(sdk: SDK.java)], + descriptors: [EmptyExampleLoadingDescriptor(sdk: Sdk.java)], ), ), ); @@ -94,16 +91,16 @@ void main() { { 'content': _content, 'name': 'name', - 'sdk': _sdk.name, + 'sdk': _sdk.id, }, { 'content': _content, 'name': null, - 'sdk': _sdk.name, + 'sdk': _sdk.id, }, { 'content': _content, - 'sdk': _sdk.name, + 'sdk': _sdk.id, }, ], }, diff --git a/playground/frontend/test/modules/messages/models/set_sdk_message_test.dart b/playground/frontend/test/modules/messages/models/set_sdk_message_test.dart index e608a5b5c024..c9223849bbfe 100644 --- a/playground/frontend/test/modules/messages/models/set_sdk_message_test.dart +++ b/playground/frontend/test/modules/messages/models/set_sdk_message_test.dart @@ -18,9 +18,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:playground/modules/messages/models/set_sdk_message.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; -const _sdk = SDK.python; +const _sdk = Sdk.python; void main() { group('SetSdkMessage.tryParse', () { @@ -49,7 +49,7 @@ void main() { test( 'parses an SDK', () { - final map = {'type': SetSdkMessage.type, 'sdk': _sdk.name}; + final map = {'type': SetSdkMessage.type, 'sdk': _sdk.id}; final parsed = SetSdkMessage.tryParse(map); diff --git a/playground/frontend/test/modules/messages/parsers/message_parser_test.dart b/playground/frontend/test/modules/messages/parsers/message_parser_test.dart index 24173cece319..00b580143457 100644 --- a/playground/frontend/test/modules/messages/parsers/message_parser_test.dart +++ b/playground/frontend/test/modules/messages/parsers/message_parser_test.dart @@ -17,14 +17,12 @@ */ import 'package:flutter_test/flutter_test.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/empty_example_loading_descriptor.dart'; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart'; import 'package:playground/modules/messages/models/set_content_message.dart'; import 'package:playground/modules/messages/models/set_sdk_message.dart'; import 'package:playground/modules/messages/parsers/messages_parser.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; +import 'package:playground_components/playground_components.dart'; -const _sdk = SDK.python; +const _sdk = Sdk.python; void main() { group('MessageParser.parse returns null for invalid inputs', () { @@ -96,7 +94,7 @@ void main() { parsed, const SetContentMessage( descriptor: ExamplesLoadingDescriptor( - descriptors: [EmptyExampleLoadingDescriptor(sdk: SDK.java)], + descriptors: [EmptyExampleLoadingDescriptor(sdk: Sdk.java)], ), ), ); @@ -106,7 +104,7 @@ void main() { test( 'MessageParser.parse parses SetSdkMessage', () { - final value = {'type': SetSdkMessage.type, 'sdk': _sdk.name}; + final value = {'type': SetSdkMessage.type, 'sdk': _sdk.id}; final parsed = MessagesParser().tryParse(value); diff --git a/playground/frontend/test/pages/playground/states/example_selector_state_test.dart b/playground/frontend/test/pages/playground/states/example_selector_state_test.dart index 99d847f38e07..1d1c37d4d957 100644 --- a/playground/frontend/test/pages/playground/states/example_selector_state_test.dart +++ b/playground/frontend/test/pages/playground/states/example_selector_state_test.dart @@ -18,30 +18,34 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/pages/playground/states/example_loaders/examples_loader.dart'; +import 'package:playground_components/src/models/example_base.dart'; +import 'package:playground_components/src/controllers/example_loaders/examples_loader.dart'; import 'package:playground/pages/playground/states/example_selector_state.dart'; -import 'package:playground/pages/playground/states/examples_state.dart'; -import 'package:playground/pages/playground/states/playground_state.dart'; +import 'package:playground_components/src/cache/example_cache.dart'; +import 'package:playground_components/src/controllers/playground_controller.dart'; import 'example_selector_state_test.mocks.dart'; -import 'mocks/categories_mock.dart'; -import 'mocks/example_repository_mock.dart'; +import '../../../../playground_components/test/src/common/categories.dart'; +import '../../../../playground_components/test/src/common/example_repository_mock.dart'; @GenerateMocks([ExamplesLoader]) void main() { - late PlaygroundState playgroundState; - late ExampleState exampleState; + late PlaygroundController playgroundController; + late ExampleCache exampleCache; late ExampleSelectorState state; final mockExampleRepository = getMockExampleRepository(); setUp(() { - exampleState = ExampleState(mockExampleRepository); - playgroundState = PlaygroundState( + exampleCache = ExampleCache( + exampleRepository: mockExampleRepository, + hasCatalog: true, + ); + + playgroundController = PlaygroundController( examplesLoader: MockExamplesLoader(), - exampleState: exampleState, + exampleCache: exampleCache, ); - state = ExampleSelectorState(playgroundState, []); + state = ExampleSelectorState(playgroundController, []); }); test( @@ -95,7 +99,7 @@ void main() { '- affect Example state categories', () { state.addListener(() { expect(state.categories, []); - expect(exampleState.sdkCategories, exampleState.sdkCategories); + expect(exampleCache.categoryListsBySdk, exampleCache.categoryListsBySdk); }); state.sortCategories(); }); @@ -107,12 +111,12 @@ void main() { 'but should NOT:' '- affect Example state categories', () { final state = ExampleSelectorState( - playgroundState, + playgroundController, categoriesMock, ); state.addListener(() { expect(state.categories, examplesSortedByTypeMock); - expect(exampleState.sdkCategories, exampleState.sdkCategories); + expect(exampleCache.categoryListsBySdk, exampleCache.categoryListsBySdk); }); state.sortExamplesByType(unsortedExamples, ExampleType.kata); }); @@ -126,12 +130,12 @@ void main() { '- be sensitive for register,' '- affect Example state categories', () { final state = ExampleSelectorState( - playgroundState, + playgroundController, categoriesMock, ); state.addListener(() { expect(state.categories, examplesSortedByNameMock); - expect(exampleState.sdkCategories, exampleState.sdkCategories); + expect(exampleCache.categoryListsBySdk, exampleCache.categoryListsBySdk); }); state.sortExamplesByName(unsortedExamples, 'X1'); }); diff --git a/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart b/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart index 253c7b5d4aac..d6ceb58247ea 100644 --- a/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart +++ b/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart @@ -19,17 +19,20 @@ // in playground/test/pages/playground/states/example_selector_state_test.dart. // Do not manually edit this file. -import 'dart:async' as _i4; +import 'dart:async' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart' - as _i6; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart' - as _i5; -import 'package:playground/pages/playground/states/example_loaders/examples_loader.dart' +import 'package:playground_components/src/controllers/example_loaders/example_loader_factory.dart' as _i2; -import 'package:playground/pages/playground/states/playground_state.dart' +import 'package:playground_components/src/controllers/example_loaders/examples_loader.dart' as _i3; +import 'package:playground_components/src/controllers/playground_controller.dart' + as _i4; +import 'package:playground_components/src/models/example_loading_descriptors/example_loading_descriptor.dart' + as _i8; +import 'package:playground_components/src/models/example_loading_descriptors/examples_loading_descriptor.dart' + as _i6; +import 'package:playground_components/src/models/sdk.dart' as _i7; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -41,29 +44,41 @@ import 'package:playground/pages/playground/states/playground_state.dart' // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types +class _FakeExampleLoaderFactory_0 extends _i1.Fake + implements _i2.ExampleLoaderFactory {} + /// A class which mocks [ExamplesLoader]. /// /// See the documentation for Mockito's code generation for more information. -class MockExamplesLoader extends _i1.Mock implements _i2.ExamplesLoader { +class MockExamplesLoader extends _i1.Mock implements _i3.ExamplesLoader { MockExamplesLoader() { _i1.throwOnMissingStub(this); } @override - void setPlaygroundState(_i3.PlaygroundState? value) => - super.noSuchMethod(Invocation.method(#setPlaygroundState, [value]), + _i2.ExampleLoaderFactory get defaultFactory => (super.noSuchMethod( + Invocation.getter(#defaultFactory), + returnValue: _FakeExampleLoaderFactory_0()) as _i2.ExampleLoaderFactory); + @override + void setPlaygroundController(_i4.PlaygroundController? value) => + super.noSuchMethod(Invocation.method(#setPlaygroundController, [value]), returnValueForMissingStub: null); @override - _i4.Future load(_i5.ExamplesLoadingDescriptor? descriptor) => + _i5.Future load(_i6.ExamplesLoadingDescriptor? descriptor) => (super.noSuchMethod(Invocation.method(#load, [descriptor]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future loadDefaultIfAny(_i7.Sdk? sdk) => + (super.noSuchMethod(Invocation.method(#loadDefaultIfAny, [sdk]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i5.Future); @override - _i4.Future loadOne( - {_i5.ExamplesLoadingDescriptor? group, - _i6.ExampleLoadingDescriptor? one}) => + _i5.Future loadOne( + {_i6.ExamplesLoadingDescriptor? group, + _i8.ExampleLoadingDescriptor? one}) => (super.noSuchMethod( Invocation.method(#loadOne, [], {#group: group, #one: one}), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i4.Future); + returnValueForMissingStub: Future.value()) as _i5.Future); } diff --git a/playground/frontend/test/pages/playground/states/mocks/categories_mock.dart b/playground/frontend/test/pages/playground/states/mocks/categories_mock.dart deleted file mode 100644 index 0079697013c0..000000000000 --- a/playground/frontend/test/pages/playground/states/mocks/categories_mock.dart +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/examples/models/category_model.dart'; -import 'package:playground/modules/examples/models/example_model.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; - -import 'example_mock.dart'; - -final categoriesMock = [ - CategoryModel(name: 'Sorted', examples: [exampleMock1]), - CategoryModel(name: 'Unsorted', examples: [exampleMock2]), -]; - -final List sortedCategories = [ - CategoryModel(name: 'Sorted', examples: [exampleMock1]), -]; - -final List unsortedExamples = [exampleMock1, exampleMock2]; - -final List examplesSortedByTypeMock = [exampleMock2]; - -final List examplesSortedByNameMock = [exampleMock1]; - -final sdkCategoriesFromServerMock = { - SDK.java: categoriesMock, - SDK.python: categoriesMock, - SDK.go: categoriesMock, - SDK.scio: categoriesMock, -}; diff --git a/playground/frontend/test/pages/playground/states/mocks/example_repository_mock.mocks.dart b/playground/frontend/test/pages/playground/states/mocks/example_repository_mock.mocks.dart deleted file mode 100644 index 8215315d7fc8..000000000000 --- a/playground/frontend/test/pages/playground/states/mocks/example_repository_mock.mocks.dart +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Mocks generated by Mockito 5.2.0 from annotations -// in playground/test/pages/playground/states/mocks/example_repository_mock.dart. -// Do not manually edit this file. - -import 'dart:async' as _i5; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:playground/modules/examples/models/category_model.dart' as _i7; -import 'package:playground/modules/examples/models/example_model.dart' as _i2; -import 'package:playground/modules/examples/repositories/example_repository.dart' - as _i4; -import 'package:playground/modules/examples/repositories/models/get_example_request.dart' - as _i9; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart' - as _i8; -import 'package:playground/modules/examples/repositories/models/get_snippet_request.dart' - as _i10; -import 'package:playground/modules/examples/repositories/models/get_snippet_response.dart' - as _i3; -import 'package:playground/modules/examples/repositories/models/save_snippet_request.dart' - as _i11; -import 'package:playground/modules/sdk/models/sdk.dart' as _i6; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -class _FakeExampleModel_0 extends _i1.Fake implements _i2.ExampleModel {} - -class _FakeGetSnippetResponse_1 extends _i1.Fake - implements _i3.GetSnippetResponse {} - -/// A class which mocks [ExampleRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockExampleRepository extends _i1.Mock implements _i4.ExampleRepository { - MockExampleRepository() { - _i1.throwOnMissingStub(this); - } - - @override - _i5.Future>> getListOfExamples( - _i8.GetListOfExamplesRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getListOfExamples, [request]), - returnValue: Future>>.value( - <_i6.SDK, List<_i7.CategoryModel>>{})) - as _i5.Future>>); - @override - _i5.Future<_i2.ExampleModel> getDefaultExample( - _i9.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getDefaultExample, [request]), - returnValue: - Future<_i2.ExampleModel>.value(_FakeExampleModel_0())) - as _i5.Future<_i2.ExampleModel>); - @override - _i5.Future getExampleSource(_i9.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleSource, [request]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future getExampleOutput(_i9.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleOutput, [request]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future getExampleLogs(_i9.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleLogs, [request]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future getExampleGraph(_i9.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExampleGraph, [request]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future<_i2.ExampleModel> getExample( - _i9.GetExampleRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getExample, [request]), - returnValue: - Future<_i2.ExampleModel>.value(_FakeExampleModel_0())) - as _i5.Future<_i2.ExampleModel>); - @override - _i5.Future<_i3.GetSnippetResponse> getSnippet( - _i10.GetSnippetRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#getSnippet, [request]), - returnValue: Future<_i3.GetSnippetResponse>.value( - _FakeGetSnippetResponse_1())) - as _i5.Future<_i3.GetSnippetResponse>); - @override - _i5.Future saveSnippet(_i11.SaveSnippetRequestWrapper? request) => - (super.noSuchMethod(Invocation.method(#saveSnippet, [request]), - returnValue: Future.value('')) as _i5.Future); -} diff --git a/playground/frontend/test/pages/playground/states/mocks/request_mock.dart b/playground/frontend/test/pages/playground/states/mocks/request_mock.dart deleted file mode 100644 index b08a81ebd55d..000000000000 --- a/playground/frontend/test/pages/playground/states/mocks/request_mock.dart +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:playground/modules/editor/repository/code_repository/code_client/output_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_code_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_example_response.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_request.dart'; -import 'package:playground/modules/examples/repositories/models/get_list_of_examples_response.dart'; -import 'package:playground/modules/sdk/models/sdk.dart'; - -import 'categories_mock.dart'; -import 'example_mock.dart'; - -final kGetListOfExamplesRequestMock = - GetListOfExamplesRequestWrapper(sdk: null, category: null); -final kGetListOfExamplesResponseMock = - GetListOfExampleResponse(sdkCategoriesFromServerMock); -final kGetExampleRequestMock = GetExampleRequestWrapper('', SDK.java); -final kGetExampleResponseMock = GetExampleResponse(exampleMock1); -final kGetExampleCodeResponseMock = GetExampleCodeResponse('test source'); -final kOutputResponseMock = OutputResponse('test outputs'); - -final kRequestForExampleInfo = - GetExampleRequestWrapper('SDK_PYTHON/Category/Name', SDK.python); -final kRequestDefaultExampleForJava = GetExampleRequestWrapper('', SDK.java); -final kRequestDefaultExampleForGo = GetExampleRequestWrapper('', SDK.go); -final kRequestDefaultExampleForPython = - GetExampleRequestWrapper('', SDK.python); -final kRequestDefaultExampleForScio = GetExampleRequestWrapper('', SDK.scio); diff --git a/playground/frontend/test/pages/playground/states/playground_state_test.mocks.dart b/playground/frontend/test/pages/playground/states/playground_state_test.mocks.dart deleted file mode 100644 index e8c4b07d0d1e..000000000000 --- a/playground/frontend/test/pages/playground/states/playground_state_test.mocks.dart +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// Mocks generated by Mockito 5.2.0 from annotations -// in playground/test/pages/playground/states/playground_state_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i5; -import 'dart:ui' as _i12; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:playground/modules/examples/models/category_model.dart' as _i10; -import 'package:playground/modules/examples/models/example_loading_descriptors/example_loading_descriptor.dart' - as _i7; -import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor.dart' - as _i6; -import 'package:playground/modules/examples/models/example_model.dart' as _i2; -import 'package:playground/modules/examples/repositories/models/shared_file_model.dart' - as _i11; -import 'package:playground/modules/sdk/models/sdk.dart' as _i9; -import 'package:playground/pages/playground/states/example_loaders/examples_loader.dart' - as _i3; -import 'package:playground/pages/playground/states/examples_state.dart' as _i8; -import 'package:playground/pages/playground/states/playground_state.dart' - as _i4; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -class _FakeExampleModel_0 extends _i1.Fake implements _i2.ExampleModel {} - -/// A class which mocks [ExamplesLoader]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockExamplesLoader extends _i1.Mock implements _i3.ExamplesLoader { - MockExamplesLoader() { - _i1.throwOnMissingStub(this); - } - - @override - void setPlaygroundState(_i4.PlaygroundState? value) => - super.noSuchMethod(Invocation.method(#setPlaygroundState, [value]), - returnValueForMissingStub: null); - @override - _i5.Future load(_i6.ExamplesLoadingDescriptor? descriptor) => - (super.noSuchMethod(Invocation.method(#load, [descriptor]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadOne( - {_i6.ExamplesLoadingDescriptor? group, - _i7.ExampleLoadingDescriptor? one}) => - (super.noSuchMethod( - Invocation.method(#loadOne, [], {#group: group, #one: one}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} - -/// A class which mocks [ExampleState]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockExampleState extends _i1.Mock implements _i8.ExampleState { - MockExampleState() { - _i1.throwOnMissingStub(this); - } - - @override - set sdkCategories(Map<_i9.SDK, List<_i10.CategoryModel>>? _sdkCategories) => - super.noSuchMethod(Invocation.setter(#sdkCategories, _sdkCategories), - returnValueForMissingStub: null); - @override - Map<_i9.SDK, _i2.ExampleModel> get defaultExamplesMap => - (super.noSuchMethod(Invocation.getter(#defaultExamplesMap), - returnValue: <_i9.SDK, _i2.ExampleModel>{}) - as Map<_i9.SDK, _i2.ExampleModel>); - @override - set defaultExamplesMap(Map<_i9.SDK, _i2.ExampleModel>? _defaultExamplesMap) => - super.noSuchMethod( - Invocation.setter(#defaultExamplesMap, _defaultExamplesMap), - returnValueForMissingStub: null); - @override - set defaultExample(_i2.ExampleModel? _defaultExample) => - super.noSuchMethod(Invocation.setter(#defaultExample, _defaultExample), - returnValueForMissingStub: null); - @override - bool get isSelectorOpened => - (super.noSuchMethod(Invocation.getter(#isSelectorOpened), - returnValue: false) as bool); - @override - set isSelectorOpened(bool? _isSelectorOpened) => super.noSuchMethod( - Invocation.setter(#isSelectorOpened, _isSelectorOpened), - returnValueForMissingStub: null); - @override - _i5.Future get allExamplesFuture => - (super.noSuchMethod(Invocation.getter(#allExamplesFuture), - returnValue: Future.value()) as _i5.Future); - @override - bool get hasExampleCatalog => - (super.noSuchMethod(Invocation.getter(#hasExampleCatalog), - returnValue: false) as bool); - @override - bool get hasListeners => - (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) - as bool); - @override - _i5.Future init() => (super.noSuchMethod(Invocation.method(#init, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - void setSdkCategories(Map<_i9.SDK, List<_i10.CategoryModel>>? map) => - super.noSuchMethod(Invocation.method(#setSdkCategories, [map]), - returnValueForMissingStub: null); - @override - List<_i10.CategoryModel> getCategories(_i9.SDK? sdk) => - (super.noSuchMethod(Invocation.method(#getCategories, [sdk]), - returnValue: <_i10.CategoryModel>[]) as List<_i10.CategoryModel>); - @override - _i5.Future getExampleOutput(String? id, _i9.SDK? sdk) => - (super.noSuchMethod(Invocation.method(#getExampleOutput, [id, sdk]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future getExampleSource(String? id, _i9.SDK? sdk) => - (super.noSuchMethod(Invocation.method(#getExampleSource, [id, sdk]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future<_i2.ExampleModel> getExample(String? path, _i9.SDK? sdk) => - (super.noSuchMethod(Invocation.method(#getExample, [path, sdk]), - returnValue: - Future<_i2.ExampleModel>.value(_FakeExampleModel_0())) - as _i5.Future<_i2.ExampleModel>); - @override - _i5.Future getExampleLogs(String? id, _i9.SDK? sdk) => - (super.noSuchMethod(Invocation.method(#getExampleLogs, [id, sdk]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future getExampleGraph(String? id, _i9.SDK? sdk) => - (super.noSuchMethod(Invocation.method(#getExampleGraph, [id, sdk]), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future<_i2.ExampleModel> loadSharedExample(String? id) => - (super.noSuchMethod(Invocation.method(#loadSharedExample, [id]), - returnValue: - Future<_i2.ExampleModel>.value(_FakeExampleModel_0())) - as _i5.Future<_i2.ExampleModel>); - @override - _i5.Future getSnippetId( - {List<_i11.SharedFile>? files, - _i9.SDK? sdk, - String? pipelineOptions}) => - (super.noSuchMethod( - Invocation.method(#getSnippetId, [], - {#files: files, #sdk: sdk, #pipelineOptions: pipelineOptions}), - returnValue: Future.value('')) as _i5.Future); - @override - _i5.Future<_i2.ExampleModel> loadExampleInfo(_i2.ExampleModel? example) => - (super.noSuchMethod(Invocation.method(#loadExampleInfo, [example]), - returnValue: - Future<_i2.ExampleModel>.value(_FakeExampleModel_0())) - as _i5.Future<_i2.ExampleModel>); - @override - void changeSelectorVisibility() => - super.noSuchMethod(Invocation.method(#changeSelectorVisibility, []), - returnValueForMissingStub: null); - @override - _i5.Future loadDefaultExamples() => - (super.noSuchMethod(Invocation.method(#loadDefaultExamples, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadDefaultExamplesIfNot() => - (super.noSuchMethod(Invocation.method(#loadDefaultExamplesIfNot, []), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future<_i2.ExampleModel?> getCatalogExampleByPath(String? path) => - (super.noSuchMethod(Invocation.method(#getCatalogExampleByPath, [path]), - returnValue: Future<_i2.ExampleModel?>.value()) - as _i5.Future<_i2.ExampleModel?>); - @override - void addListener(_i12.VoidCallback? listener) => - super.noSuchMethod(Invocation.method(#addListener, [listener]), - returnValueForMissingStub: null); - @override - void removeListener(_i12.VoidCallback? listener) => - super.noSuchMethod(Invocation.method(#removeListener, [listener]), - returnValueForMissingStub: null); - @override - void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), - returnValueForMissingStub: null); - @override - void notifyListeners() => - super.noSuchMethod(Invocation.method(#notifyListeners, []), - returnValueForMissingStub: null); -} From d735c71192427f333d98085c4d7f59ee2bae1be0 Mon Sep 17 00:00:00 2001 From: Alexey Inkin Date: Thu, 15 Sep 2022 17:39:38 +0400 Subject: [PATCH 11/17] Minor fixes (#22600) --- .../components/profile/profile_content.dart | 3 ++ .../lib/components/split_view/pan.dart | 35 ------------------- .../lib/pages/tour/playground_demo.dart | 3 +- .../frontend/lib/pages/tour/screen.dart | 6 ++-- playground/frontend/README.md | 12 +++++++ .../shortcuts/components/shortcuts_modal.dart | 1 - .../components/playground_page_body.dart | 1 - .../components/playground_page_providers.dart | 3 +- .../lib/src/cache/example_cache.dart | 1 + .../lib/src/models/example.dart | 1 + .../lib/src/models/example_base.dart | 4 +++ .../lib/src/widgets/drag_handle.dart | 2 +- .../lib/src/widgets/split_view.dart | 6 ++-- .../playground_components/pubspec.yaml | 2 +- 14 files changed, 33 insertions(+), 47 deletions(-) delete mode 100644 learning/tour-of-beam/frontend/lib/components/split_view/pan.dart diff --git a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart index a036a37ad2a6..bb00f20e2aab 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart @@ -97,6 +97,7 @@ class _Buttons extends StatelessWidget { iconPath: Assets.png.profileWebsite.path, label: 'ui.toWebsite'.tr(), ), + const BeamDivider(), _IconLabel( onTap: () {}, iconPath: Assets.svg.profileAbout, @@ -123,6 +124,8 @@ class _IconLabel extends StatelessWidget { final String iconPath; final String label; final void Function()? onTap; + + // TODO: Auto-determine. final bool isSvg; const _IconLabel({ diff --git a/learning/tour-of-beam/frontend/lib/components/split_view/pan.dart b/learning/tour-of-beam/frontend/lib/components/split_view/pan.dart deleted file mode 100644 index 4ef912b9b5c7..000000000000 --- a/learning/tour-of-beam/frontend/lib/components/split_view/pan.dart +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import 'package:flutter/material.dart'; - -class Pan { - final Widget child; - - /// Fraction of a split view, from 0 to 1. - /// For example, 0.5 is half of a 2-column split view. - /// Null means no limit. - final double? minWeight; - final double? maxWeight; - - const Pan({ - required this.child, - this.minWeight, - this.maxWeight, - }); -} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart index ed9dace49dc2..9f9003f6aa1b 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart @@ -20,6 +20,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:playground_components/playground_components.dart'; +// This is for demo only. Need a thought-though import in production. + const String kApiClientURL = 'https://backend-router-beta-dot-apache-beam-testing.appspot.com'; const String kApiJavaClientURL = @@ -77,7 +79,6 @@ class _PlaygroundDemoWidgetState extends State { descriptors: [ CatalogDefaultExampleLoadingDescriptor(sdk: Sdk.java), ], - //initialSdk: Sdk.java, ), ); } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart index 513a6879f729..f47c7b462201 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -83,7 +83,7 @@ class _NarrowTour extends StatelessWidget { top: BorderSide(color: Theme.of(context).dividerColor), ), ), - child: const _Playground(), + child: const _NarrowScreenPlayground(), ), ], ), @@ -347,8 +347,8 @@ class _ContentFooter extends StatelessWidget { } } -class _Playground extends StatelessWidget { - const _Playground(); +class _NarrowScreenPlayground extends StatelessWidget { + const _NarrowScreenPlayground(); @override Widget build(BuildContext context) { diff --git a/playground/frontend/README.md b/playground/frontend/README.md index 92eb33628ab2..eb015b48b9ea 100644 --- a/playground/frontend/README.md +++ b/playground/frontend/README.md @@ -25,6 +25,18 @@ Beam Playground is an interactive environment to try out Beam transforms and exa ## Getting Started +Running, debugging, and testing all require this first step that fetches +dependencies and generates code: + +```bash +cd playground_components +flutter pub get +flutter pub run build_runner build +cd .. +flutter pub get +flutter pub run build_runner build +``` + ### Run See [playground/README.md](../README.md) for details on requirements and setup. diff --git a/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart b/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart index 2c5ee6a775a0..1f334327f6b0 100644 --- a/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart +++ b/playground/frontend/lib/modules/shortcuts/components/shortcuts_modal.dart @@ -68,7 +68,6 @@ class ShortcutsModal extends StatelessWidget { flex: 3, child: Text( shortcut.actionIntent.slug.tr(), - //localize(context, shortcut.name), style: const TextStyle(fontWeight: kBoldWeight), ), ), diff --git a/playground/frontend/lib/pages/playground/components/playground_page_body.dart b/playground/frontend/lib/pages/playground/components/playground_page_body.dart index 419b44dca5df..63d918ae292e 100644 --- a/playground/frontend/lib/pages/playground/components/playground_page_body.dart +++ b/playground/frontend/lib/pages/playground/components/playground_page_body.dart @@ -33,7 +33,6 @@ class PlaygroundPageBody extends StatelessWidget { return Consumer2( builder: (context, outputState, playgroundState, child) { final output = OutputWidget( - // isEmbedded: false, graphDirection: outputState.placement.graphDirection, playgroundController: playgroundState, trailing: const OutputPlacements(), diff --git a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart index 69c2713e30f1..a5e89130b051 100644 --- a/playground/frontend/lib/pages/playground/components/playground_page_providers.dart +++ b/playground/frontend/lib/pages/playground/components/playground_page_providers.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:playground/config.g.dart'; +import 'package:playground/constants/params.dart'; import 'package:playground/modules/analytics/analytics_service.dart'; import 'package:playground/modules/analytics/google_analytics_service.dart'; import 'package:playground/modules/examples/models/example_loading_descriptors/examples_loading_descriptor_factory.dart'; @@ -64,7 +65,7 @@ class PlaygroundPageProviders extends StatelessWidget { final exampleCache = ExampleCache( exampleRepository: exampleRepository, - hasCatalog: true, + hasCatalog: !isEmbedded(), )..init(); final controller = PlaygroundController( diff --git a/playground/frontend/playground_components/lib/src/cache/example_cache.dart b/playground/frontend/playground_components/lib/src/cache/example_cache.dart index ea755ba5949a..04d1425ec80c 100644 --- a/playground/frontend/playground_components/lib/src/cache/example_cache.dart +++ b/playground/frontend/playground_components/lib/src/cache/example_cache.dart @@ -33,6 +33,7 @@ import '../repositories/models/shared_file.dart'; import '../repositories/models/get_snippet_request.dart'; import '../repositories/models/save_snippet_request.dart'; +/// A runtime cache for examples fetched from a repository. class ExampleCache extends ChangeNotifier { /// If set, then categories and default examples are enabled. /// Otherwise examples can only be queried by paths. diff --git a/playground/frontend/playground_components/lib/src/models/example.dart b/playground/frontend/playground_components/lib/src/models/example.dart index e35856a6264f..a598b6b6446f 100644 --- a/playground/frontend/playground_components/lib/src/models/example.dart +++ b/playground/frontend/playground_components/lib/src/models/example.dart @@ -18,6 +18,7 @@ import 'example_base.dart'; +/// A [ExampleBase] that also has all large fields fetched. class Example extends ExampleBase { final String source; final String? outputs; diff --git a/playground/frontend/playground_components/lib/src/models/example_base.dart b/playground/frontend/playground_components/lib/src/models/example_base.dart index 2a340e56ba2b..2c493edf79f6 100644 --- a/playground/frontend/playground_components/lib/src/models/example_base.dart +++ b/playground/frontend/playground_components/lib/src/models/example_base.dart @@ -18,6 +18,7 @@ import 'package:equatable/equatable.dart'; +import '../repositories/example_repository.dart'; import 'sdk.dart'; enum ExampleType { @@ -42,6 +43,9 @@ extension ExampleTypeToString on ExampleType { } } +/// An example's basic info that does not contain source code +/// and other large fields. +/// These objects are fetched as lists from [ExampleRepository]. class ExampleBase with Comparable, EquatableMixin { final Sdk sdk; final ExampleType type; diff --git a/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart b/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart index 7dfed2c589ae..505b8afb0c9a 100644 --- a/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart +++ b/playground/frontend/playground_components/lib/src/widgets/drag_handle.dart @@ -32,7 +32,7 @@ class DragHandle extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: Use a single file and just rotate it if needed. - // Currently it gets blurred in HTML renderer. Find a fix. + // Currently a rotated widget gets blurred in HTML renderer. Find a fix. return SvgPicture.asset( direction == Axis.horizontal ? Assets.svg.dragHorizontal diff --git a/playground/frontend/playground_components/lib/src/widgets/split_view.dart b/playground/frontend/playground_components/lib/src/widgets/split_view.dart index 937685618dd3..904d03788d61 100644 --- a/playground/frontend/playground_components/lib/src/widgets/split_view.dart +++ b/playground/frontend/playground_components/lib/src/widgets/split_view.dart @@ -29,14 +29,14 @@ class SplitView extends StatefulWidget { final Widget first; final Widget second; final Axis direction; - final double ratio; + final double initialRatio; const SplitView({ super.key, required this.first, required this.second, required this.direction, - this.ratio = defaultRatio, + this.initialRatio = defaultRatio, }); @override @@ -59,7 +59,7 @@ class _SplitViewState extends State { @override void initState() { super.initState(); - _ratio = widget.ratio; + _ratio = widget.initialRatio; } @override diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index 1321bee934cc..194c788fe272 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -21,7 +21,7 @@ version: 0.0.1 environment: sdk: '>=2.17.6 <3.0.0' - flutter: '>=1.17.0' + flutter: '>=3.3.1' dependencies: aligned_dialog: ^0.0.6 From 1baa787081c125333e32f7afc2388d3338a9d6d4 Mon Sep 17 00:00:00 2001 From: Alexey Inkin Date: Fri, 16 Sep 2022 12:30:01 +0400 Subject: [PATCH 12/17] Address review issues (#22600) --- .../frontend/assets/translations/en.yaml | 8 ++++---- .../frontend/lib/components/login/login_content.dart | 12 ++++++------ .../frontend/lib/pages/tour/playground_demo.dart | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index 919c2a39ae72..a5a9f16eba45 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -21,23 +21,23 @@ ui: continueGitHub: Continue with GitHub continueGoogle: Continue with Google copyright: © The Apache Software Foundation + deleteAccount: Delete my account privacyPolicy: Privacy Policy reportIssue: Report Issue in GitHub signIn: Sign in signOut: Sign out toWebsite: To Apache Beam website - deleteAccount: Delete my account pages: welcome: - title: Welcome to the Tour of Beam! ifSaveProgress: Your journey is broken down into learning modules. If you would like to save your progress and track completed modules, please - signIn: ' sign in.' selectLanguage: 'Please select the default language (you may change the language at any time):' + signIn: ' sign in.' startLearning: Start learning + title: Welcome to the Tour of Beam! tour: - summaryTitle: Table of Contents completeUnit: Complete Unit + summaryTitle: Table of Contents dialogs: signInIf: If you would like to save your progress and track completed modules diff --git a/learning/tour-of-beam/frontend/lib/components/login/login_content.dart b/learning/tour-of-beam/frontend/lib/components/login/login_content.dart index f0046184d0e0..d4d0d8873cce 100644 --- a/learning/tour-of-beam/frontend/lib/components/login/login_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/login/login_content.dart @@ -98,25 +98,25 @@ class _BrandedLoginButtons extends StatelessWidget { const minimumSize = MaterialStatePropertyAll(Size(double.infinity, 0)); final darkButtonStyle = ButtonStyle( + backgroundColor: const MaterialStatePropertyAll(BeamColors.darkGrey), minimumSize: minimumSize, padding: padding, textStyle: textStyle, - backgroundColor: const MaterialStatePropertyAll(BeamColors.darkGrey), ); final githubLightButtonStyle = ButtonStyle( + backgroundColor: const MaterialStatePropertyAll(BeamColors.darkBlue), minimumSize: minimumSize, padding: padding, textStyle: textStyle, - backgroundColor: const MaterialStatePropertyAll(BeamColors.darkBlue), ); final googleLightButtonStyle = ButtonStyle( - minimumSize: minimumSize, - padding: padding, + backgroundColor: const MaterialStatePropertyAll(BeamColors.white), elevation: const MaterialStatePropertyAll(BeamSizes.size4), foregroundColor: const MaterialStatePropertyAll(BeamColors.black), - textStyle: textStyle, + minimumSize: minimumSize, overlayColor: MaterialStatePropertyAll(Theme.of(context).hoverColor), - backgroundColor: const MaterialStatePropertyAll(BeamColors.white), + padding: padding, + textStyle: textStyle, ); return Column( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart index 9f9003f6aa1b..384d46b0fc29 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/playground_demo.dart @@ -20,7 +20,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:playground_components/playground_components.dart'; -// This is for demo only. Need a thought-though import in production. +// This is for demo only. Need a thought-through import in production. const String kApiClientURL = 'https://backend-router-beta-dot-apache-beam-testing.appspot.com'; @@ -34,7 +34,7 @@ const String kApiScioClientURL = 'https://backend-scio-beta-dot-apache-beam-testing.appspot.com'; class PlaygroundDemoWidget extends StatefulWidget { - const PlaygroundDemoWidget({Key? key}) : super(key: key); + const PlaygroundDemoWidget({super.key}); @override State createState() => _PlaygroundDemoWidgetState(); @@ -75,7 +75,7 @@ class _PlaygroundDemoWidgetState extends State { ); playgroundController.examplesLoader.load( - ExamplesLoadingDescriptor( + const ExamplesLoadingDescriptor( descriptors: [ CatalogDefaultExampleLoadingDescriptor(sdk: Sdk.java), ], From f6dc5bcd98d3c81f2a31c23d14859a73758bdb54 Mon Sep 17 00:00:00 2001 From: Alexey Inkin Date: Fri, 16 Sep 2022 12:40:19 +0400 Subject: [PATCH 13/17] Upgrade Flutter to v3.3.2 (#22600) --- .../workflows/build_playground_frontend.yml | 6 +++--- learning/tour-of-beam/frontend/pubspec.lock | 4 ++-- learning/tour-of-beam/frontend/pubspec.yaml | 4 ++-- playground/frontend/Dockerfile | 19 ++++++++++++++----- .../playground_components/pubspec.yaml | 6 +++--- playground/frontend/pubspec.lock | 6 +++--- playground/frontend/pubspec.yaml | 4 ++-- 7 files changed, 29 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build_playground_frontend.yml b/.github/workflows/build_playground_frontend.yml index 8b4d0953e6b3..ed64a489f051 100644 --- a/.github/workflows/build_playground_frontend.yml +++ b/.github/workflows/build_playground_frontend.yml @@ -32,7 +32,7 @@ jobs: GO_VERSION: 1.18.0 BEAM_VERSION: 2.40.0 TERRAFORM_VERSION: 1.0.9 - FLUTTER_VERSION: 3.0.1-stable + FLUTTER_VERSION: 3.3.2 STAND_SUFFIX: '' GOOGLE_DOMAIN: '-dot-apache-beam-testing.appspot.com' steps: @@ -44,8 +44,8 @@ jobs: java-version: '8' - name: install flutter run: | - wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION.tar.xz &&\ - tar -xf flutter_linux_$FLUTTER_VERSION.tar.xz &&\ + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\ + tar -xf flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\ mv flutter /opt/ &&\ ln -s /opt/flutter/bin/flutter /usr/local/bin/flutter &&\ ln -s /opt/flutter/bin/dart /usr/local/bin/dart &&\ diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index 85f5d587816b..c59bb0affd66 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -912,5 +912,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.17.6 <3.0.0" - flutter: ">=3.3.1" + dart: ">=2.18.1 <3.0.0" + flutter: ">=3.3.2" diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index 547d378d2f93..fb9ee46307d6 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -23,8 +23,8 @@ publish_to: 'none' version: 0.1.0 environment: - sdk: ">=2.17.6 <3.0.0" - flutter: ">=3.3.1" + sdk: ">=2.18.1 <3.0.0" + flutter: ">=3.3.2" dependencies: code_text_field: diff --git a/playground/frontend/Dockerfile b/playground/frontend/Dockerfile index 426518693075..2b342adfca1c 100644 --- a/playground/frontend/Dockerfile +++ b/playground/frontend/Dockerfile @@ -17,11 +17,11 @@ ############################################################################### FROM debian:11 as build -ARG FLUTTER_VERSION=3.0.1-stable +ARG FLUTTER_VERSION=3.3.2 RUN apt-get update && apt-get install -y wget xz-utils git -RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION.tar.xz &&\ - tar -xf flutter_linux_$FLUTTER_VERSION.tar.xz &&\ +RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\ + tar -xf flutter_linux_$FLUTTER_VERSION-stable.tar.xz &&\ mv flutter /opt/ &&\ ln -s /opt/flutter/bin/flutter /usr/bin/flutter &&\ ln -s /opt/flutter/bin/dart /usr/bin/dart &&\ @@ -31,9 +31,18 @@ RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/li COPY . /app WORKDIR /app -RUN cd /app && flutter build web -v + +RUN cd /app/playground_components &&\ + flutter pub get -v &&\ + flutter pub run build_runner build -v + +RUN cd /app &&\ + flutter pub get -v &&\ + flutter pub run build_runner build -v &&\ + flutter build web -v + FROM nginx:1.21.3 COPY --from=build /app/nginx_default.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/build/web/ /usr/share/nginx/html -RUN cp /usr/share/nginx/html/assets/assets/* /usr/share/nginx/html/assets/ +RUN cp -r /usr/share/nginx/html/assets/assets/* /usr/share/nginx/html/assets/ RUN gzip -kr /usr/share/nginx/html/assets/* diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index 194c788fe272..e3597cd40f2a 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -20,8 +20,8 @@ description: Reusable Playground components version: 0.0.1 environment: - sdk: '>=2.17.6 <3.0.0' - flutter: '>=3.3.1' + sdk: '>=2.18.1 <3.0.0' + flutter: '>=3.3.2' dependencies: aligned_dialog: ^0.0.6 @@ -40,8 +40,8 @@ dependencies: grpc: ^3.0.2 highlight: ^0.7.0 meta: ^1.7.0 - provider: ^6.0.3 protobuf: ^2.1.0 + provider: ^6.0.3 shared_preferences: ^2.0.15 dev_dependencies: diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock index 757308ffe2c9..7b3186dbd6bd 100644 --- a/playground/frontend/pubspec.lock +++ b/playground/frontend/pubspec.lock @@ -325,7 +325,7 @@ packages: source: hosted version: "2.0.2" google_fonts: - dependency: transitive + dependency: "direct main" description: name: google_fonts url: "https://pub.dartlang.org" @@ -925,5 +925,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.17.6 <3.0.0" - flutter: ">=3.3.1" + dart: ">=2.18.1 <3.0.0" + flutter: ">=3.3.2" diff --git a/playground/frontend/pubspec.yaml b/playground/frontend/pubspec.yaml index b989f7fe4443..adba7a981e24 100644 --- a/playground/frontend/pubspec.yaml +++ b/playground/frontend/pubspec.yaml @@ -21,8 +21,8 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" - flutter: ">=3.3.1" + sdk: ">=2.18.1 <3.0.0" + flutter: ">=3.3.2" dependencies: akvelon_flutter_issue_106664_workaround: ^0.1.2 From 09c32b63c44bc89502d822469d9f8a57037bb4d2 Mon Sep 17 00:00:00 2001 From: Alexey Inkin Date: Fri, 16 Sep 2022 18:12:19 +0400 Subject: [PATCH 14/17] Add precommit Gradle task for playground_components, add code generation to frontend Gradle task, remove generated mocks, fix linter issues (#22600) --- .gitignore | 1 + playground/frontend/build.gradle | 63 ++++++++----- .../playground_components/build.gradle.kts | 90 +++++++++++++++++++ .../controllers/playground_controller.dart | 2 +- .../playground_components/pubspec.yaml | 1 + .../example_selector_state_test.mocks.dart | 84 ----------------- settings.gradle.kts | 1 + 7 files changed, 137 insertions(+), 105 deletions(-) create mode 100644 playground/frontend/playground_components/build.gradle.kts delete mode 100644 playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart diff --git a/.gitignore b/.gitignore index 443ced0aaeec..ec600f50f830 100644 --- a/.gitignore +++ b/.gitignore @@ -125,6 +125,7 @@ website/www/yarn-error.log **/.flutter-plugins **/.flutter-plugins-dependencies **/generated_plugin_registrant.dart +**/*.mocks.dart # Ignore Beam Playground Terraform **/.terraform diff --git a/playground/frontend/build.gradle b/playground/frontend/build.gradle index fe8aa85bdf10..5843927fc777 100644 --- a/playground/frontend/build.gradle +++ b/playground/frontend/build.gradle @@ -40,26 +40,18 @@ dependencies { dockerDependency project(path: playgroundJobServerProject, configuration: "shadow") } -task removeBuild { - group = "verification" - description = "remove build artifacts" - doLast { - exec { - executable("rm") - args("-r", "build") - } - } -} - task analyze { - dependsOn("pubGet") - dependsOn("removeBuild") + dependsOn("playground_components:generate") + dependsOn("generate") + group = "verification" description = "Analyze dart code" + doLast { exec { + // Exact paths instead of '.' so it does not go into playground_components executable("dart") - args("analyze", ".") + args("analyze", "lib", "test") } } } @@ -80,8 +72,9 @@ task format { description = "Idiomatically format Dart source code" doLast { exec { + // Exact paths instead of '.' so it does not go into playground_components executable("dart") - args("format", ".") + args("format", "lib", "test") } } } @@ -98,9 +91,12 @@ task run { } task test { - dependsOn("pubGet") + dependsOn("playground_components:generate") + dependsOn("generate") + group = "verification" description = "flutter test" + doLast { exec { executable("flutter") @@ -110,10 +106,37 @@ task test { } task precommit { - dependsOn(":playground:frontend:removeBuild") - dependsOn(":playground:frontend:pubGet") - dependsOn(":playground:frontend:analyze") - dependsOn(":playground:frontend:test") + dependsOn("playground_components:precommit") + + dependsOn("analyze") + dependsOn("test") +} + +task generate { + dependsOn("flutterClean") + dependsOn("pubGet") + + group = "build" + description = "Generate code" + + doLast { + exec { + executable("flutter") + args("pub", "run", "build_runner", "build", "--delete-conflicting-outputs") + } + } +} + +task flutterClean { + group = "build" + description = "Remove build artifacts" + + doLast { + exec { + executable("flutter") + args("clean") + } + } } task copyDockerfileDependencies(type: Copy) { diff --git a/playground/frontend/playground_components/build.gradle.kts b/playground/frontend/playground_components/build.gradle.kts new file mode 100644 index 000000000000..bacc8daddecf --- /dev/null +++ b/playground/frontend/playground_components/build.gradle.kts @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * License); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an AS IS BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +description = "Apache Beam :: Playground :: playground_components Flutter Package" + +tasks.register("precommit") { + dependsOn("analyze") + dependsOn("test") +} + +tasks.register("analyze") { + dependsOn("generate") + + group = "verification" + description = "Run dart analyzer" + + doLast { + exec { + executable("dart") + args("analyze", ".") + } + } +} + +tasks.register("test") { + dependsOn("generate") + + group = "verification" + description = "Run tests" + + doLast { + exec { + executable("flutter") + args("test") + } + } +} + +tasks.register("clean") { + group = "build" + description = "Remove build artifacts" + doLast { + exec { + executable("flutter") + args("clean") + } + } +} + +tasks.register("pubGet") { + group = "build" + description = "Install dependencies" + + doLast { + exec { + executable("flutter") + args("pub", "get") + } + } +} + +tasks.register("generate") { + dependsOn("clean") + dependsOn("pubGet") + + group = "build" + description = "Generate code" + + doLast { + exec { + executable("flutter") + args("pub", "run", "build_runner", "build", "--delete-conflicting-outputs") + } + } +} diff --git a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart index d68a30eede37..794449110772 100644 --- a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart +++ b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart @@ -240,7 +240,7 @@ class PlaygroundController with ChangeNotifier { ); _runSubscription = _codeRepository?.runCode(request).listen((event) { _result = event; - filterOutput(selectedOutputFilterType ?? OutputType.all); + filterOutput(selectedOutputFilterType); if (event.isFinished && onFinish != null) { onFinish(); diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index e3597cd40f2a..801fcf29abbd 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -18,6 +18,7 @@ name: playground_components description: Reusable Playground components version: 0.0.1 +publish_to: none environment: sdk: '>=2.18.1 <3.0.0' diff --git a/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart b/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart deleted file mode 100644 index d6ceb58247ea..000000000000 --- a/playground/frontend/test/pages/playground/states/example_selector_state_test.mocks.dart +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// Mocks generated by Mockito 5.0.16 from annotations -// in playground/test/pages/playground/states/example_selector_state_test.dart. -// Do not manually edit this file. - -import 'dart:async' as _i5; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:playground_components/src/controllers/example_loaders/example_loader_factory.dart' - as _i2; -import 'package:playground_components/src/controllers/example_loaders/examples_loader.dart' - as _i3; -import 'package:playground_components/src/controllers/playground_controller.dart' - as _i4; -import 'package:playground_components/src/models/example_loading_descriptors/example_loading_descriptor.dart' - as _i8; -import 'package:playground_components/src/models/example_loading_descriptors/examples_loading_descriptor.dart' - as _i6; -import 'package:playground_components/src/models/sdk.dart' as _i7; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -class _FakeExampleLoaderFactory_0 extends _i1.Fake - implements _i2.ExampleLoaderFactory {} - -/// A class which mocks [ExamplesLoader]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockExamplesLoader extends _i1.Mock implements _i3.ExamplesLoader { - MockExamplesLoader() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.ExampleLoaderFactory get defaultFactory => (super.noSuchMethod( - Invocation.getter(#defaultFactory), - returnValue: _FakeExampleLoaderFactory_0()) as _i2.ExampleLoaderFactory); - @override - void setPlaygroundController(_i4.PlaygroundController? value) => - super.noSuchMethod(Invocation.method(#setPlaygroundController, [value]), - returnValueForMissingStub: null); - @override - _i5.Future load(_i6.ExamplesLoadingDescriptor? descriptor) => - (super.noSuchMethod(Invocation.method(#load, [descriptor]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadDefaultIfAny(_i7.Sdk? sdk) => - (super.noSuchMethod(Invocation.method(#loadDefaultIfAny, [sdk]), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); - @override - _i5.Future loadOne( - {_i6.ExamplesLoadingDescriptor? group, - _i8.ExampleLoadingDescriptor? one}) => - (super.noSuchMethod( - Invocation.method(#loadOne, [], {#group: group, #one: one}), - returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 71fe8bd8107f..d17d4caead0a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -60,6 +60,7 @@ include(":model:pipeline") include(":playground") include(":playground:backend") include(":playground:frontend") +include(":playground:frontend:playground_components") include(":playground:backend:containers") include(":playground:backend:containers:java") include(":playground:backend:containers:go") From 14858f883b46d924352d3e62e3cf0f3bec884086 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Fri, 23 Sep 2022 11:17:10 +0600 Subject: [PATCH 15/17] startTour button (#22600) --- .../tour-of-beam/frontend/assets/translations/en.yaml | 2 +- .../frontend/lib/pages/welcome/screen.dart | 2 +- .../playground_components/lib/src/theme/theme.dart | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index a5a9f16eba45..8dff25f4f91e 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -33,7 +33,7 @@ pages: ifSaveProgress: Your journey is broken down into learning modules. If you would like to save your progress and track completed modules, please selectLanguage: 'Please select the default language (you may change the language at any time):' signIn: ' sign in.' - startLearning: Start learning + startTour: Start your tour title: Welcome to the Tour of Beam! tour: completeUnit: Complete Unit diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart index 8c295687cedf..ae799b7e77c4 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart @@ -224,7 +224,7 @@ class _Buttons extends StatelessWidget { onPressed: () { // TODO(nausharipov): redirect }, - child: const Text('pages.welcome.startLearning').tr(), + child: const Text('pages.welcome.startTour').tr(), ), ], ); diff --git a/playground/frontend/playground_components/lib/src/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart index 9f6625316b94..98d27fb9e279 100644 --- a/playground/frontend/playground_components/lib/src/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -311,7 +311,10 @@ OutlinedButtonThemeData _getOutlineButtonTheme( style: OutlinedButton.styleFrom( foregroundColor: textColor, side: BorderSide(color: outlineColor, width: 3), - padding: _buttonPadding, + padding: const EdgeInsets.symmetric( + vertical: BeamSizes.size20, + horizontal: BeamSizes.size40, + ), shape: _getButtonBorder(BeamBorderRadius.small), ), ); @@ -320,6 +323,7 @@ OutlinedButtonThemeData _getOutlineButtonTheme( ElevatedButtonThemeData _getElevatedButtonTheme(Color color) { return ElevatedButtonThemeData( style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(BeamSizes.size20), foregroundColor: BeamColors.white, backgroundColor: color, ), @@ -351,11 +355,6 @@ AppBarTheme _getAppBarTheme(Color backgroundColor) { ); } -const EdgeInsets _buttonPadding = EdgeInsets.symmetric( - vertical: BeamSizes.size20, - horizontal: BeamSizes.size40, -); - RoundedRectangleBorder _getButtonBorder(double radius) { return RoundedRectangleBorder( borderRadius: BorderRadius.all( From 63919e464a19b1bbdf9c2e1540581fb24b4ab2e5 Mon Sep 17 00:00:00 2001 From: "darkhan.nausharipov" Date: Fri, 23 Sep 2022 11:18:43 +0600 Subject: [PATCH 16/17] lint fixes (#22600) --- .../frontend/lib/components/profile/profile_content.dart | 2 +- learning/tour-of-beam/frontend/lib/pages/tour/screen.dart | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart index bb00f20e2aab..65f039dad326 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/profile_content.dart @@ -125,7 +125,7 @@ class _IconLabel extends StatelessWidget { final String label; final void Function()? onTap; - // TODO: Auto-determine. + // TODO(nausharipov): Auto-determine. final bool isSvg; const _IconLabel({ diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart index f47c7b462201..7d708ff25089 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -314,7 +314,8 @@ class _ContentFooter extends StatelessWidget { border: Border( top: BorderSide(color: themeData.dividerColor), ), - color: themeData.extension()?.secondaryBackgroundColor, + color: + themeData.extension()?.secondaryBackgroundColor, ), width: double.infinity, padding: const EdgeInsets.all(BeamSizes.size20), From e00889a0095547f3b4b86c9734955471f5948e56 Mon Sep 17 00:00:00 2001 From: Alexey Inkin Date: Fri, 23 Sep 2022 17:54:07 +0400 Subject: [PATCH 17/17] Fix highlighting for Python and SCIO (#22600) --- .../frontend/playground_components/lib/src/models/sdk.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playground/frontend/playground_components/lib/src/models/sdk.dart b/playground/frontend/playground_components/lib/src/models/sdk.dart index ae29dc1a06f3..5d1cc49f7928 100644 --- a/playground/frontend/playground_components/lib/src/models/sdk.dart +++ b/playground/frontend/playground_components/lib/src/models/sdk.dart @@ -85,11 +85,11 @@ class Sdk with EquatableMixin { return known.firstWhereOrNull((e) => e.id == value); } - static final _idToHighlightMode = { + static final _idToHighlightMode = { Sdk.java.id: mode_java.java, Sdk.go.id: mode_go.go, - Sdk.python: mode_python.python, - Sdk.scio: mode_scala.scala, + Sdk.python.id: mode_python.python, + Sdk.scio.id: mode_scala.scala, }; Mode? get highlightMode => _idToHighlightMode[id];