diff --git a/learning/tour-of-beam/frontend/assets/translations/en.yaml b/learning/tour-of-beam/frontend/assets/translations/en.yaml index 0bb0ca900d4d..380bb0ee4a5a 100644 --- a/learning/tour-of-beam/frontend/assets/translations/en.yaml +++ b/learning/tour-of-beam/frontend/assets/translations/en.yaml @@ -17,14 +17,18 @@ ui: about: About Tour of Beam - builtWith: Built with Apache Beam + builtWith: Built with Apache Beam {beamSdkVersion} + cancel: Cancel continueGitHub: Continue with GitHub continueGoogle: Continue with Google - hint: Hint - deleteAccount: Delete my account + copyright: © The Apache Software Foundation + deleteMyAccount: Delete my account + deleteTobAccount: Delete my Tour of Beam account + feedbackTitle: Enjoying Tour of Beam? + privacyPolicy: Privacy Policy + reportIssue: Report Issue in GitHub signIn: Sign in signOut: Sign out - solution: Solution toWebsite: To Apache Beam website pages: @@ -35,11 +39,22 @@ pages: startTour: Start your tour title: Welcome to the Tour of Beam! tour: + assignment: Assignment completeUnit: Complete Unit + content: Content + hint: Hint + showSolution: Show the solution + solution: Solution + solveYourself: Before revealing the solution, try solving the challenge on your own. Remember, the more you practice, the better you will become. Give it a shot and see how far you can get. + example: Example + myCode: My code + playground: Playground + saving: Saving... summaryTitle: Table of Contents dialogs: signInIf: If you would like to save your progress and track completed modules + deleteAccountWarning: Are you sure you want to delete your Tour of Beam account? This will permanently erase your learning progress. complexity: basic: Basic level diff --git a/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart b/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart index 924b1e38993b..9cfcc7360cb1 100644 --- a/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart +++ b/learning/tour-of-beam/frontend/lib/assets/assets.gen.dart @@ -5,7 +5,7 @@ // coverage:ignore-file // ignore_for_file: type=lint -// ignore_for_file: directives_ordering,unnecessary_import +// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use import 'package:flutter/widgets.dart'; @@ -23,6 +23,9 @@ class $AssetsPngGen { /// File path: assets/png/profile-website.png AssetGenImage get profileWebsite => const AssetGenImage('assets/png/profile-website.png'); + + /// List of all assets + List get values => [laptopDark, laptopLight, profileWebsite]; } class $AssetsSvgGen { @@ -57,6 +60,20 @@ class $AssetsSvgGen { /// File path: assets/svg/welcome-progress-0.svg String get welcomeProgress0 => 'assets/svg/welcome-progress-0.svg'; + + /// List of all assets + List get values => [ + githubLogo, + googleLogo, + hint, + profileAbout, + profileDelete, + profileLogout, + solution, + unitProgress0, + unitProgress100, + welcomeProgress0 + ]; } class $AssetsTranslationsGen { @@ -64,6 +81,9 @@ class $AssetsTranslationsGen { /// File path: assets/translations/en.yaml String get en => 'assets/translations/en.yaml'; + + /// List of all assets + List get values => [en]; } class Assets { @@ -132,6 +152,8 @@ class AssetGenImage { ); } + ImageProvider provider() => AssetImage(_assetName); + String get path => _assetName; String get keyName => _assetName; diff --git a/learning/tour-of-beam/frontend/lib/auth/notifier.dart b/learning/tour-of-beam/frontend/lib/auth/notifier.dart index 2eb7be819f39..2d59208277e3 100644 --- a/learning/tour-of-beam/frontend/lib/auth/notifier.dart +++ b/learning/tour-of-beam/frontend/lib/auth/notifier.dart @@ -21,6 +21,10 @@ import 'dart:async'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../cache/unit_progress.dart'; class AuthNotifier extends ChangeNotifier { AuthNotifier() { @@ -36,10 +40,25 @@ class AuthNotifier extends ChangeNotifier { } Future logIn(AuthProvider authProvider) async { - await FirebaseAuth.instance.signInWithPopup(authProvider); + try { + await FirebaseAuth.instance.signInWithPopup(authProvider); + } on Exception catch (e) { + PlaygroundComponents.toastNotifier.addException(e); + } } Future logOut() async { await FirebaseAuth.instance.signOut(); } + + Future deleteAccount() async { + try { + // If there are more things to do before account deletion, + // add final _accountDeletionListeners = []. + await GetIt.instance.get().deleteUserProgress(); + await FirebaseAuth.instance.currentUser?.delete(); + } on Exception catch (e) { + PlaygroundComponents.toastNotifier.addException(e); + } + } } diff --git a/learning/tour-of-beam/frontend/lib/cache/content_tree.dart b/learning/tour-of-beam/frontend/lib/cache/content_tree.dart index d67d0b891e8f..fd683d21b8ba 100644 --- a/learning/tour-of-beam/frontend/lib/cache/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/cache/content_tree.dart @@ -18,6 +18,8 @@ import 'dart:async'; +import 'package:playground_components/playground_components.dart'; + import '../models/content_tree.dart'; import 'cache.dart'; @@ -29,12 +31,12 @@ class ContentTreeCache extends Cache { final _treesBySdkId = {}; final _futuresBySdkId = >{}; - ContentTreeModel? getContentTree(String sdkId) { - if (!_futuresBySdkId.containsKey(sdkId)) { - unawaited(_loadContentTree(sdkId)); + ContentTreeModel? getContentTree(Sdk sdk) { + if (!_futuresBySdkId.containsKey(sdk.id)) { + unawaited(_loadContentTree(sdk.id)); } - return _treesBySdkId[sdkId]; + return _treesBySdkId[sdk.id]; } Future _loadContentTree(String sdkId) async { diff --git a/learning/tour-of-beam/frontend/lib/cache/unit_content.dart b/learning/tour-of-beam/frontend/lib/cache/unit_content.dart index d8499a641e27..8470ca3115d8 100644 --- a/learning/tour-of-beam/frontend/lib/cache/unit_content.dart +++ b/learning/tour-of-beam/frontend/lib/cache/unit_content.dart @@ -29,13 +29,16 @@ class UnitContentCache extends Cache { final _unitContents = >{}; final _futures = >>{}; - UnitContentModel? getUnitContent(String sdkId, String unitId) { + Future getUnitContent( + String sdkId, + String unitId, + ) async { final future = _futures[sdkId]?[unitId]; if (future == null) { - unawaited(_loadUnitContent(sdkId, unitId)); + await _loadUnitContent(sdkId, unitId); } - return _unitContents[sdkId]?[unitId]; + return _unitContents[sdkId]![unitId]!; } Future _loadUnitContent(String sdkId, String unitId) async { diff --git a/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart b/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart index 5649e7464674..db72d000331a 100644 --- a/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart +++ b/learning/tour-of-beam/frontend/lib/cache/unit_progress.dart @@ -18,23 +18,92 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; +import 'package:playground_components/playground_components.dart'; import '../auth/notifier.dart'; +import '../enums/snippet_type.dart'; import '../enums/unit_completion.dart'; +import '../models/unit_progress.dart'; +import '../repositories/client/client.dart'; import '../repositories/models/get_user_progress_response.dart'; +import '../repositories/user_progress/abstract.dart'; +import '../repositories/user_progress/cloud.dart'; +import '../repositories/user_progress/hive.dart'; import '../state.dart'; -import 'cache.dart'; -class UnitProgressCache extends Cache { - UnitProgressCache({required super.client}); +class UnitProgressCache extends ChangeNotifier { + final _cloudUserProgressRepository = CloudUserProgressRepository( + client: GetIt.instance.get(), + ); + final _localStorageUserProgressRepository = HiveUserProgressRepository(); + + AbstractUserProgressRepository _getUserProgressRepository() { + if (isAuthenticated) { + return _cloudUserProgressRepository; + } + return _localStorageUserProgressRepository; + } + + Future? _future; + + var _unitProgress = []; + final _unitProgressByUnitId = {}; final _completedUnitIds = {}; final _updatingUnitIds = {}; - Future? _future; + + bool get isAuthenticated => + GetIt.instance.get().isAuthenticated; + + Future loadUnitProgress(Sdk sdk) async { + _future = _getUserProgressRepository().getUserProgress(sdk); + final result = await _future; + + _unitProgressByUnitId.clear(); + if (result != null) { + _unitProgress = result.units; + for (final unitProgress in _unitProgress) { + _unitProgressByUnitId[unitProgress.id] = unitProgress; + } + } else { + _unitProgress = []; + } + notifyListeners(); + } + + List _getUnitProgress() { + if (_future == null) { + unawaited(loadUnitProgress(GetIt.instance.get().sdk!)); + } + return _unitProgress; + } + + // Completion + + Future completeUnit(String sdkId, String unitId) async { + try { + addUpdatingUnitId(unitId); + await _getUserProgressRepository().completeUnit(sdkId, unitId); + } finally { + await loadUnitProgress(GetIt.instance.get().sdk!); + clearUpdatingUnitId(unitId); + } + } Set getUpdatingUnitIds() => _updatingUnitIds; + Set getCompletedUnits() { + _completedUnitIds.clear(); + for (final unitProgress in _getUnitProgress()) { + if (unitProgress.isCompleted) { + _completedUnitIds.add(unitProgress.id); + } + } + return _completedUnitIds; + } + void addUpdatingUnitId(String unitId) { _updatingUnitIds.add(unitId); notifyListeners(); @@ -52,6 +121,14 @@ class UnitProgressCache extends Cache { return _getUnitCompletion(unitId) == UnitCompletion.uncompleted; } + bool isUnitCompleted(String? unitId) { + return getCompletedUnits().contains(unitId); + } + + String? getUnitSavedSnippetId(String? unitId) { + return _unitProgressByUnitId[unitId]?.userSnippetId; + } + UnitCompletion _getUnitCompletion(String unitId) { final authNotifier = GetIt.instance.get(); if (!authNotifier.isAuthenticated) { @@ -66,38 +143,40 @@ class UnitProgressCache extends Cache { return UnitCompletion.uncompleted; } - bool isUnitCompleted(String? unitId) { - return getCompletedUnits().contains(unitId); - } + // Snippet - Future updateCompletedUnits() async { - final sdkId = GetIt.instance.get().sdkId; - if (sdkId != null) { - await _loadCompletedUnits(sdkId); - } + bool hasSavedSnippet(String? unitId) { + return _unitProgressByUnitId[unitId]?.userSnippetId != null; } - Set getCompletedUnits() { - if (_future == null) { - unawaited(updateCompletedUnits()); - } - - return _completedUnitIds; + Future saveSnippet({ + required Sdk sdk, + required List snippetFiles, + required SnippetType snippetType, + required String unitId, + }) async { + await _getUserProgressRepository().saveUnitSnippet( + sdk: sdk, + snippetFiles: snippetFiles, + snippetType: snippetType, + unitId: unitId, + ); } - Future _loadCompletedUnits(String sdkId) async { - _future = client.getUserProgress(sdkId); - final result = await _future; - - _completedUnitIds.clear(); - if (result != null) { - for (final unitProgress in result.units) { - if (unitProgress.isCompleted) { - _completedUnitIds.add(unitProgress.id); - } - } - } + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }) async { + return _getUserProgressRepository().getSavedDescriptor( + sdk: sdk, + unitId: unitId, + ); + } - notifyListeners(); + Future deleteUserProgress() async { + await Future.wait([ + _localStorageUserProgressRepository.deleteUserProgress(), + _cloudUserProgressRepository.deleteUserProgress(), + ]); } } diff --git a/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart b/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart index 8ef706ead45a..7be3430a8a30 100644 --- a/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/components/builders/content_tree.dart @@ -18,16 +18,17 @@ import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; +import 'package:playground_components/playground_components.dart'; import '../../cache/content_tree.dart'; import '../../models/content_tree.dart'; class ContentTreeBuilder extends StatelessWidget { - final String sdkId; + final Sdk sdk; final ValueWidgetBuilder builder; const ContentTreeBuilder({ - required this.sdkId, + required this.sdk, required this.builder, }); @@ -39,7 +40,7 @@ class ContentTreeBuilder extends StatelessWidget { animation: contentTreeCache, builder: (context, child) => builder( context, - contentTreeCache.getContentTree(sdkId), + contentTreeCache.getContentTree(sdk), child, ), ); diff --git a/learning/tour-of-beam/frontend/lib/components/footer.dart b/learning/tour-of-beam/frontend/lib/components/footer.dart index 93c405ea3380..daba2dbe6e96 100644 --- a/learning/tour-of-beam/frontend/lib/components/footer.dart +++ b/learning/tour-of-beam/frontend/lib/components/footer.dart @@ -18,9 +18,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:playground_components/playground_components.dart'; import '../constants/sizes.dart'; +import '../state.dart'; class Footer extends StatelessWidget { const Footer({ @@ -40,18 +42,16 @@ class Footer extends StatelessWidget { spacing: BeamSizes.size16, crossAxisAlignment: WrapCrossAlignment.center, children: [ + FeedbackWidget( + controller: GetIt.instance.get(), + title: 'ui.feedbackTitle'.tr(), + ), ReportIssueButton(playgroundController: playgroundController), const PrivacyPolicyButton(), const CopyrightWidget(), ], ), - // TODO(nausharipov): get version, https://github.com/apache/beam/issues/23038 - Text( - '${'ui.builtWith'.tr()} (TODO: Version)', - style: const TextStyle( - color: BeamColors.grey3, - ), - ), + const _BeamVersion(), ], ), ); @@ -85,3 +85,41 @@ class _Body extends StatelessWidget { ); } } + +class _BeamVersion extends StatelessWidget { + const _BeamVersion(); + + Future _getBeamSdkVersion() async { + final sdk = GetIt.instance.get().sdk; + if (sdk == null) { + return null; + } + final runnerVersion = await GetIt.instance + .get() + .getRunnerVersion(sdk); + return runnerVersion.beamSdkVersion; + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: GetIt.instance.get(), + builder: (context, child) => FutureBuilder( + // ignore: discarded_futures + future: _getBeamSdkVersion(), + builder: (context, snapshot) => snapshot.hasData + ? Text( + 'ui.builtWith'.tr( + namedArgs: { + 'beamSdkVersion': snapshot.data!, + }, + ), + style: const TextStyle( + color: BeamColors.grey3, + ), + ) + : Container(), + ), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/components/login/button.dart b/learning/tour-of-beam/frontend/lib/components/login/button.dart index 2fb2038d6bfd..6ecf2ba5da3b 100644 --- a/learning/tour-of-beam/frontend/lib/components/login/button.dart +++ b/learning/tour-of-beam/frontend/lib/components/login/button.dart @@ -30,7 +30,7 @@ class LoginButton extends StatelessWidget { return TextButton( onPressed: () { final closeNotifier = PublicNotifier(); - openOverlay( + showOverlay( context: context, closeNotifier: closeNotifier, positioned: Positioned( diff --git a/learning/tour-of-beam/frontend/lib/components/login/content.dart b/learning/tour-of-beam/frontend/lib/components/login/content.dart index ae1fe80fa028..88f1d2b1a3c1 100644 --- a/learning/tour-of-beam/frontend/lib/components/login/content.dart +++ b/learning/tour-of-beam/frontend/lib/components/login/content.dart @@ -84,10 +84,13 @@ class _BrandedLoginButtons extends StatelessWidget { required this.onLoggedIn, }); + Future _logIn(AuthProvider authProvider) async { + await GetIt.instance.get().logIn(authProvider); + onLoggedIn(); + } + @override Widget build(BuildContext context) { - final authNotifier = GetIt.instance.get(); - final isLightTheme = Theme.of(context).brightness == Brightness.light; final textStyle = MaterialStatePropertyAll(Theme.of(context).textTheme.bodyMedium); @@ -124,16 +127,17 @@ class _BrandedLoginButtons extends StatelessWidget { return Column( children: [ ElevatedButton.icon( - onPressed: () {}, + onPressed: () { + _logIn(GithubAuthProvider()); + }, style: isLightTheme ? githubLightButtonStyle : darkButtonStyle, icon: SvgPicture.asset(Assets.svg.githubLogo), label: const Text('ui.continueGitHub').tr(), ), const SizedBox(height: BeamSizes.size16), ElevatedButton.icon( - onPressed: () async { - await authNotifier.logIn(GoogleAuthProvider()); - onLoggedIn(); + onPressed: () { + _logIn(GoogleAuthProvider()); }, style: isLightTheme ? googleLightButtonStyle : darkButtonStyle, icon: SvgPicture.asset(Assets.svg.googleLogo), 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 d4b88584dab6..d6e6621e97f0 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/avatar.dart @@ -32,7 +32,7 @@ class Avatar extends StatelessWidget { return GestureDetector( onTap: () { final closeNotifier = PublicNotifier(); - openOverlay( + showOverlay( context: context, closeNotifier: closeNotifier, positioned: Positioned( diff --git a/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart b/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart index 5fb49fa7192b..f3f633b0028f 100644 --- a/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart +++ b/learning/tour-of-beam/frontend/lib/components/profile/user_menu.dart @@ -125,9 +125,24 @@ class _Buttons extends StatelessWidget { ), const BeamDivider(), _IconLabel( - onTap: () {}, + onTap: () async { + closeOverlayCallback(); + final confirmed = await ConfirmDialog.show( + context: context, + confirmButtonText: 'ui.deleteMyAccount'.tr(), + subtitle: 'dialogs.deleteAccountWarning'.tr(), + title: 'ui.deleteTobAccount'.tr(), + ); + if (confirmed) { + ProgressDialog.show( + future: authNotifier.deleteAccount(), + navigatorKey: + GetIt.instance.get().navigatorKey!, + ); + } + }, iconPath: Assets.svg.profileDelete, - label: 'ui.deleteAccount'.tr(), + label: 'ui.deleteMyAccount'.tr(), ), ], ); diff --git a/learning/tour-of-beam/frontend/lib/components/scaffold.dart b/learning/tour-of-beam/frontend/lib/components/scaffold.dart index cbbade4c26c8..ea91b4cbe079 100644 --- a/learning/tour-of-beam/frontend/lib/components/scaffold.dart +++ b/learning/tour-of-beam/frontend/lib/components/scaffold.dart @@ -103,13 +103,13 @@ class _SdkSelector extends StatelessWidget { return AnimatedBuilder( animation: appNotifier, builder: (context, child) { - final sdkId = appNotifier.sdkId; - return sdkId == null + final sdk = appNotifier.sdk; + return sdk == null ? Container() : SdkDropdown( - sdkId: sdkId, + value: sdk, onChanged: (value) { - appNotifier.sdkId = value; + appNotifier.sdk = value; }, ); }, 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 9e84e3698e37..45e884db58a1 100644 --- a/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart +++ b/learning/tour-of-beam/frontend/lib/components/sdk_dropdown.dart @@ -22,11 +22,11 @@ import 'package:playground_components/playground_components.dart'; import 'builders/sdks.dart'; class SdkDropdown extends StatelessWidget { - final String sdkId; - final ValueChanged onChanged; + final Sdk value; + final ValueChanged onChanged; const SdkDropdown({ - required this.sdkId, + required this.value, required this.onChanged, }); @@ -40,10 +40,10 @@ class SdkDropdown extends StatelessWidget { return _DropdownWrapper( child: DropdownButton( - value: sdkId, - onChanged: (sdk) { - if (sdk != null) { - onChanged(sdk); + value: value.id, + onChanged: (sdkId) { + if (sdkId != null) { + onChanged(Sdk.parseOrCreate(sdkId)); } }, items: sdks diff --git a/learning/tour-of-beam/frontend/lib/config.dart b/learning/tour-of-beam/frontend/lib/config.dart index 3fb4be59eff9..e82b333e1663 100644 --- a/learning/tour-of-beam/frontend/lib/config.dart +++ b/learning/tour-of-beam/frontend/lib/config.dart @@ -18,8 +18,10 @@ // TODO(alexeyinkin): Generate this file on deployment. -const _cloudFunctionsProjectRegion = 'us-central1'; -const _cloudFunctionsProjectId = 'tour-of-beam-2'; +const environment = 'stg_'; + +const _cloudFunctionsProjectRegion = 'us-west1'; +const _cloudFunctionsProjectId = 'apache-beam-testing'; const cloudFunctionsBaseUrl = 'https://' '$_cloudFunctionsProjectRegion-$_cloudFunctionsProjectId' - '.cloudfunctions.net'; + '.cloudfunctions.net/$environment'; diff --git a/learning/tour-of-beam/frontend/lib/constants/hive_box_names.dart b/learning/tour-of-beam/frontend/lib/constants/hive_box_names.dart new file mode 100644 index 000000000000..c3900ccc5823 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/constants/hive_box_names.dart @@ -0,0 +1,28 @@ +/* + * 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/playground_components.dart'; + +class HiveBoxNames { + static const unitProgress = 'unit_progress'; + static const snippets = 'snippets'; + + static String getSdkBoxName(Sdk sdk, String boxName) { + return '${sdk.id}_$boxName'; + } +} diff --git a/learning/tour-of-beam/frontend/lib/constants/sizes.dart b/learning/tour-of-beam/frontend/lib/constants/sizes.dart index fd0eec3b97d1..9bd2c9477622 100644 --- a/learning/tour-of-beam/frontend/lib/constants/sizes.dart +++ b/learning/tour-of-beam/frontend/lib/constants/sizes.dart @@ -19,7 +19,7 @@ class TobSizes { static const double footerHeight = 35; static const double authOverlayWidth = 260; - static const double hintPopupWidth = 420; + static const double hintPopupWidth = 510; } class ScreenSizes { diff --git a/learning/tour-of-beam/frontend/lib/enums/save_code_status.dart b/learning/tour-of-beam/frontend/lib/enums/save_code_status.dart new file mode 100644 index 000000000000..cd5d302a198a --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/save_code_status.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 SaveCodeStatus { + error, + saved, + saving, +} diff --git a/learning/tour-of-beam/frontend/lib/enums/snippet_type.dart b/learning/tour-of-beam/frontend/lib/enums/snippet_type.dart new file mode 100644 index 000000000000..202d1c6fb9da --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/snippet_type.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 SnippetType { + original, + saved, + solution, +} diff --git a/learning/tour-of-beam/frontend/lib/enums/tour_view.dart b/learning/tour-of-beam/frontend/lib/enums/tour_view.dart new file mode 100644 index 000000000000..06eaea6fe766 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/tour_view.dart @@ -0,0 +1,26 @@ +/* + * 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:enum_map/enum_map.dart'; + +part 'tour_view.g.dart'; + +@unmodifiableEnumMap +enum TourView { + content, + playground, +} diff --git a/learning/tour-of-beam/frontend/lib/enums/tour_view.g.dart b/learning/tour-of-beam/frontend/lib/enums/tour_view.g.dart new file mode 100644 index 000000000000..c131f8aeca0c --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/enums/tour_view.g.dart @@ -0,0 +1,162 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tour_view.dart'; + +// ************************************************************************** +// UnmodifiableEnumMapGenerator +// ************************************************************************** + +class UnmodifiableTourViewMap extends UnmodifiableEnumMap { + final V content; + final V playground; + + const UnmodifiableTourViewMap({ + required this.content, + required this.playground, + }); + + @override + Map cast() { + return Map.castFrom(this); + } + + @override + bool containsValue(Object? value) { + if (this.content == value) return true; + if (this.playground == value) return true; + return false; + } + + @override + bool containsKey(Object? key) { + return key.runtimeType == TourView; + } + + @override + V? operator [](Object? key) { + switch (key) { + case TourView.content: + return this.content; + case TourView.playground: + return this.playground; + } + + return null; + } + + @override + void operator []=(TourView key, V value) { + throw Exception("Cannot modify this map."); + } + + @override + Iterable> get entries { + return [ + MapEntry(TourView.content, this.content), + MapEntry(TourView.playground, this.playground), + ]; + } + + @override + Map map(MapEntry transform(TourView key, V value)) { + final content = transform(TourView.content, this.content); + final playground = transform(TourView.playground, this.playground); + return { + content.key: content.value, + playground.key: playground.value, + }; + } + + @override + void addEntries(Iterable> newEntries) { + throw Exception("Cannot modify this map."); + } + + @override + V update(TourView key, V update(V value), {V Function()? ifAbsent}) { + throw Exception("Cannot modify this map."); + } + + @override + void updateAll(V update(TourView key, V value)) { + throw Exception("Cannot modify this map."); + } + + @override + void removeWhere(bool test(TourView key, V value)) { + throw Exception("Objects in this map cannot be removed."); + } + + @override + V putIfAbsent(TourView key, V ifAbsent()) { + return this.get(key); + } + + @override + void addAll(Map other) { + throw Exception("Cannot modify this map."); + } + + @override + V? remove(Object? key) { + throw Exception("Objects in this map cannot be removed."); + } + + @override + void clear() { + throw Exception("Objects in this map cannot be removed."); + } + + @override + void forEach(void action(TourView key, V value)) { + action(TourView.content, this.content); + action(TourView.playground, this.playground); + } + + @override + Iterable get keys { + return TourView.values; + } + + @override + Iterable get values { + return [ + this.content, + this.playground, + ]; + } + + @override + int get length { + return 2; + } + + @override + bool get isEmpty { + return false; + } + + @override + bool get isNotEmpty { + return true; + } + + V get(TourView key) { + switch (key) { + case TourView.content: + return this.content; + case TourView.playground: + return this.playground; + } + } + + @override + String toString() { + final buffer = StringBuffer("{"); + buffer.write("TourView.content: ${this.content}"); + buffer.write(", "); + buffer.write("TourView.playground: ${this.playground}"); + buffer.write("}"); + return buffer.toString(); + } +} diff --git a/learning/tour-of-beam/frontend/lib/firebase_options.dart b/learning/tour-of-beam/frontend/lib/firebase_options.dart index e2a871d637b3..e64b594af433 100644 --- a/learning/tour-of-beam/frontend/lib/firebase_options.dart +++ b/learning/tour-of-beam/frontend/lib/firebase_options.dart @@ -53,11 +53,11 @@ class DefaultFirebaseOptions { } static const FirebaseOptions web = FirebaseOptions( - apiKey: 'AIzaSyBtAreurqJ5D4IK6cNisZh5dnDRKljbJAw', - authDomain: 'astest-369409.firebaseapp.com', - projectId: 'astest-369409', - storageBucket: 'astest-369409.appspot.com', - messagingSenderId: '534850967604', - appId: '1:534850967604:web:55c6af8da7940df1ddd261', + apiKey: 'AIzaSyAg7ZLslQRrEhwVVVzZb1OFMRMHL8kNL38', + authDomain: 'apache-beam-testing.firebaseapp.com', + projectId: 'apache-beam-testing', + storageBucket: 'apache-beam-testing.appspot.com', + messagingSenderId: '844138762903', + appId: '1:844138762903:web:c0094c4e6bba87d73d8fd2', ); } diff --git a/learning/tour-of-beam/frontend/lib/locator.dart b/learning/tour-of-beam/frontend/lib/locator.dart index e627c3b99ca6..46095e92948a 100644 --- a/learning/tour-of-beam/frontend/lib/locator.dart +++ b/learning/tour-of-beam/frontend/lib/locator.dart @@ -25,7 +25,6 @@ import 'cache/content_tree.dart'; import 'cache/sdk.dart'; import 'cache/unit_content.dart'; import 'cache/unit_progress.dart'; -import 'config.dart'; import 'pages/welcome/page.dart'; import 'repositories/client/client.dart'; import 'repositories/client/cloud_functions_client.dart'; @@ -33,13 +32,36 @@ import 'router/page_factory.dart'; import 'router/route_information_parser.dart'; import 'state.dart'; +final _client = CloudFunctionsTobClient(); + Future initializeServiceLocator() async { + await _initializeRepositories(); _initializeAuth(); _initializeState(); _initializeServices(); _initializeCaches(); } +Future _initializeRepositories() async { + final routerUrl = await getRouterUrl(); + + final codeClient = GrpcCodeClient( + url: routerUrl, + // TODO(nausharipov): Remove the hardcoded SDKs when runners are hidden. + runnerUrlsById: { + Sdk.java.id: await getRunnerUrl(Sdk.java), + Sdk.go.id: await getRunnerUrl(Sdk.go), + Sdk.python.id: await getRunnerUrl(Sdk.python), + }, + ); + final exampleClient = GrpcExampleClient(url: routerUrl); + + GetIt.instance.registerSingleton(codeClient); + GetIt.instance.registerSingleton(CodeRepository(client: codeClient)); + GetIt.instance.registerSingleton(exampleClient); + GetIt.instance.registerSingleton(ExampleRepository(client: exampleClient)); +} + void _initializeAuth() { GetIt.instance.registerSingleton(AuthNotifier()); } @@ -51,18 +73,18 @@ void _initializeCaches() { GetIt.instance.registerSingleton(ContentTreeCache(client: client)); GetIt.instance.registerSingleton(SdkCache(client: client)); GetIt.instance.registerSingleton(UnitContentCache(client: client)); - GetIt.instance.registerSingleton(UnitProgressCache(client: client)); + GetIt.instance.registerSingleton(UnitProgressCache()); } void _initializeState() { - GetIt.instance.registerSingleton(AppNotifier()); - GetIt.instance.registerSingleton( - PageStack( - bottomPage: WelcomePage(), - createPage: PageFactory.createPage, - routeInformationParser: TobRouteInformationParser(), - ), + final pageStack = PageStack( + bottomPage: WelcomePage(), + createPage: PageFactory.createPage, + routeInformationParser: TobRouteInformationParser(), ); + GetIt.instance.registerSingleton(AppNotifier()); + GetIt.instance.registerSingleton(pageStack); + GetIt.instance.registerSingleton(BeamRouterDelegate(pageStack)); } void _initializeServices() { diff --git a/learning/tour-of-beam/frontend/lib/main.dart b/learning/tour-of-beam/frontend/lib/main.dart index 405c99b25da6..c8493293960c 100644 --- a/learning/tour-of-beam/frontend/lib/main.dart +++ b/learning/tour-of-beam/frontend/lib/main.dart @@ -42,7 +42,7 @@ void main() async { const englishLocale = Locale('en'); final pageStack = GetIt.instance.get(); - final routerDelegate = PageStackRouterDelegate(pageStack); + final routerDelegate = GetIt.instance.get(); final routeInformationParser = TobRouteInformationParser(); final backButtonDispatcher = PageStackBackButtonDispatcher(pageStack); diff --git a/learning/tour-of-beam/frontend/lib/models/unit_progress.dart b/learning/tour-of-beam/frontend/lib/models/unit_progress.dart index 473c8ae0d4e6..e3753b81048d 100644 --- a/learning/tour-of-beam/frontend/lib/models/unit_progress.dart +++ b/learning/tour-of-beam/frontend/lib/models/unit_progress.dart @@ -20,16 +20,20 @@ import 'package:json_annotation/json_annotation.dart'; part 'unit_progress.g.dart'; -@JsonSerializable(createToJson: false) +@JsonSerializable() class UnitProgressModel { final String id; final bool isCompleted; + final String? userSnippetId; const UnitProgressModel({ required this.id, required this.isCompleted, + required this.userSnippetId, }); factory UnitProgressModel.fromJson(Map json) => _$UnitProgressModelFromJson(json); + + Map toJson() => _$UnitProgressModelToJson(this); } diff --git a/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart b/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart index c1a773cd66a9..5c5511604b2b 100644 --- a/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart +++ b/learning/tour-of-beam/frontend/lib/models/unit_progress.g.dart @@ -10,4 +10,12 @@ UnitProgressModel _$UnitProgressModelFromJson(Map json) => UnitProgressModel( id: json['id'] as String, isCompleted: json['isCompleted'] as bool, + userSnippetId: json['userSnippetId'] as String?, ); + +Map _$UnitProgressModelToJson(UnitProgressModel instance) => + { + 'id': instance.id, + 'isCompleted': instance.isCompleted, + 'userSnippetId': instance.userSnippetId, + }; diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart b/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart index bcdb686a10b7..aa6118778219 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/controllers/content_tree.dart @@ -26,9 +26,8 @@ import '../../../models/node.dart'; import '../../../models/unit.dart'; class ContentTreeController extends ChangeNotifier { - String _sdkId; + Sdk _sdk; List _treeIds; - // TODO(nausharipov): non-nullable currentNode? NodeModel? _currentNode; final _contentTreeCache = GetIt.instance.get(); final _expandedIds = {}; @@ -36,9 +35,9 @@ class ContentTreeController extends ChangeNotifier { Set get expandedIds => _expandedIds; ContentTreeController({ - required String initialSdkId, + required Sdk initialSdk, List initialTreeIds = const [], - }) : _sdkId = initialSdkId, + }) : _sdk = initialSdk, _treeIds = initialTreeIds { _expandedIds.addAll(initialTreeIds); @@ -46,29 +45,23 @@ class ContentTreeController extends ChangeNotifier { _onContentTreeCacheChange(); } - Sdk get sdk => Sdk.parseOrCreate(_sdkId); - String get sdkId => _sdkId; - set sdkId(String newValue) { - _sdkId = newValue; + Sdk get sdk => _sdk; + + set sdk(Sdk newValue) { + _sdk = newValue; notifyListeners(); } List get treeIds => _treeIds; NodeModel? get currentNode => _currentNode; - void openNode(NodeModel node) { - if (!_expandedIds.contains(node.id)) { - _expandedIds.add(node.id); - } - - if (node == _currentNode) { - return; - } - + void onNodePressed(NodeModel node) { if (node is GroupModel) { - openNode(node.nodes.first); + _onGroupPressed(node); } else if (node is UnitModel) { - _currentNode = node; + if (node != _currentNode) { + _currentNode = node; + } } if (_currentNode != null) { @@ -77,6 +70,19 @@ class ContentTreeController extends ChangeNotifier { notifyListeners(); } + void _onGroupPressed(GroupModel group) { + if (_expandedIds.contains(group.id)) { + _expandedIds.remove(group.id); + notifyListeners(); + } else { + _expandedIds.add(group.id); + final groupFirstUnit = group.nodes.first; + if (groupFirstUnit != _currentNode) { + onNodePressed(groupFirstUnit); + } + } + } + void expandGroup(GroupModel group) { _expandedIds.add(group.id); notifyListeners(); @@ -98,12 +104,12 @@ class ContentTreeController extends ChangeNotifier { } void _onContentTreeCacheChange() { - final contentTree = _contentTreeCache.getContentTree(_sdkId); + final contentTree = _contentTreeCache.getContentTree(_sdk); if (contentTree == null) { return; } - openNode( + onNodePressed( contentTree.getNodeByTreeIds(_treeIds) ?? contentTree.getFirstUnit(), ); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/unit.dart b/learning/tour-of-beam/frontend/lib/pages/tour/controllers/unit.dart deleted file mode 100644 index 2008b403c39c..000000000000 --- a/learning/tour-of-beam/frontend/lib/pages/tour/controllers/unit.dart +++ /dev/null @@ -1,58 +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/widgets.dart'; -import 'package:get_it/get_it.dart'; -import 'package:playground_components/playground_components.dart'; - -import '../../../cache/unit_progress.dart'; -import '../../../models/event_context.dart'; -import '../../../models/unit.dart'; -import '../../../repositories/client/client.dart'; -import '../../../services/analytics/events/unit_completed.dart'; - -/// The state object for the [unit] being currently open. -class UnitController extends ChangeNotifier { - final UnitModel unit; - final Sdk sdk; - - UnitController({ - required this.unit, - required this.sdk, - }); - - Future completeUnit() async { - final client = GetIt.instance.get(); - final unitProgressCache = GetIt.instance.get(); - try { - unitProgressCache.addUpdatingUnitId(unit.id); - await client.postUnitComplete(sdk.id, unit.id); - } finally { - PlaygroundComponents.analyticsService.sendUnawaited( - UnitCompletedTobAnalyticsEvent( - tobContext: TobEventContext( - sdkId: sdk.id, - unitId: unit.id, - ), - ), - ); - await unitProgressCache.updateCompletedUnits(); - unitProgressCache.clearUpdatingUnitId(unit.id); - } - } -} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/page.dart b/learning/tour-of-beam/frontend/lib/pages/tour/page.dart index 1272a5a35fbb..628576e3a217 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/page.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/page.dart @@ -18,6 +18,7 @@ import 'package:app_state/app_state.dart'; import 'package:flutter/widgets.dart'; +import 'package:playground_components/playground_components.dart'; import 'screen.dart'; import 'state.dart'; @@ -27,12 +28,12 @@ class TourPage extends StatefulMaterialPage { /// Called when navigating to the page programmatically. TourPage({ - required String sdkId, + required Sdk sdk, List treeIds = const [], }) : super( key: const ValueKey(classFactoryKey), state: TourNotifier( - initialSdkId: sdkId, + initialSdk: sdk, initialTreeIds: treeIds, ), createScreen: TourScreen.new, @@ -43,7 +44,7 @@ class TourPage extends StatefulMaterialPage { final treeIds = state['treeIds']; return TourPage( - sdkId: state['sdkId'], + sdk: Sdk.parseOrCreate(state['sdkId']), treeIds: treeIds is List ? treeIds.cast() : const [], ); } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/path.dart b/learning/tour-of-beam/frontend/lib/pages/tour/path.dart index 07dd386bdfcb..af8452d17548 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/path.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/path.dart @@ -26,7 +26,8 @@ class TourPath extends PagePath { final String sdkId; final List treeIds; - static final _regExp = RegExp(r'^/tour/([a-z]+)((/[-a-zA-Z0-9]+)*)$'); + static final _regExp = + RegExp(r'^/tour/([a-z]+)/?([-a-zA-Z0-9]+(/[-a-zA-Z0-9]+)*)?/?$'); TourPath({ required this.sdkId, @@ -49,10 +50,8 @@ class TourPath extends PagePath { final sdkId = matches[1] ?? (throw Error()); final treeIdsString = matches[2]; - final treeIds = (treeIdsString == null) - ? const [] - // TODO(nausharipov): use RegExp to remove the slash - : treeIdsString.substring(1).split('/'); + final treeIds = + treeIdsString == null ? const [] : treeIdsString.split('/'); return TourPath( sdkId: sdkId, 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 383e5f315438..21bb11017727 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/screen.dart @@ -16,11 +16,14 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:keyed_collection_widgets/keyed_collection_widgets.dart'; import 'package:playground_components/playground_components.dart'; import '../../components/scaffold.dart'; import '../../constants/sizes.dart'; +import '../../enums/tour_view.dart'; import '../../shortcuts/shortcuts_manager.dart'; import 'state.dart'; import 'widgets/content_tree.dart'; @@ -78,36 +81,42 @@ class _NarrowTour extends StatelessWidget { @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ContentTreeWidget(controller: tourNotifier.contentTreeController), - Expanded(child: UnitContentWidget(tourNotifier)), - ], - ), - DecoratedBox( - decoration: BoxDecoration( - border: Border( - top: BorderSide(color: Theme.of(context).dividerColor), - ), + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ContentTreeWidget(controller: tourNotifier.contentTreeController), + Expanded( + child: DefaultKeyedTabController.fromKeys( + keys: TourView.values, + child: Column( + children: [ + KeyedTabBar.withDefaultController( + tabs: UnmodifiableTourViewMap( + content: Tab( + text: 'pages.tour.content'.tr(), + ), + playground: Tab( + text: 'pages.tour.playground'.tr(), + ), + ), + ), + Expanded( + child: KeyedTabBarView.withDefaultController( + children: UnmodifiableTourViewMap( + content: UnitContentWidget( + tourNotifier, + ), + playground: PlaygroundWidget( + tourNotifier: tourNotifier, + ), + ), + ), + ), + ], ), - child: const _NarrowScreenPlayground(), ), - ], - ), + ), + ], ); } } - -class _NarrowScreenPlayground extends StatelessWidget { - const _NarrowScreenPlayground(); - - @override - Widget build(BuildContext context) { - // 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/tour/state.dart b/learning/tour-of-beam/frontend/lib/pages/tour/state.dart index c069b384d73f..ea2d2ac113f8 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/state.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/state.dart @@ -22,11 +22,14 @@ import 'package:app_state/app_state.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:playground_components/playground_components.dart'; +import 'package:rate_limiter/rate_limiter.dart'; import '../../auth/notifier.dart'; import '../../cache/unit_content.dart'; import '../../cache/unit_progress.dart'; import '../../config.dart'; +import '../../enums/save_code_status.dart'; +import '../../enums/snippet_type.dart'; import '../../models/event_context.dart'; import '../../models/unit.dart'; import '../../models/unit_content.dart'; @@ -34,13 +37,14 @@ import '../../services/analytics/events/unit_closed.dart'; import '../../services/analytics/events/unit_opened.dart'; import '../../state.dart'; import 'controllers/content_tree.dart'; -import 'controllers/unit.dart'; import 'path.dart'; class TourNotifier extends ChangeNotifier with PageStateMixin { + static const _saveUserCodeDebounceDuration = Duration(seconds: 2); + Debounce? _saveCodeDebounced; + final ContentTreeController contentTreeController; final PlaygroundController playgroundController; - UnitController? currentUnitController; final _appNotifier = GetIt.instance.get(); final _authNotifier = GetIt.instance.get(); final _unitContentCache = GetIt.instance.get(); @@ -52,113 +56,125 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { TobEventContext get tobEventContext => _tobEventContext; TourNotifier({ - required String initialSdkId, + required Sdk initialSdk, List initialTreeIds = const [], }) : contentTreeController = ContentTreeController( - initialSdkId: initialSdkId, + initialSdk: initialSdk, initialTreeIds: initialTreeIds, ), - playgroundController = _createPlaygroundController(initialSdkId) { + playgroundController = _createPlaygroundController(initialSdk.id) { + _appNotifier.sdk ??= initialSdk; contentTreeController.addListener(_onUnitChanged); _unitContentCache.addListener(_onUnitChanged); _appNotifier.addListener(_onAppNotifierChanged); - _authNotifier.addListener(_onUnitProgressChanged); - _onUnitChanged(); + _authNotifier.addListener(_onAuthChanged); + _saveCodeDebounced = _saveCode.debounced( + _saveUserCodeDebounceDuration, + ); + // setSdk creates snippetEditingController if it doesn't exist. + playgroundController.setSdk(currentSdk); + _listenToCurrentSnippetEditingController(); + unawaited(_onUnitChanged()); + } + + @override + void setStateMap(Map state) { + super.setStateMap(state); + _appNotifier.sdk = Sdk.parseOrCreate(state['sdkId']); } @override PagePath get path => TourPath( - sdkId: contentTreeController.sdkId, + sdkId: contentTreeController.sdk.id, treeIds: contentTreeController.treeIds, ); - String? get currentUnitId => currentUnitController?.unit.id; + bool get isAuthenticated => _authNotifier.isAuthenticated; + + Sdk get currentSdk => _appNotifier.sdk!; + String? get currentUnitId => _currentUnitContent?.id; UnitContentModel? get currentUnitContent => _currentUnitContent; - bool get doesCurrentUnitHaveSolution => - currentUnitContent?.solutionSnippetId != null; - bool _isShowingSolution = false; - bool get isShowingSolution => _isShowingSolution; - - void toggleShowingSolution() { - if (doesCurrentUnitHaveSolution) { - _isShowingSolution = !_isShowingSolution; - - final snippetId = _isShowingSolution - ? _currentUnitContent?.solutionSnippetId - : _currentUnitContent?.taskSnippetId; - if (snippetId != null) { - // TODO(nausharipov): store/recover - unawaited(_setPlaygroundSnippet(snippetId)); - } - - notifyListeners(); - } - } - void _createCurrentUnitController(Sdk sdk, UnitModel unit) { - currentUnitController = UnitController( - unit: unit, - sdk: sdk, - ); - } + bool get hasSolution => currentUnitContent?.solutionSnippetId != null; + bool get isCodeSaved => _unitProgressCache.hasSavedSnippet(currentUnitId); + + SnippetType _snippetType = SnippetType.original; + SnippetType get snippetType => _snippetType; - Future _onUnitProgressChanged() async { - await _unitProgressCache.updateCompletedUnits(); + SaveCodeStatus _saveCodeStatus = SaveCodeStatus.saved; + SaveCodeStatus get saveCodeStatus => _saveCodeStatus; + set saveCodeStatus(SaveCodeStatus saveCodeStatus) { + _saveCodeStatus = saveCodeStatus; + notifyListeners(); } - void _onAppNotifierChanged() { - final sdkId = _appNotifier.sdkId; - if (sdkId != null) { - playgroundController.setSdk(Sdk.parseOrCreate(sdkId)); - contentTreeController.sdkId = sdkId; - _onUnitProgressChanged(); + Future _onAuthChanged() async { + await _unitProgressCache.loadUnitProgress(currentSdk); + // The local changes are preserved if the user signs in. + if (_snippetType != SnippetType.saved || !isAuthenticated) { + _trySetSnippetType(SnippetType.saved); + await _loadSnippetByType(); } + notifyListeners(); + } + + Future _onAppNotifierChanged() async { + contentTreeController.sdk = currentSdk; + playgroundController.setSdk(currentSdk); + _listenToCurrentSnippetEditingController(); + + await _unitProgressCache.loadUnitProgress(currentSdk); + _trySetSnippetType(SnippetType.saved); + await _loadSnippetByType(); } - void _onUnitChanged() { + Future _onUnitChanged() async { emitPathChanged(); final currentNode = contentTreeController.currentNode; - if (currentNode is UnitModel) { - final sdk = contentTreeController.sdk; - _createCurrentUnitController(contentTreeController.sdk, currentNode); - _setCurrentUnitContent(currentNode, sdk: sdk); + if (currentNode is! UnitModel) { + await _emptyPlayground(); } else { - _emptyPlayground(); + final sdk = contentTreeController.sdk; + final content = await _unitContentCache.getUnitContent( + sdk.id, + currentNode.id, + ); + _setUnitContent(content); + await _unitProgressCache.loadUnitProgress(currentSdk); + _trySetSnippetType(SnippetType.saved); + await _loadSnippetByType(); } - notifyListeners(); } - Future _setCurrentUnitContent( - UnitModel unit, { - required Sdk sdk, - }) async { - final content = _unitContentCache.getUnitContent( - sdk.id, - unit.id, - ); - if (content == _currentUnitContent) { + void _setUnitContent(UnitContentModel? unitContent) { + if (unitContent == null || unitContent == _currentUnitContent) { return; } if (_currentUnitOpenedAt != null && _currentUnitContent != null) { - PlaygroundComponents.analyticsService.sendUnawaited( - UnitClosedTobAnalyticsEvent( - tobContext: _tobEventContext, - timeSpent: DateTime.now().difference(_currentUnitOpenedAt!), - ), - ); + _trackUnitClosed(); } - _currentUnitContent = content; - if (content == null) { - return; - } + _currentUnitContent = unitContent; + _trackUnitOpened(unitContent.id); + } + + void _trackUnitClosed() { + PlaygroundComponents.analyticsService.sendUnawaited( + UnitClosedTobAnalyticsEvent( + tobContext: _tobEventContext, + timeSpent: DateTime.now().difference(_currentUnitOpenedAt!), + ), + ); + } + + void _trackUnitOpened(String unitId) { _currentUnitOpenedAt = DateTime.now(); _tobEventContext = TobEventContext( - sdkId: sdk.id, - unitId: unit.id, + sdkId: currentSdk.id, + unitId: unitId, ); playgroundController .requireSnippetEditingController() @@ -168,60 +184,165 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { tobContext: _tobEventContext, ), ); + } - final taskSnippetId = content.taskSnippetId; - await _setPlaygroundSnippet(taskSnippetId); - _isShowingSolution = false; + // Save user code. + + Future showSnippetByType(SnippetType snippetType) async { + _trySetSnippetType(snippetType); + await _loadSnippetByType(); + notifyListeners(); } - Future _setPlaygroundSnippet(String? snippetId) async { - if (snippetId == null) { - await _emptyPlayground(); - return; + void _listenToCurrentSnippetEditingController() { + playgroundController.snippetEditingController?.addListener( + _onActiveFileControllerChanged, + ); + } + + void _onActiveFileControllerChanged() { + playgroundController + .snippetEditingController?.activeFileController?.codeController + .addListener(_onCodeChanged); + } + + void _onCodeChanged() { + final snippetEditingController = + playgroundController.snippetEditingController!; + final isCodeChanged = + snippetEditingController.activeFileController?.isChanged ?? false; + final snippetFiles = snippetEditingController.getFiles(); + + final doSave = _isSnippetTypeSavable() && + isCodeChanged && + _currentUnitContent != null && + snippetFiles.isNotEmpty; + + if (doSave) { + // Snapshot of sdk and unitId at the moment of editing. + final sdk = currentSdk; + final unitId = currentUnitId; + _saveCodeDebounced?.call([], { + const Symbol('sdk'): sdk, + const Symbol('snippetFiles'): snippetFiles, + const Symbol('unitId'): unitId, + }); } + } - final selectedSdk = _appNotifier.sdk; - if (selectedSdk != null) { - await playgroundController.examplesLoader.load( - ExamplesLoadingDescriptor( - descriptors: [ - UserSharedExampleLoadingDescriptor( - sdk: selectedSdk, - snippetId: snippetId, - ), - ], - ), + bool _isSnippetTypeSavable() { + return snippetType != SnippetType.solution; + } + + Future _saveCode({ + required Sdk sdk, + required List snippetFiles, + required String unitId, + }) async { + saveCodeStatus = SaveCodeStatus.saving; + try { + await _unitProgressCache.saveSnippet( + sdk: sdk, + snippetFiles: snippetFiles, + snippetType: _snippetType, + unitId: unitId, ); + saveCodeStatus = SaveCodeStatus.saved; + await _unitProgressCache.loadUnitProgress(currentSdk); + _trySetSnippetType(SnippetType.saved); + } on Exception catch (e) { + print(['Could not save code: ', e]); + _saveCodeStatus = SaveCodeStatus.error; } } - // TODO(alexeyinkin): Hide the entire right pane instead. - Future _emptyPlayground() async { + void _trySetSnippetType(SnippetType snippetType) { + if (snippetType == SnippetType.saved && !isCodeSaved) { + _snippetType = SnippetType.original; + } else { + _snippetType = snippetType; + } + notifyListeners(); + } + + Future _loadSnippetByType() async { + final ExampleLoadingDescriptor descriptor; + switch (_snippetType) { + case SnippetType.original: + descriptor = _getStandardOrEmptyDescriptor( + currentSdk, + _currentUnitContent!.taskSnippetId, + ); + break; + case SnippetType.saved: + descriptor = await _unitProgressCache.getSavedDescriptor( + sdk: currentSdk, + unitId: _currentUnitContent!.id, + ); + break; + case SnippetType.solution: + descriptor = _getStandardOrEmptyDescriptor( + currentSdk, + _currentUnitContent!.solutionSnippetId, + ); + break; + } await playgroundController.examplesLoader.load( ExamplesLoadingDescriptor( descriptors: [ - EmptyExampleLoadingDescriptor(sdk: contentTreeController.sdk), + descriptor, ], ), ); + + _fillFeedbackController(); } - static PlaygroundController _createPlaygroundController(String initialSdkId) { - final exampleRepository = GetIt.instance.get(); - final codeRepository = GetIt.instance.get(); + void _fillFeedbackController() { + final controller = GetIt.instance.get(); + controller.eventSnippetContext = playgroundController.eventSnippetContext; + controller.additionalParams = _tobEventContext.toJson(); + } - final exampleCache = ExampleCache( - exampleRepository: exampleRepository, + ExampleLoadingDescriptor _getStandardOrEmptyDescriptor( + Sdk sdk, + String? snippetId, + ) { + if (snippetId == null) { + return EmptyExampleLoadingDescriptor( + sdk: currentSdk, + ); + } + return StandardExampleLoadingDescriptor( + path: snippetId, + sdk: sdk, + ); + } + + // TODO(alexeyinkin): Hide the entire right pane instead. + Future _emptyPlayground() async { + await playgroundController.examplesLoader.loadIfNew( + ExamplesLoadingDescriptor( + descriptors: [ + EmptyExampleLoadingDescriptor(sdk: contentTreeController.sdk), + ], + ), ); + } + + // Playground controller. + static PlaygroundController _createPlaygroundController(String initialSdkId) { final playgroundController = PlaygroundController( - codeRepository: codeRepository, - exampleCache: exampleCache, + codeRepository: GetIt.instance.get(), + exampleCache: ExampleCache( + exampleRepository: GetIt.instance.get(), + ), examplesLoader: ExamplesLoader(), ); unawaited( - playgroundController.examplesLoader.load( + playgroundController.examplesLoader.loadIfNew( ExamplesLoadingDescriptor( descriptors: [ EmptyExampleLoadingDescriptor(sdk: Sdk.parseOrCreate(initialSdkId)), @@ -238,7 +359,13 @@ class TourNotifier extends ChangeNotifier with PageStateMixin { _unitContentCache.removeListener(_onUnitChanged); contentTreeController.removeListener(_onUnitChanged); _appNotifier.removeListener(_onAppNotifierChanged); - _authNotifier.removeListener(_onUnitProgressChanged); + _authNotifier.removeListener(_onAuthChanged); + playgroundController.snippetEditingController + ?.removeListener(_onActiveFileControllerChanged); + // TODO(nausharipov): Use stream events https://github.com/apache/beam/issues/25185 + playgroundController + .snippetEditingController?.activeFileController?.codeController + .removeListener(_onCodeChanged); await super.dispose(); } } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart index f562f1218252..db0b174e5daa 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/binary_progress.dart @@ -50,7 +50,10 @@ class BinaryProgressIndicator extends StatelessWidget { ), child: SvgPicture.asset( isCompleted ? Assets.svg.unitProgress100 : Assets.svg.unitProgress0, - color: color, + colorFilter: ColorFilter.mode( + color, + BlendMode.srcIn, + ), ), ); } diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart index f29c04a56c5a..8b13d993e095 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/complete_unit_button.dart @@ -28,6 +28,17 @@ class CompleteUnitButton extends StatelessWidget { final TourNotifier tourNotifier; const CompleteUnitButton(this.tourNotifier); + Future _onPressed() async { + final unitId = tourNotifier.currentUnitId; + if (unitId == null) { + return; + } + await GetIt.instance.get().completeUnit( + tourNotifier.currentSdk.id, + unitId, + ); + } + @override Widget build(BuildContext context) { final themeData = Theme.of(context); @@ -40,9 +51,7 @@ class CompleteUnitButton extends StatelessWidget { unitProgressCache.canCompleteUnit(tourNotifier.currentUnitId); final borderColor = canComplete ? themeData.primaryColor : themeData.disabledColor; - final onPressed = canComplete - ? tourNotifier.currentUnitController?.completeUnit - : null; + final onPressed = canComplete ? _onPressed : null; return Flexible( child: OutlinedButton( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart index a40f14d35c60..caee4c9d9f3d 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/content_tree.dart @@ -34,28 +34,28 @@ class ContentTreeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return SizedBox( width: 250, - padding: const EdgeInsets.symmetric(horizontal: BeamSizes.size12), child: ContentTreeBuilder( - sdkId: controller.sdkId, + sdk: controller.sdk, builder: (context, contentTree, child) { if (contentTree == null) { return Container(); } return SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: BeamSizes.size12, + ), child: Column( children: [ const ContentTreeTitleWidget(), - ...contentTree.modules - .map( - (module) => ModuleWidget( - module: module, - contentTreeController: controller, - ), - ) - .toList(growable: false), + ...contentTree.modules.map( + (module) => ModuleWidget( + module: module, + contentTreeController: controller, + ), + ), const SizedBox(height: BeamSizes.size12), ], ), diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart index fad732b105bb..a3937e27d7a6 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/group.dart @@ -52,7 +52,7 @@ class GroupWidget extends StatelessWidget { title: GroupTitleWidget( group: group, onTap: () { - contentTreeController.openNode(group); + contentTreeController.onNodePressed(group); }, ), child: GroupNodesWidget( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart index 4ae39f99e5df..424ce969d96f 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/hints.dart @@ -22,7 +22,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:playground_components/playground_components.dart'; import '../../../assets/assets.gen.dart'; -import '../../../constants/sizes.dart'; import 'markdown/tob_markdown.dart'; class HintsWidget extends StatelessWidget { @@ -47,7 +46,7 @@ class HintsWidget extends StatelessWidget { } }, icon: SvgPicture.asset(Assets.svg.hint), - label: const Text('ui.hint').tr(), + label: const Text('pages.tour.hint').tr(), ); } } @@ -63,22 +62,24 @@ class _Popup extends StatelessWidget { Widget build(BuildContext context) { return OverlayBody( child: Container( - width: TobSizes.hintPopupWidth, + width: BeamSizes.popupWidth, padding: const EdgeInsets.all(BeamSizes.size16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'ui.hint', - style: Theme.of(context).textTheme.headlineLarge, - ).tr(), - const SizedBox(height: BeamSizes.size8), - TobMarkdown( - padding: EdgeInsets.zero, - data: hint, - ), - ], + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'pages.tour.hint', + style: Theme.of(context).textTheme.headlineLarge, + ).tr(), + const SizedBox(height: BeamSizes.size8), + TobMarkdown( + padding: EdgeInsets.zero, + data: hint, + ), + ], + ), ), ), ); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart index c00bfc3f830f..2eaabc10a506 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/code_builder.dart @@ -27,13 +27,36 @@ class MarkdownCodeBuilder extends MarkdownElementBuilder { final String textContent = element.textContent; final bool isCodeBlock = textContent.contains('\n'); if (isCodeBlock) { - /// codeblockDecoration is applied - return null; + return _CodeBlock(text: textContent); } return _InlineCode(text: textContent); } } +class _CodeBlock extends StatelessWidget { + final String text; + const _CodeBlock({required this.text}); + + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return Padding( + padding: const EdgeInsets.all(BeamSizes.size4), + child: Scrollbar( + controller: scrollController, + scrollbarOrientation: ScrollbarOrientation.bottom, + thumbVisibility: true, + child: SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.all(BeamSizes.size10), + scrollDirection: Axis.horizontal, + child: Text(text), + ), + ), + ); + } +} + class _InlineCode extends StatelessWidget { final String text; const _InlineCode({required this.text}); diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart index 0c8e74184998..dcbf11d8efa2 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/markdown/tob_markdown.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:playground_components/playground_components.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'code_builder.dart'; @@ -42,6 +43,11 @@ class TobMarkdown extends StatelessWidget { builders: { 'code': MarkdownCodeBuilder(), }, + onTapLink: (text, url, title) async { + if (url != null) { + await launchUrl(Uri.parse(url)); + } + }, padding: padding, selectable: true, shrinkWrap: shrinkWrap, diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart index b01987bf0a7c..383fa54925ad 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/module.dart @@ -39,7 +39,7 @@ class ModuleWidget extends StatelessWidget { children: [ ModuleTitleWidget( module: module, - onTap: () => contentTreeController.openNode(module), + onTap: () => contentTreeController.onNodePressed(module), ), ...module.nodes .map( diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart index b9ca640f7d93..c8ee6627a252 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit.dart @@ -44,7 +44,7 @@ class UnitWidget extends StatelessWidget { final isSelected = contentTreeController.currentNode?.id == unit.id; return ClickableWidget( - onTap: () => contentTreeController.openNode(unit), + onTap: () => contentTreeController.onNodePressed(unit), child: Container( decoration: BoxDecoration( color: isSelected ? Theme.of(context).selectedRowColor : null, diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart index 314c01aa6532..3b47937a8d43 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart +++ b/learning/tour-of-beam/frontend/lib/pages/tour/widgets/unit_content.dart @@ -16,16 +16,19 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; + import 'package:playground_components/playground_components.dart'; import '../../../constants/sizes.dart'; +import '../../../enums/save_code_status.dart'; +import '../../../enums/snippet_type.dart'; import '../../../models/unit_content.dart'; import '../state.dart'; import 'complete_unit_button.dart'; import 'hints.dart'; import 'markdown/tob_markdown.dart'; -import 'solution_button.dart'; class UnitContentWidget extends StatelessWidget { final TourNotifier tourNotifier; @@ -82,23 +85,25 @@ class _Content extends StatelessWidget { @override Widget build(BuildContext context) { final content = unitContent; - if (content == null) { return Container(); } - if (content.isChallenge) { - return _ChallengeContent( - tourNotifier: tourNotifier, - unitContent: content, - ); - } + return ListView( children: [ - _Title(title: content.title), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _Title(title: content.title), + _Buttons( + unitContent: content, + tourNotifier: tourNotifier, + ), + ], + ), TobMarkdown( - padding: const EdgeInsets.all( - BeamSizes.size12, - ), + padding: const EdgeInsets.all(BeamSizes.size12), data: content.description, ), ], @@ -130,74 +135,135 @@ class _Title extends StatelessWidget { } } -class _ChallengeContent extends StatelessWidget { +class _Buttons extends StatelessWidget { final TourNotifier tourNotifier; final UnitContentModel unitContent; - const _ChallengeContent({ - required this.unitContent, + const _Buttons({ required this.tourNotifier, + required this.unitContent, }); @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Title(title: unitContent.title), - _ChallengeButtons( - unitContent: unitContent, - tourNotifier: tourNotifier, + final hints = unitContent.hints; + + return Padding( + padding: const EdgeInsets.all(BeamSizes.size10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (hints.isNotEmpty) + HintsWidget( + hints: hints, ), - ], - ), - TobMarkdown( - padding: const EdgeInsets.all(BeamSizes.size12), - data: unitContent.description, - ), - ], + _SnippetTypeSwitcher( + tourNotifier: tourNotifier, + unitContent: unitContent, + ), + ], + ), ); } } -class _ChallengeButtons extends StatelessWidget { +class _SnippetTypeSwitcher extends StatelessWidget { final TourNotifier tourNotifier; final UnitContentModel unitContent; - const _ChallengeButtons({ + const _SnippetTypeSwitcher({ required this.tourNotifier, required this.unitContent, }); - static const _buttonPadding = EdgeInsets.only( - top: BeamSizes.size10, - right: BeamSizes.size10, - ); + Future _setSnippetByType(SnippetType snippetType) async { + await tourNotifier.showSnippetByType(snippetType); + } @override Widget build(BuildContext context) { - final hints = unitContent.hints; + return AnimatedBuilder( + animation: tourNotifier, + builder: (context, child) { + final groupValue = tourNotifier.snippetType; - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (unitContent.isChallenge) - Padding( - padding: _buttonPadding, - child: HintsWidget( - hints: hints, - ), - ), - if (tourNotifier.doesCurrentUnitHaveSolution) - Padding( - padding: _buttonPadding, - child: SolutionButton(tourNotifier: tourNotifier), - ), - ], + return Row( + children: [ + if (tourNotifier.hasSolution) + _SnippetTypeButton( + groupValue: groupValue, + title: 'pages.tour.solution'.tr(), + value: SnippetType.solution, + onChanged: () async { + await _setSnippetByType(SnippetType.solution); + }, + ), + if (tourNotifier.hasSolution || tourNotifier.isCodeSaved) + _SnippetTypeButton( + groupValue: groupValue, + title: unitContent.isChallenge + ? 'pages.tour.assignment'.tr() + : 'pages.tour.example'.tr(), + value: SnippetType.original, + onChanged: () async { + await _setSnippetByType(SnippetType.original); + }, + ), + if (tourNotifier.isCodeSaved) + _SnippetTypeButton( + groupValue: groupValue, + title: tourNotifier.saveCodeStatus == SaveCodeStatus.saving + ? 'pages.tour.saving'.tr() + : 'pages.tour.myCode'.tr(), + value: SnippetType.saved, + onChanged: () async { + await _setSnippetByType(SnippetType.saved); + }, + ), + ], + ); + }, + ); + } +} + +class _SnippetTypeButton extends StatelessWidget { + final SnippetType groupValue; + final VoidCallback onChanged; + final String title; + final SnippetType value; + + const _SnippetTypeButton({ + required this.groupValue, + required this.onChanged, + required this.value, + required this.title, + }); + + @override + Widget build(BuildContext context) { + final isSelected = value == groupValue; + final Color? bgColor; + final Color? fgColor; + final VoidCallback? onPressed; + if (isSelected) { + bgColor = Theme.of(context).splashColor; + fgColor = Theme.of(context).colorScheme.onSurface; + onPressed = null; + } else { + bgColor = null; + fgColor = null; + onPressed = onChanged; + } + + return TextButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(bgColor), + foregroundColor: MaterialStateProperty.all(fgColor), + ), + onPressed: onPressed, + child: Text(title), ); } } 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 3e9341a6ff5f..4b6b5eb5e4b1 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/screen.dart @@ -136,10 +136,10 @@ class _SdkSelection extends StatelessWidget { animation: appNotifier, builder: (context, child) => _SdkButtons( sdks: sdks, - sdkId: appNotifier.sdkId, - setSdkId: (v) => appNotifier.sdkId = v, - onStartPressed: () { - _startTour(appNotifier.sdkId); + groupValue: appNotifier.sdk, + onChanged: (v) => appNotifier.sdk = v, + onStartPressed: () async { + await _startTour(appNotifier.sdk); }, ), ); @@ -153,11 +153,11 @@ class _SdkSelection extends StatelessWidget { ); } - void _startTour(String? sdkId) { - if (sdkId == null) { + Future _startTour(Sdk? sdk) async { + if (sdk == null) { return; } - GetIt.instance.get().push(TourPage(sdkId: sdkId)); + await GetIt.instance.get().push(TourPage(sdk: sdk)); } } @@ -170,8 +170,8 @@ class _TourSummary extends StatelessWidget { return AnimatedBuilder( animation: appNotifier, builder: (context, child) { - final sdkId = appNotifier.sdkId; - if (sdkId == null) { + final sdk = appNotifier.sdk; + if (sdk == null) { return Container(); } @@ -181,7 +181,7 @@ class _TourSummary extends StatelessWidget { horizontal: 27, ), child: ContentTreeBuilder( - sdkId: sdkId, + sdk: sdk, builder: (context, contentTree, child) { if (contentTree == null) { return Container(); @@ -285,14 +285,14 @@ class _IntroTextBody extends StatelessWidget { class _SdkButtons extends StatelessWidget { final List sdks; - final String? sdkId; - final ValueChanged setSdkId; + final Sdk? groupValue; + final ValueChanged onChanged; final VoidCallback onStartPressed; const _SdkButtons({ required this.sdks, - required this.sdkId, - required this.setSdkId, + required this.groupValue, + required this.onChanged, required this.onStartPressed, }); @@ -305,15 +305,15 @@ class _SdkButtons extends StatelessWidget { .map( (sdk) => _SdkButton( title: sdk.title, - value: sdk.id, - groupValue: sdkId, - onChanged: setSdkId, + value: sdk, + groupValue: groupValue, + onChanged: onChanged, ), ) .toList(growable: false), ), ElevatedButton( - onPressed: sdkId == null ? null : onStartPressed, + onPressed: groupValue == null ? null : onStartPressed, child: const Text('pages.welcome.startTour').tr(), ), ], @@ -323,9 +323,9 @@ class _SdkButtons extends StatelessWidget { class _SdkButton extends StatelessWidget { final String title; - final String value; - final String? groupValue; - final ValueChanged onChanged; + final Sdk value; + final Sdk? groupValue; + final ValueChanged onChanged; const _SdkButton({ required this.title, @@ -391,7 +391,10 @@ class _ModuleHeader extends StatelessWidget { padding: const EdgeInsets.all(BeamSizes.size4), child: SvgPicture.asset( Assets.svg.welcomeProgress0, - color: BeamColors.grey4, + colorFilter: const ColorFilter.mode( + BeamColors.grey4, + BlendMode.srcIn, + ), ), ), const SizedBox(width: BeamSizes.size16), diff --git a/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart b/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart index 0b381a995743..b743490a59ca 100644 --- a/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart +++ b/learning/tour-of-beam/frontend/lib/pages/welcome/state.dart @@ -21,7 +21,6 @@ import 'package:flutter/widgets.dart'; import 'path.dart'; class WelcomeNotifier extends ChangeNotifier with PageStateMixin { - // TODO(nausharipov): remove state from Welcome? @override PagePath get path => const WelcomePath(); } diff --git a/learning/tour-of-beam/frontend/lib/repositories/client/client.dart b/learning/tour-of-beam/frontend/lib/repositories/client/client.dart index 66fd4a996316..bbcb217c9c08 100644 --- a/learning/tour-of-beam/frontend/lib/repositories/client/client.dart +++ b/learning/tour-of-beam/frontend/lib/repositories/client/client.dart @@ -16,6 +16,8 @@ * limitations under the License. */ +import 'package:playground_components/playground_components.dart'; + import '../../models/content_tree.dart'; import '../../models/unit_content.dart'; import '../models/get_sdks_response.dart'; @@ -31,4 +33,12 @@ abstract class TobClient { Future getUserProgress(String sdkId); Future postUnitComplete(String sdkId, String id); + + Future postDeleteUserProgress(); + + Future postUserCode({ + required List snippetFiles, + required String sdkId, + required String unitId, + }); } diff --git a/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart b/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart index 8986de435290..9be881abcedd 100644 --- a/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart +++ b/learning/tour-of-beam/frontend/lib/repositories/client/cloud_functions_client.dart @@ -21,6 +21,7 @@ import 'dart:io'; import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; +import 'package:playground_components/playground_components.dart'; import '../../auth/notifier.dart'; import '../../config.dart'; @@ -31,42 +32,76 @@ import '../models/get_sdks_response.dart'; import '../models/get_user_progress_response.dart'; import 'client.dart'; -// TODO(nausharipov): add repository and handle exceptions +enum RequestMethod { + post, + get, +} + class CloudFunctionsTobClient extends TobClient { + Future _makeRequest({ + required String path, + required RequestMethod method, + Map queryParameters = const {}, + dynamic body, + }) async { + final token = await GetIt.instance.get().getToken(); + final uri = Uri.parse('$cloudFunctionsBaseUrl$path') + .replace(queryParameters: queryParameters); + final headers = token != null + ? {HttpHeaders.authorizationHeader: 'Bearer $token'} + : null; + + http.Response response; + switch (method) { + case RequestMethod.post: + response = await http.post( + uri, + headers: headers, + body: body, + ); + break; + case RequestMethod.get: + response = await http.get( + uri, + headers: headers, + ); + break; + } + return jsonDecode(utf8.decode(response.bodyBytes)); + } + @override Future getSdks() async { - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getSdkList', - ), + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getSdkList', ); - - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; return GetSdksResponse.fromJson(map); } @override Future getContentTree(String sdkId) async { - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getContentTree?sdk=$sdkId', - ), + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getContentTree', + queryParameters: { + 'sdk': sdkId, + }, ); - - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; final response = GetContentTreeResponse.fromJson(map); return ContentTreeModel.fromResponse(response); } @override Future getUnitContent(String sdkId, String unitId) async { - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getUnitContent?sdk=$sdkId&id=$unitId', - ), + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getUnitContent', + queryParameters: { + 'sdk': sdkId, + 'id': unitId, + }, ); - - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; return UnitContentModel.fromJson(map); } @@ -76,29 +111,54 @@ class CloudFunctionsTobClient extends TobClient { if (token == null) { return null; } - final json = await http.get( - Uri.parse( - '$cloudFunctionsBaseUrl/getUserProgress?sdk=$sdkId', - ), - headers: { - HttpHeaders.authorizationHeader: 'Bearer $token', + final map = await _makeRequest( + method: RequestMethod.get, + path: 'getUserProgress', + queryParameters: { + 'sdk': sdkId, }, ); - final map = jsonDecode(utf8.decode(json.bodyBytes)) as Map; final response = GetUserProgressResponse.fromJson(map); return response; } @override Future postUnitComplete(String sdkId, String id) async { - final token = await GetIt.instance.get().getToken(); - await http.post( - Uri.parse( - '$cloudFunctionsBaseUrl/postUnitComplete?sdk=$sdkId&id=$id', - ), - headers: { - HttpHeaders.authorizationHeader: 'Bearer $token', + await _makeRequest( + method: RequestMethod.post, + path: 'postUnitComplete', + queryParameters: { + 'sdk': sdkId, + 'id': id, + }, + ); + } + + @override + Future postDeleteUserProgress() async { + await _makeRequest( + method: RequestMethod.post, + path: 'postDeleteProgress', + ); + } + + @override + Future postUserCode({ + required List snippetFiles, + required String sdkId, + required String unitId, + }) async { + await _makeRequest( + path: 'postUserCode', + method: RequestMethod.post, + queryParameters: { + 'sdk': sdkId, + 'id': unitId, }, + body: jsonEncode({ + 'files': snippetFiles.map((file) => file.toJson()).toList(), + 'pipelineOptions': '', + }), ); } } diff --git a/learning/tour-of-beam/frontend/lib/repositories/user_progress/abstract.dart b/learning/tour-of-beam/frontend/lib/repositories/user_progress/abstract.dart new file mode 100644 index 000000000000..9d5aa3311412 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/repositories/user_progress/abstract.dart @@ -0,0 +1,47 @@ +/* + * 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/playground_components.dart'; + +import '../../enums/snippet_type.dart'; +import '../models/get_user_progress_response.dart'; + +abstract class AbstractUserProgressRepository { + Future getUserProgress( + Sdk sdk, + ); + + Future completeUnit( + String sdkId, + String unitId, + ); + + Future saveUnitSnippet({ + required Sdk sdk, + required List snippetFiles, + required SnippetType snippetType, + required String unitId, + }); + + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }); + + Future deleteUserProgress(); +} diff --git a/learning/tour-of-beam/frontend/lib/repositories/user_progress/cloud.dart b/learning/tour-of-beam/frontend/lib/repositories/user_progress/cloud.dart new file mode 100644 index 000000000000..f4722aac00ea --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/repositories/user_progress/cloud.dart @@ -0,0 +1,80 @@ +/* + * 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:playground_components/playground_components.dart'; + +import '../../enums/snippet_type.dart'; +import '../client/client.dart'; +import '../models/get_user_progress_response.dart'; +import 'abstract.dart'; + +class CloudUserProgressRepository extends AbstractUserProgressRepository { + CloudUserProgressRepository({ + required this.client, + }); + final TobClient client; + + @override + Future completeUnit(String sdkId, String unitId) async { + await client.postUnitComplete(sdkId, unitId); + } + + @override + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }) async { + final userProgressResponse = await getUserProgress(sdk); + final unitProgress = userProgressResponse?.units.firstWhereOrNull( + (unit) => unit.id == unitId, + ); + final userSnippetId = unitProgress?.userSnippetId; + if (userSnippetId == null) { + return EmptyExampleLoadingDescriptor(sdk: sdk); + } + return UserSharedExampleLoadingDescriptor( + sdk: sdk, + snippetId: userSnippetId, + ); + } + + @override + Future getUserProgress(Sdk sdk) async { + return client.getUserProgress(sdk.id); + } + + @override + Future saveUnitSnippet({ + required Sdk sdk, + required List snippetFiles, + required String unitId, + required SnippetType? snippetType, + }) async { + await client.postUserCode( + snippetFiles: snippetFiles, + sdkId: sdk.id, + unitId: unitId, + ); + } + + @override + Future deleteUserProgress() async { + await client.postDeleteUserProgress(); + } +} diff --git a/learning/tour-of-beam/frontend/lib/repositories/user_progress/hive.dart b/learning/tour-of-beam/frontend/lib/repositories/user_progress/hive.dart new file mode 100644 index 000000000000..65be2acab583 --- /dev/null +++ b/learning/tour-of-beam/frontend/lib/repositories/user_progress/hive.dart @@ -0,0 +1,140 @@ +/* + * 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:convert'; + +import 'package:get_it/get_it.dart'; +import 'package:hive/hive.dart'; +import 'package:playground_components/playground_components.dart'; + +import '../../cache/sdk.dart'; +import '../../constants/hive_box_names.dart'; +import '../../enums/snippet_type.dart'; +import '../../models/unit_progress.dart'; +import '../models/get_user_progress_response.dart'; +import 'abstract.dart'; + +class HiveUserProgressRepository extends AbstractUserProgressRepository { + @override + Future completeUnit(String sdkId, String unitId) { + throw UnimplementedError(); + } + + @override + Future getSavedDescriptor({ + required Sdk sdk, + required String unitId, + }) async { + try { + final unitProgressBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + final unitProgress = UnitProgressModel.fromJson( + jsonDecode(unitProgressBox.get(unitId)), + ); + return HiveExampleLoadingDescriptor( + boxName: HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.snippets), + sdk: sdk, + snippetId: unitProgress.userSnippetId!, + ); + } on Exception { + return EmptyExampleLoadingDescriptor(sdk: sdk); + } + } + + @override + Future getUserProgress(Sdk sdk) async { + final sdkUnitProgressBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + return GetUserProgressResponse.fromJson({ + // TODO(nausharipov): Replace lambda with tear-off when this lands: https://github.com/dart-lang/language/issues/1813 + 'units': sdkUnitProgressBox.values.map((e) => jsonDecode(e)).toList(), + }); + } + + @override + Future saveUnitSnippet({ + required Sdk sdk, + required List snippetFiles, + required SnippetType snippetType, + required String unitId, + }) async { + final snippetsBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.snippets), + ); + final snippetId = 'local_$unitId'; + + await _saveUnitProgressIfUnsaved( + sdk: sdk, + unitId: unitId, + userSnippetId: snippetId, + ); + + await snippetsBox.put( + snippetId, + jsonEncode( + Example( + files: snippetFiles, + name: 'name', + sdk: sdk, + type: ExampleType.example, + path: 'path', + ).toJson(), + ), + ); + } + + Future _saveUnitProgressIfUnsaved({ + required Sdk sdk, + required String unitId, + required String userSnippetId, + }) async { + final unitProgressBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + final unitProgressEncoded = unitProgressBox.get(unitId); + if (unitProgressEncoded == null) { + await unitProgressBox.put( + unitId, + jsonEncode( + UnitProgressModel( + id: unitId, + isCompleted: false, + userSnippetId: userSnippetId, + ).toJson(), + ), + ); + } + } + + @override + Future deleteUserProgress() async { + final sdks = GetIt.instance.get().getSdks(); + for (final sdk in sdks) { + final unitProgress = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.unitProgress), + ); + final snippetsBox = await Hive.openBox( + HiveBoxNames.getSdkBoxName(sdk, HiveBoxNames.snippets), + ); + await unitProgress.clear(); + await snippetsBox.clear(); + } + } +} diff --git a/learning/tour-of-beam/frontend/lib/state.dart b/learning/tour-of-beam/frontend/lib/state.dart index c67b037d8d92..e690e398faea 100644 --- a/learning/tour-of-beam/frontend/lib/state.dart +++ b/learning/tour-of-beam/frontend/lib/state.dart @@ -25,34 +25,32 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'constants/storage_keys.dart'; class AppNotifier extends ChangeNotifier { - String? _sdkId; + Sdk? _sdk; AppNotifier() { - unawaited(_readSdkId()); + unawaited(_readSdk()); } - // TODO(nausharipov): remove sdkId getter and setter - String? get sdkId => _sdkId; - Sdk? get sdk => Sdk.tryParse(_sdkId); + Sdk? get sdk => _sdk; - set sdkId(String? newValue) { - _sdkId = newValue; - unawaited(_writeSdkId(newValue)); + set sdk(Sdk? newValue) { + _sdk = newValue; + unawaited(_writeSdk(newValue)); notifyListeners(); } - Future _writeSdkId(String? value) async { + Future _writeSdk(Sdk? value) async { final preferences = await SharedPreferences.getInstance(); if (value != null) { - await preferences.setString(StorageKeys.sdkId, value); + await preferences.setString(StorageKeys.sdkId, value.id); } else { await preferences.remove(StorageKeys.sdkId); } } - Future _readSdkId() async { + Future _readSdk() async { final preferences = await SharedPreferences.getInstance(); - _sdkId = preferences.getString(StorageKeys.sdkId); + _sdk = Sdk.tryParse(preferences.getString(StorageKeys.sdkId)); notifyListeners(); } } diff --git a/learning/tour-of-beam/frontend/pubspec.lock b/learning/tour-of-beam/frontend/pubspec.lock index acda06e0dc17..8f638918ce4f 100644 --- a/learning/tour-of-beam/frontend/pubspec.lock +++ b/learning/tour-of-beam/frontend/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "3ff770dfff04a67b0863dff205a0936784de1b87a5e99b11c693fc10e66a9ce3" + sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441" url: "https://pub.dev" source: hosted - version: "1.0.12" + version: "1.0.16" aligned_dialog: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: args - sha256: b003c3098049a51720352d219b0bb5f219b60fbfb68e7a4748139a06a5676515 + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: @@ -85,18 +85,18 @@ packages: dependency: transitive description: name: build - sha256: "29a03af98de60b4eb9136acd56608a54e989f6da238a80af739415b05589d6df" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.1" build_config: dependency: transitive description: name: build_config - sha256: "5b7355c14258f5e7df24bad1566f7b991de3e54aeacfb94e1a65e5233d9739c1" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" build_daemon: dependency: transitive description: @@ -109,26 +109,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "9aae031a54ab0beebc30a888c93e900d15ae2fd8883d031dbfbd5ebdb57f5a4c" + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "56942f8114731d1e79942cd981cfef29501937ff1bccf4dbdce0273f31f13640" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.2.7" built_collection: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: d7a9cd57c215bdf8d502772447aa6b52a8ab3f956d25d5fdea6ef1df2d2dad60 + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "8.4.3" characters: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "43743b95913fd28b95184eb1bed7e4bd85b802b8fad0a52522702dbeda4ee3d5" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.4.0" collection: dependency: "direct main" description: @@ -253,10 +253,10 @@ packages: dependency: transitive description: name: convert - sha256: "196284f26f69444b7f5c50692b55ec25da86d9e500451dc09333bf2e3ad69259" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" coverage: dependency: transitive description: @@ -285,10 +285,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "8aff82f9b26fd868992e5430335a9d773bfef01e1d852d7ba71bf4c5d9349351" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" dartx: dependency: transitive description: @@ -338,13 +338,21 @@ packages: source: hosted version: "0.0.2" enum_map: - dependency: transitive + dependency: "direct main" description: name: enum_map sha256: "0dfe18306d2e9b0e9d381f5e11aac4c8255d5f5eddc68b0ab037f7d00aa36126" url: "https://pub.dev" source: hosted version: "0.2.1" + enum_map_gen: + dependency: "direct dev" + description: + name: enum_map_gen + sha256: "5c99bdd426f4ea457ad6e928eeb44111224881791025cf8afa1921e7a55cf232" + url: "https://pub.dev" + source: hosted + version: "0.2.0" equatable: dependency: "direct main" description: @@ -381,58 +389,58 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: "721b90fe1a0966add31b47a490672954ac4fe45cfe721fd8a11ffbf4c166f611" + sha256: "9907d80446466e638dad31c195150b305dffd145dc57610fcd12c72289432143" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.2.9" firebase_auth_platform_interface: dependency: "direct main" description: name: firebase_auth_platform_interface - sha256: "325d934e21826b3e7030f5018ef61927e2083b4c4fb25218ddef6ffc0012b717" + sha256: c645fec50b0391aa878288f58fa4fe9762c271380c457aedf5c7c9b718604f68 url: "https://pub.dev" source: hosted - version: "6.11.7" + version: "6.11.11" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "1a7fe4aafed9b29229aa1de6910e0631be94633b4a235e739cc2830a0f110361" + sha256: "2dcf2a36852b9091741b4a4047a02e1f2c43a62c6cacec7df573a793a6543e6d" url: "https://pub.dev" source: hosted - version: "5.1.3" + version: "5.2.8" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: c129209ba55f3d4272c89fb4a4994c15bea77fb6de63a82d45fb6bc5c94e4355 + sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.7.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5fab93f5b354648efa62e7cc829c90efb68c8796eecf87e0888cae2d5f3accd4" + sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "18b35ce111b0a4266abf723c825bcf9d4e2519d13638cc7f06f2a8dd960c75bc" + sha256: "291fbcace608aca6c860652e1358ef89752be8cc3ef227f8bbcd1e62775b833a" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.1" fixnum: dependency: transitive description: name: fixnum - sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -442,10 +450,10 @@ packages: dependency: transitive description: name: flutter_code_editor - sha256: a220a7dcd197d793f46f2c2132551128bb41b1ccc08f7afb781223e1b5e400a1 + sha256: "73313c8235b242102af1935312933134774f62c7ed8ad8297beedb88340cc7e1" url: "https://pub.dev" source: hosted - version: "0.2.12" + version: "0.2.18" flutter_driver: dependency: transitive description: flutter @@ -518,18 +526,18 @@ packages: dependency: transitive description: name: fluttertoast - sha256: "7a738eddad04c7b27a1ecfecd12e8ecd4b188cdd2d91c252a02a4aba65838c9d" + sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.2.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "4f4a162323c86ffc1245765cfe138872b8f069deb42f7dbb36115fa27f31469b" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -547,10 +555,10 @@ packages: dependency: transitive description: name: glob - sha256: c51b4fdfee4d281f49b8c957f1add91b815473597f76bcf07377987f66a55729 + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" google_fonts: dependency: "direct main" description: @@ -559,54 +567,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.3" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "5d9af2f1fa192f2629a266d038ee9307b0abe729a4f1b454dd21b414f5e7d381" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - google_sign_in: - dependency: "direct main" - description: - name: google_sign_in - sha256: "4f7177a6116738b0c54230a864f1d44d5d2bbec3e43b4d00c16735e32bb8e8da" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - google_sign_in_android: - dependency: transitive - description: - name: google_sign_in_android - sha256: "41187ee48f8f3f7588cb932a5ab3cc8c83f354d1d50c750f61b240efac1b33d2" - url: "https://pub.dev" - source: hosted - version: "6.1.4" - google_sign_in_ios: - dependency: transitive - description: - name: google_sign_in_ios - sha256: "1116aff5e87f89837b052a81abe6259be7c4dd418275786864d27b74cb2a4e70" - url: "https://pub.dev" - source: hosted - version: "5.5.1" - google_sign_in_platform_interface: - dependency: transitive - description: - name: google_sign_in_platform_interface - sha256: "61306213c76bb8170c3aa20017df296c0131c24d7f6c0cc7e2eeaeac34c9f457" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - google_sign_in_web: - dependency: transitive - description: - name: google_sign_in_web - sha256: a33778787257c348f1ec8f0ab51bc680af7dc224ad7a71fb5a5d49177dca3c49 - url: "https://pub.dev" - source: hosted - version: "0.11.0" googleapis_auth: dependency: transitive description: @@ -619,18 +579,18 @@ packages: dependency: transitive description: name: graphs - sha256: ae0b3d956ff324c6f8671f08dcb2dbd71c99cdbf2aa3ca63a14190c47aa6679c + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" grpc: dependency: transitive description: name: grpc - sha256: "3e8e04c6277059b66d67951143842097e52bbf3f2c6fca2e67d3607b48d5c3ab" + sha256: a73c16e4f6a4a819be892bb2c73cc1d0b00e36095f69b0738cc91a733e3d27ba url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.0" highlight: dependency: transitive description: @@ -640,13 +600,21 @@ packages: source: hosted version: "0.7.0" hive: - dependency: transitive + dependency: "direct main" description: name: hive sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" url: "https://pub.dev" source: hosted version: "2.2.3" + hive_test: + dependency: transitive + description: + name: hive_test + sha256: dd7a5cf0be7af288566a96180b5d07574023777aa947ef252b69046ec36d8eb2 + url: "https://pub.dev" + source: hosted + version: "1.0.1" http: dependency: "direct main" description: @@ -659,10 +627,10 @@ packages: dependency: transitive description: name: http2 - sha256: feb9fbe4790be90fef454eb930368c40ae56df598b3e9b9c10cc216d68f75720 + sha256: "58805ebc6513eed3b98ee0a455a8357e61d187bf2e0fdc1e53120770f78de258" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" http_multi_server: dependency: transitive description: @@ -675,10 +643,10 @@ packages: dependency: transitive description: name: http_parser - sha256: db3060f22889f3d9d55f6a217565486737037eec3609f7f3eca4d0c67ee0d8a0 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" integration_test: dependency: "direct dev" description: flutter @@ -696,10 +664,10 @@ packages: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: @@ -720,12 +688,12 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "581006a34721ff9b9cbc2ba6aab4c81ee9a9f345e9f046f9feef5732417cfe4b" + sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.5.4" keyed_collection_widgets: - dependency: transitive + dependency: "direct main" description: name: keyed_collection_widgets sha256: "9db2df4c4897c35fe167bdca82d307d81baa4161c3118da3f06ab4fd2d75291b" @@ -744,10 +712,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.1" markdown: dependency: "direct main" description: @@ -784,10 +752,10 @@ packages: dependency: transitive description: name: mime - sha256: dab22e92b41aa1255ea90ddc4bc2feaf35544fd0728e209638cad041a6e3928a + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" mocktail: dependency: transitive description: @@ -816,10 +784,10 @@ packages: dependency: transitive description: name: node_preamble - sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" os_detect: dependency: transitive description: @@ -856,58 +824,50 @@ packages: dependency: transitive description: name: path_provider - sha256: "050e8e85e4b7fecdf2bb3682c1c64c4887a183720c802d323de8a5fd76d372dd" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "833c8bcb182b515cd872c113e29aaaffd29a1c720259dd2f65ab35ed5e0db748" + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e url: "https://pub.dev" source: hosted - version: "2.0.17" - path_provider_ios: + version: "2.0.22" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - sha256: "03d639406f5343478352433f00d3c4394d52dac8df3d847869c5e2333e0bbce8" + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 - url: "https://pub.dev" - source: hosted - version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - sha256: "2a97e7fbb7ae9dcd0dfc1220a78e9ec3e71da691912e617e8715ff2a13086ae8" + sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.8" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "27dc7a224fcd07444cb5e0e60423ccacea3e13cf00fc5282ac2c918132da931d" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "999d3dc2ac03ca3f8433018efa40b73558fa4f9759bf8383a217861d120c7d74" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" petitparser: dependency: transitive description: @@ -967,18 +927,18 @@ packages: dependency: "direct main" description: name: provider - sha256: "8d7d4c2df46d6a6270a4e10404bfecb18a937e3e00f710c260d0a10415ce6b7b" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "816c1a640e952d213ddd223b3e7aafae08cd9f8e1f6864eed304cc13b0272b07" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: @@ -987,14 +947,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - quiver: - dependency: transitive + rate_limiter: + dependency: "direct main" description: - name: quiver - sha256: "93982981971e812c94d4a6fa3a57b89f9ec12b38b6380cd3c1370c3b01e4580e" + name: rate_limiter + sha256: "2bae2e961adedf7fc2e8b0305d30e3a3619baf001d050c6907870c5c6235b559" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "1.0.0" rxdart: dependency: transitive description: @@ -1015,50 +975,42 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "76917b7d4b9526b2ba416808a7eb9fb2863c1a09cf63ec85f1453da240fa818a" + sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "853801ce6ba7429ec4e923e37317f32a57c903de50b8c33ffcfbdb7e6f0dd39c" + sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" url: "https://pub.dev" source: hosted - version: "2.0.12" - shared_preferences_ios: + version: "2.0.15" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios - sha256: "585a14cefec7da8c9c2fb8cd283a3bb726b4155c0952afe6a0caaa7b2272de34" + name: shared_preferences_foundation + sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "28aefc1261746e7bad3d09799496054beb84e8c4ffcdfed7734e17b4ada459a5" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - sha256: fbb94bf296576f49be37a1496d5951796211a8db0aa22cc0d68c46440dad808c + sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.3" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "992f0fdc46d0a3c0ac2e5859f2de0e577bbe51f78a77ee8f357cbe626a2ad32d" + sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" shared_preferences_web: dependency: transitive description: @@ -1071,18 +1023,18 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: "97f7ab9a7da96d9cf19581f5de520ceb529548498bd6b5e0ccd02d68a0d15eba" + sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" shelf: dependency: transitive description: name: shelf - sha256: "8ec607599dd0a78931a5114cdac7d609b6dbbf479a38acc9a6dba024b2a30ea0" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" shelf_packages_handler: dependency: transitive description: @@ -1103,10 +1055,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "6db16374bc3497d21aa0eebe674d3db9fdf82082aac0f04dc7b44e4af5b08afc" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -1116,10 +1068,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "85f8c7d6425dff95475db618404732f034c87fe23efe05478cea50520a2517a3" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" url: "https://pub.dev" source: hosted - version: "1.2.5" + version: "1.2.6" source_helper: dependency: transitive description: @@ -1172,10 +1124,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -1228,26 +1180,26 @@ packages: dependency: transitive description: name: time - sha256: "267028bb7b3e87bbfd66876c6389d7101e4b14eb94fe863d3e008e497ca07844" + sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" timing: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" total_lints: dependency: "direct dev" description: name: total_lints - sha256: "8da9ee8d6a8e7c28e5e25bc6f35fb2102eaa7151044d7fabfa11c293ad8b4281" + sha256: "5424a55034e89a9c6198518356842dfdc33b6f0b4d557071f84d6095e5a9f8ea" url: "https://pub.dev" source: hosted - version: "2.17.4" + version: "2.19.0" tuple: dependency: transitive description: @@ -1276,58 +1228,58 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1ccd353c1bff66b49863527c02759f4d06b92744bd9777c96a00ca6a9e8e1d2f" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "6ba7dddee26c9fae27c9203c424631109d73c8fa26cfa7bc3e35e751cb87f62e" + sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.0" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "360fa359ab06bcb4f7c5cd3123a2a9a4d3364d4575d27c4b33468bd4497dd094" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: a9b3ea9043eabfaadfa3fb89de67a11210d85569086d22b3854484beab8b3978 + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "80b860b31a11ebbcbe51b8fe887efc204f3af91522f3b51bcda4622d276d2120" + sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "15fd9dbb306d5efce57dcf62dcb1ae045fbf74079ab4464a950e099bf5800deb" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: e3c3b16d3104260c10eea3b0e34272aaa57921f83148b0619f74c2eced9b7ef1 + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" url_strategy: dependency: "direct main" description: @@ -1388,18 +1340,18 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "3a969ddcc204a3e34e863d204b29c0752716f78b6f9cc8235083208d268a4ccd" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" webdriver: dependency: transitive description: @@ -1420,18 +1372,18 @@ packages: dependency: transitive description: name: win32 - sha256: "6b75ac2ddd42f5c226fdaf4498a2b04071c06f1f2b8f7ab1c3f77cc7f2285ff1" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "3.1.3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "060b6e1c891d956f72b5ac9463466c37cce3fa962a921532fc001e86fe93438e" + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 url: "https://pub.dev" source: hosted - version: "0.2.0+1" + version: "1.0.0" xml: dependency: transitive description: diff --git a/learning/tour-of-beam/frontend/pubspec.yaml b/learning/tour-of-beam/frontend/pubspec.yaml index 0e580d500526..edaec367456c 100644 --- a/learning/tour-of-beam/frontend/pubspec.yaml +++ b/learning/tour-of-beam/frontend/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: easy_localization: ^3.0.1 easy_localization_ext: ^0.1.0 easy_localization_loader: ^1.0.0 + enum_map: ^0.2.1 equatable: ^2.0.5 firebase_auth: ^4.1.1 firebase_auth_platform_interface: ^6.11.7 @@ -41,18 +42,21 @@ dependencies: flutter_svg: ^2.0.1 get_it: ^7.2.0 google_fonts: ^4.0.3 - google_sign_in: ^6.0.0 + hive: ^2.2.3 http: ^0.13.5 json_annotation: ^4.7.0 + keyed_collection_widgets: ^0.4.3 markdown: ^7.0.1 playground_components: { path: ../../../playground/frontend/playground_components } provider: ^6.0.3 + rate_limiter: ^1.0.0 shared_preferences: ^2.0.15 url_launcher: ^6.1.5 url_strategy: ^0.2.0 dev_dependencies: build_runner: ^2.2.0 + enum_map_gen: ^0.2.0 flutter_gen_runner: ^5.2.0 flutter_test: { sdk: flutter } integration_test: { sdk: flutter } diff --git a/playground/frontend/assets/translations/en.yaml b/playground/frontend/assets/translations/en.yaml index d34c40e1bb46..19c85673df23 100644 --- a/playground/frontend/assets/translations/en.yaml +++ b/playground/frontend/assets/translations/en.yaml @@ -24,3 +24,6 @@ intents: showSuggestions: 'Show Suggestions' viewOnGithub: 'View on GitHub' usesEmulatedData: 'This examples uses emulated data' + +ui: + feedbackTitle: 'Enjoying Playground?' diff --git a/playground/frontend/integration_test/common/common_finders.dart b/playground/frontend/integration_test/common/common_finders.dart index 4e4be822544c..051c51167dd7 100644 --- a/playground/frontend/integration_test/common/common_finders.dart +++ b/playground/frontend/integration_test/common/common_finders.dart @@ -30,8 +30,6 @@ import 'package:playground/modules/sdk/components/sdk_selector.dart'; import 'package:playground/modules/sdk/components/sdk_selector_row.dart'; import 'package:playground/modules/shortcuts/components/shortcuts_dialog.dart'; import 'package:playground/pages/standalone_playground/widgets/editor_textarea_wrapper.dart'; -import 'package:playground/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart'; -import 'package:playground/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart'; import 'package:playground/pages/standalone_playground/widgets/more_actions.dart'; import 'package:playground_components/playground_components.dart'; import 'package:playground_components/src/widgets/drag_handle.dart'; @@ -66,24 +64,24 @@ extension CommonFindersExtension on CommonFinders { return byType(ExampleSelector); } - Finder feedbackDropdownCancelButton() { - return find.byKey(FeedbackDropdownContent.cancelButtonKey); + Finder dismissibleOverlay() { + return find.byKey(BeamOverlay.dismissibleAreaKey); } Finder feedbackDropdownContent() { - return byType(FeedbackDropdownContent); + return byType(FeedbackDropdown); } Finder feedbackDropdownSendButton() { - return find.byKey(FeedbackDropdownContent.sendButtonKey); + return find.byKey(FeedbackDropdown.sendButtonKey); } Finder feedbackDropdownTextField() { - return find.byKey(FeedbackDropdownContent.textFieldKey); + return find.byKey(FeedbackDropdown.textFieldKey); } Finder feedbackThumb(FeedbackRating rating) { - return find.byType(FeedbackDropdownIconButton).and( + return find.byType(InkWell).and( find.byKey(Key(rating.name)), ); } diff --git a/playground/frontend/integration_test/initial_urls_test.dart b/playground/frontend/integration_test/initial_urls_test.dart index a62747620890..bb8784dcc0ef 100644 --- a/playground/frontend/integration_test/initial_urls_test.dart +++ b/playground/frontend/integration_test/initial_urls_test.dart @@ -239,7 +239,7 @@ Future _testUserSharedExampleLoader(WidgetTester wt) async { final snippetId = await exampleCache.saveSnippet( files: [SnippetFile(content: content, isMain: false, name: 'name')], sdk: Sdk.go, - pipelineOptions: 'a=b', + pipelineOptions: '--name=value', ); print('Created user-shared example ID: $snippetId'); diff --git a/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart b/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart index c29e2e02281f..02852b4a3bab 100644 --- a/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart +++ b/playground/frontend/integration_test/miscellaneous_ui/feedback_test.dart @@ -52,7 +52,7 @@ Future _checkFeedback( expect(find.feedbackDropdownContent(), findsOneWidget); if (!send) { - await wt.tapAndSettle(find.feedbackDropdownCancelButton()); + await wt.tapAndSettle(find.dismissibleOverlay()); } else { final text = 'This is $rating text.'; await wt.enterText(find.feedbackDropdownTextField(), text); diff --git a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart index 0c671b1b5f43..f0e7f991b298 100644 --- a/playground/frontend/lib/components/dropdown_button/dropdown_button.dart +++ b/playground/frontend/lib/components/dropdown_button/dropdown_button.dart @@ -179,7 +179,7 @@ class _AppDropdownButtonState extends State void _open() { animationController.forward(); dropdown = createDropdown(); - Overlay.of(context)?.insert(dropdown!); + Overlay.of(context).insert(dropdown!); setState(() { isOpen = true; }); diff --git a/playground/frontend/lib/controllers/factories.dart b/playground/frontend/lib/controllers/factories.dart index 396025c6ae48..92863c18bb73 100644 --- a/playground/frontend/lib/controllers/factories.dart +++ b/playground/frontend/lib/controllers/factories.dart @@ -54,7 +54,7 @@ Future _loadExamples( ExamplesLoadingDescriptor descriptor, ) async { try { - await controller.examplesLoader.load(descriptor); + await controller.examplesLoader.loadIfNew(descriptor); } on Exception catch (ex) { PlaygroundComponents.toastNotifier.addException(ex); diff --git a/playground/frontend/lib/modules/examples/example_selector.dart b/playground/frontend/lib/modules/examples/example_selector.dart index 8c86a38e0dcd..9950ceaafeba 100644 --- a/playground/frontend/lib/modules/examples/example_selector.dart +++ b/playground/frontend/lib/modules/examples/example_selector.dart @@ -68,7 +68,7 @@ class _ExampleSelectorState extends State { } else { unawaited(_loadCatalogIfNot(widget.playgroundController)); _overlayEntry = _createExamplesDropdown(); - Overlay.of(context)?.insert(_overlayEntry!); + Overlay.of(context).insert(_overlayEntry!); widget.playgroundController.exampleCache.setSelectorOpened(true); } }, 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 6a807e206938..ce482f07c097 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 @@ -46,7 +46,7 @@ class SetContentMessageHandler extends AbstractMessageHandler { final descriptor = message.descriptor; try { - await playgroundController.examplesLoader.load(descriptor); + await playgroundController.examplesLoader.loadIfNew(descriptor); } on Exception catch (ex) { PlaygroundComponents.toastNotifier.addException(ex); diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart b/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart deleted file mode 100644 index 93591fd637f1..000000000000 --- a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_content.dart +++ /dev/null @@ -1,198 +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 '../../../../constants/font_weight.dart'; -import '../../../../constants/fonts.dart'; -import '../../../../constants/sizes.dart'; -import 'feedback_dropdown_icon_button.dart'; - -const double kTextFieldWidth = 365.0; -const double kTextFieldHeight = 68.0; -const String kFeedbackTitleText = 'Feedback'; -const String kCancelButtonTitle = 'Cancel'; -const String kSendFeedbackButtonTitle = 'Send feedback'; -const String kFeedbackContentText = 'Have feedback? We\'d love to hear it,' - ' but please don\'t share sensitive information.' - '\nHave questions? Try help or support.'; - -class FeedbackDropdownContent extends StatelessWidget { - static const textFieldKey = Key('feedbackTextFieldKey'); - static const cancelButtonKey = Key('cancelButtonKey'); - static const sendButtonKey = Key('sendFeedbackButtonKey'); - - final void Function() close; - final EventSnippetContext eventSnippetContext; - final FeedbackRating feedbackRating; - final TextEditingController textController; - - const FeedbackDropdownContent({ - required this.close, - required this.eventSnippetContext, - required this.feedbackRating, - required this.textController, - }); - - @override - Widget build(BuildContext context) { - final borderColor = - Theme.of(context).extension()!.borderColor; - - final OutlineInputBorder border = OutlineInputBorder( - borderSide: BorderSide(color: borderColor), - borderRadius: BorderRadius.circular(kMdBorderRadius), - ); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: kXlSpacing), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: kXlSpacing), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: kXlSpacing), - child: Text( - kFeedbackTitleText, - style: getTitleFontStyle( - textStyle: const TextStyle( - fontSize: kFeedbackTitleFontSize, - fontWeight: kBoldWeight, - ), - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => close(), - child: const Icon(Icons.close), - ), - ), - ], - ), - Text( - kFeedbackContentText, - style: getTitleFontStyle( - textStyle: const TextStyle( - fontSize: kFeedbackContentFontSize, - fontWeight: kNormalWeight, - ), - ), - ), - Container( - margin: const EdgeInsets.only( - top: kMdSpacing, - bottom: kXlSpacing, - ), - width: kTextFieldWidth, - height: kTextFieldHeight, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kMdBorderRadius), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(kMdBorderRadius), - child: TextFormField( - key: textFieldKey, - controller: textController, - decoration: InputDecoration( - focusedBorder: border, - enabledBorder: border, - contentPadding: const EdgeInsets.all(kMdSpacing), - ), - cursorColor: borderColor, - cursorWidth: kCursorSize, - onFieldSubmitted: (String filterText) {}, - maxLines: 3, - ), - ), - ), - ], - ), - ), - const BeamDivider(), - Padding( - padding: const EdgeInsets.only( - top: kXlSpacing, - left: kXlSpacing, - right: kXlSpacing, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - height: kContainerHeight, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kSmBorderRadius), - border: Border.all( - color: borderColor, - ), - ), - child: TextButton( - key: cancelButtonKey, - onPressed: () { - close(); - textController.clear(); - }, - child: const Text(kCancelButtonTitle), - ), - ), - Container( - margin: const EdgeInsets.only(left: kLgSpacing), - height: kContainerHeight, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(kSmBorderRadius), - ), - child: ElevatedButton( - key: sendButtonKey, - onPressed: () { - if (textController.text.isNotEmpty) { - PlaygroundComponents.analyticsService.sendUnawaited( - FeedbackFormSentAnalyticsEvent( - rating: feedbackRating, - text: textController.text, - snippetContext: eventSnippetContext, - ), - ); - } - close(); - textController.clear(); - }, - child: const Text(kSendFeedbackButtonTitle), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart b/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart deleted file mode 100644 index 8fa794af76d8..000000000000 --- a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/feedback_dropdown_icon_button.dart +++ /dev/null @@ -1,194 +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_components/playground_components.dart'; - -import '../../../../constants/sizes.dart'; -import '../../../../src/assets/assets.gen.dart'; -import 'feedback_dropdown_content.dart'; - -const double kFeedbackTitleFontSize = 24.0; -const double kFeedbackContentFontSize = 14.0; -const double kFeedbackDyBottomAlignment = 50.0; -const double kFeedbackDxLeftAlignment = 10.0; -const double kFeedbackDropdownWidth = 400.0; - -const int kAnimationDurationInMilliseconds = 80; -const Offset kAnimationBeginOffset = Offset(0.0, -0.02); -const Offset kAnimationEndOffset = Offset(0.0, 0.0); - -class FeedbackDropdownIconButton extends StatefulWidget { - final bool isSelected; - final FeedbackRating feedbackRating; - final void Function() onClick; - final PlaygroundController playgroundController; - - const FeedbackDropdownIconButton({ - Key? key, - required this.isSelected, - required this.feedbackRating, - required this.onClick, - required this.playgroundController, - }) : super(key: key); - - @override - State createState() => - _FeedbackDropdownIconButton(); -} - -class _FeedbackDropdownIconButton extends State - with TickerProviderStateMixin { - final GlobalKey feedbackKey = LabeledGlobalKey('FeedbackDropdown'); - final TextEditingController feedbackTextController = TextEditingController(); - late OverlayEntry? dropdown; - late AnimationController animationController; - late Animation offsetAnimation; - bool isOpen = false; - - @override - void initState() { - super.initState(); - animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: kAnimationDurationInMilliseconds), - ); - offsetAnimation = Tween( - begin: kAnimationBeginOffset, - end: kAnimationEndOffset, - ).animate(animationController); - } - - @override - void dispose() { - animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final AppLocalizations appLocale = AppLocalizations.of(context)!; - - final String tooltip; - final String icon; - final String filledIcon; - - switch (widget.feedbackRating) { - case FeedbackRating.positive: - tooltip = appLocale.enjoying; - icon = Assets.thumbUp; - filledIcon = Assets.thumbUpFilled; - break; - case FeedbackRating.negative: - tooltip = appLocale.notEnjoying; - icon = Assets.thumbDown; - filledIcon = Assets.thumbDownFilled; - break; - } - - return Semantics( - container: true, - child: IconButton( - key: feedbackKey, - padding: EdgeInsets.zero, - onPressed: () { - _changeSelectorVisibility(); - widget.onClick(); - }, - tooltip: tooltip, - icon: SvgPicture.asset( - widget.isSelected ? filledIcon : icon, - ), - ), - ); - } - - OverlayEntry createDropdown() { - return OverlayEntry( - builder: (context) { - return Stack( - children: [ - GestureDetector( - onTap: () { - _close(); - }, - child: Container( - color: Colors.transparent, - height: double.infinity, - width: double.infinity, - ), - ), - Positioned( - left: kFeedbackDxLeftAlignment, - bottom: kFeedbackDyBottomAlignment, - child: SlideTransition( - position: offsetAnimation, - child: Material( - elevation: kElevation * 2, - borderRadius: BorderRadius.circular(kMdBorderRadius), - child: Container( - width: kFeedbackDropdownWidth, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(kMdBorderRadius), - ), - child: FeedbackDropdownContent( - close: _close, - eventSnippetContext: - widget.playgroundController.eventSnippetContext, - feedbackRating: widget.feedbackRating, - textController: feedbackTextController, - ), - ), - ), - ), - ), - ], - ); - }, - ); - } - - void _close() { - animationController.reverse(); - dropdown?.remove(); - setState(() { - isOpen = false; - }); - feedbackTextController.clear(); - } - - void _open() { - animationController.forward(); - dropdown = createDropdown(); - Overlay.of(context)?.insert(dropdown!); - setState(() { - isOpen = true; - }); - } - - void _changeSelectorVisibility() { - if (isOpen) { - _close(); - } else { - _open(); - } - } -} diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/playground_feedback.dart b/playground/frontend/lib/pages/standalone_playground/widgets/feedback/playground_feedback.dart deleted file mode 100644 index a80ba4d8cd8a..000000000000 --- a/playground/frontend/lib/pages/standalone_playground/widgets/feedback/playground_feedback.dart +++ /dev/null @@ -1,89 +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_components/playground_components.dart'; -import 'package:provider/provider.dart'; - -import '../../../../constants/font_weight.dart'; -import '../../notifiers/feedback_state.dart'; -import 'feedback_dropdown_icon_button.dart'; - -/// A status bar item for feedback. -class PlaygroundFeedback extends StatelessWidget { - const PlaygroundFeedback({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, playgroundController, child) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context)!.enjoyingPlayground, - style: const TextStyle(fontWeight: kBoldWeight), - ), - FeedbackDropdownIconButton( - key: Key(FeedbackRating.positive.name), - feedbackRating: FeedbackRating.positive, - isSelected: _getFeedbackState(context, true).feedbackRating == - FeedbackRating.positive, - onClick: () => _onRated( - context, - FeedbackRating.positive, - playgroundController, - ), - playgroundController: playgroundController, - ), - FeedbackDropdownIconButton( - key: Key(FeedbackRating.negative.name), - feedbackRating: FeedbackRating.negative, - isSelected: _getFeedbackState(context, true).feedbackRating == - FeedbackRating.negative, - onClick: () => _onRated( - context, - FeedbackRating.negative, - playgroundController, - ), - playgroundController: playgroundController, - ), - ], - ), - ); - } - - void _onRated( - BuildContext context, - FeedbackRating rating, - PlaygroundController playgroundController, - ) { - _getFeedbackState(context, false).setEnjoying(rating); - - PlaygroundComponents.analyticsService.sendUnawaited( - AppRatedAnalyticsEvent( - snippetContext: playgroundController.eventSnippetContext, - rating: rating, - ), - ); - } - - FeedbackState _getFeedbackState(BuildContext context, bool listen) { - return Provider.of(context, listen: listen); - } -} diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart b/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart index 5873499413ad..8af1ab6eca31 100644 --- a/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart +++ b/playground/frontend/lib/pages/standalone_playground/widgets/playground_page_footer.dart @@ -16,12 +16,13 @@ * limitations under the License. */ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:playground_components/playground_components.dart'; import 'package:provider/provider.dart'; import '../../../constants/sizes.dart'; -import 'feedback/playground_feedback.dart'; class PlaygroundPageFooter extends StatelessWidget { const PlaygroundPageFooter({Key? key}) : super(key: key); @@ -43,7 +44,10 @@ class PlaygroundPageFooter extends StatelessWidget { spacing: kXlSpacing, crossAxisAlignment: WrapCrossAlignment.center, children: [ - const PlaygroundFeedback(), + FeedbackWidget( + controller: GetIt.instance.get(), + title: 'ui.feedbackTitle'.tr(), + ), ReportIssueButton(playgroundController: playgroundController), const PrivacyPolicyButton(), const CopyrightWidget(), diff --git a/playground/frontend/playground_components/assets/svg/thumb_down.svg b/playground/frontend/playground_components/assets/svg/thumb_down.svg new file mode 100644 index 000000000000..5abbc3e06fa5 --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_down.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/svg/thumb_down_filled.svg b/playground/frontend/playground_components/assets/svg/thumb_down_filled.svg new file mode 100644 index 000000000000..cfa675696a11 --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_down_filled.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/svg/thumb_up.svg b/playground/frontend/playground_components/assets/svg/thumb_up.svg new file mode 100644 index 000000000000..cf508c90c85e --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_up.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/svg/thumb_up_filled.svg b/playground/frontend/playground_components/assets/svg/thumb_up_filled.svg new file mode 100644 index 000000000000..6540bceeaf4e --- /dev/null +++ b/playground/frontend/playground_components/assets/svg/thumb_up_filled.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/playground/frontend/playground_components/assets/translations/en.yaml b/playground/frontend/playground_components/assets/translations/en.yaml index afdb313aa232..996baed40802 100644 --- a/playground/frontend/playground_components/assets/translations/en.yaml +++ b/playground/frontend/playground_components/assets/translations/en.yaml @@ -15,6 +15,9 @@ # specific language governing permissions and limitations # under the License. +dialogs: + cancel: Cancel + errors: error: 'Error' failedParseOptions: > @@ -43,12 +46,18 @@ intents: reset: 'Reset Code' widgets: - codeEditor: label: 'Code Text Area' closeButton: label: 'Close' + + feedback: + hint: "Have feedback? We'd love to hear it, but please don't share sensitive information." + negative: 'Bad Experience' + positive: 'Good Experience' + send: 'Send Feedback' + title: 'Feedback' output: filter: diff --git a/playground/frontend/playground_components/lib/playground_components.dart b/playground/frontend/playground_components/lib/playground_components.dart index d292b6e7edbc..856a1ba81833 100644 --- a/playground/frontend/playground_components/lib/playground_components.dart +++ b/playground/frontend/playground_components/lib/playground_components.dart @@ -22,8 +22,9 @@ export 'src/constants/analytics.dart'; export 'src/constants/colors.dart'; export 'src/constants/links.dart'; export 'src/constants/sizes.dart'; - +export 'src/controllers/build_metadata.dart'; export 'src/controllers/example_loaders/examples_loader.dart'; +export 'src/controllers/feedback_controller.dart'; export 'src/controllers/playground_controller.dart'; export 'src/controllers/public_notifier.dart'; export 'src/controllers/window_close_notifier/window_close_notifier.dart'; @@ -42,6 +43,7 @@ export 'src/models/example_loading_descriptors/content_example_loading_descripto 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/hive_example_loading_descriptor.dart'; export 'src/models/example_loading_descriptors/http_example_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'; @@ -102,7 +104,10 @@ export 'src/widgets/close_button.dart'; export 'src/widgets/complexity.dart'; export 'src/widgets/copyright.dart'; export 'src/widgets/dialog.dart'; +export 'src/widgets/dialogs/confirm.dart'; +export 'src/widgets/dialogs/progress.dart'; export 'src/widgets/divider.dart'; +export 'src/widgets/feedback.dart'; export 'src/widgets/header_icon_button.dart'; export 'src/widgets/loading_error.dart'; export 'src/widgets/loading_indicator.dart'; @@ -111,8 +116,8 @@ export 'src/widgets/output/output.dart'; export 'src/widgets/output/output_tab.dart'; export 'src/widgets/output/result_tab.dart'; export 'src/widgets/overlay/body.dart'; -export 'src/widgets/overlay/dismissible.dart'; export 'src/widgets/overlay/opener.dart'; +export 'src/widgets/overlay/widget.dart'; export 'src/widgets/reset_button.dart'; export 'src/widgets/run_or_cancel_button.dart'; export 'src/widgets/shortcut_tooltip.dart'; diff --git a/playground/frontend/playground_components/lib/src/assets/assets.gen.dart b/playground/frontend/playground_components/lib/src/assets/assets.gen.dart index 288b595d38ba..981557a7e465 100644 --- a/playground/frontend/playground_components/lib/src/assets/assets.gen.dart +++ b/playground/frontend/playground_components/lib/src/assets/assets.gen.dart @@ -60,8 +60,27 @@ class $AssetsSvgGen { /// File path: assets/svg/drag-vertical.svg String get dragVertical => 'assets/svg/drag-vertical.svg'; + /// File path: assets/svg/thumb_down.svg + String get thumbDown => 'assets/svg/thumb_down.svg'; + + /// File path: assets/svg/thumb_down_filled.svg + String get thumbDownFilled => 'assets/svg/thumb_down_filled.svg'; + + /// File path: assets/svg/thumb_up.svg + String get thumbUp => 'assets/svg/thumb_up.svg'; + + /// File path: assets/svg/thumb_up_filled.svg + String get thumbUpFilled => 'assets/svg/thumb_up_filled.svg'; + /// List of all assets - List get values => [dragHorizontal, dragVertical]; + List get values => [ + dragHorizontal, + dragVertical, + thumbDown, + thumbDownFilled, + thumbUp, + thumbUpFilled + ]; } class $AssetsSymbolsGen { 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 4258cddd0bed..6582ba7ee145 100644 --- a/playground/frontend/playground_components/lib/src/cache/example_cache.dart +++ b/playground/frontend/playground_components/lib/src/cache/example_cache.dart @@ -134,7 +134,8 @@ class ExampleCache extends ChangeNotifier { return Example( complexity: result.complexity, files: result.files, - name: result.files.first.name, + name: 'User Snippet', + isMultiFile: result.files.length > 1, path: id, sdk: result.sdk, pipelineOptions: result.pipelineOptions, diff --git a/playground/frontend/playground_components/lib/src/constants/sizes.dart b/playground/frontend/playground_components/lib/src/constants/sizes.dart index d98ccd044881..8a85dfb61e23 100644 --- a/playground/frontend/playground_components/lib/src/constants/sizes.dart +++ b/playground/frontend/playground_components/lib/src/constants/sizes.dart @@ -43,6 +43,7 @@ class BeamSizes { static const double loadingIndicator = 40; static const double splitViewSeparator = BeamSizes.size8; static const double tabBarHeight = 50; + static const double popupWidth = 420; } class BeamBorderRadius { 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 index 75342c3ec0fe..5a52659caf50 100644 --- 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 @@ -31,6 +31,7 @@ import 'content_example_loader.dart'; import 'empty_example_loader.dart'; import 'example_loader.dart'; import 'example_loader_factory.dart'; +import 'hive_example_loader.dart'; import 'http_example_loader.dart'; import 'standard_example_loader.dart'; import 'user_shared_example_loader.dart'; @@ -44,6 +45,7 @@ class ExamplesLoader { defaultFactory.add(CatalogDefaultExampleLoader.new); defaultFactory.add(ContentExampleLoader.new); defaultFactory.add(EmptyExampleLoader.new); + defaultFactory.add(HiveExampleLoader.new); defaultFactory.add(HttpExampleLoader.new); defaultFactory.add(StandardExampleLoader.new); defaultFactory.add(UserSharedExampleLoader.new); @@ -56,11 +58,14 @@ class ExamplesLoader { /// Loads examples from [descriptor]'s immediate list. /// /// Sets empty editor for SDKs of failed examples. - Future load(ExamplesLoadingDescriptor descriptor) async { + Future loadIfNew(ExamplesLoadingDescriptor descriptor) async { if (_descriptor == descriptor) { return; } + await load(descriptor); + } + Future load(ExamplesLoadingDescriptor descriptor) async { _descriptor = descriptor; final loaders = descriptor.descriptors.map(_createLoader).whereNotNull(); diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/hive_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/hive_example_loader.dart new file mode 100644 index 000000000000..7f394cfa606d --- /dev/null +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/hive_example_loader.dart @@ -0,0 +1,49 @@ +/* + * 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:convert'; + +import 'package:hive/hive.dart'; + +import '../../cache/example_cache.dart'; +import '../../models/example.dart'; +import '../../models/example_loading_descriptors/hive_example_loading_descriptor.dart'; +import '../../models/sdk.dart'; +import 'example_loader.dart'; + +class HiveExampleLoader extends ExampleLoader { + @override + final HiveExampleLoadingDescriptor descriptor; + + final ExampleCache exampleCache; + + HiveExampleLoader({ + required this.descriptor, + required this.exampleCache, + }); + + @override + Sdk? get sdk => descriptor.sdk; + + @override + Future get future async { + final box = await Hive.openBox(descriptor.boxName); + final Map map = jsonDecode(box.get(descriptor.snippetId)); + return Example.fromJson(map); + } +} diff --git a/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart b/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart index 0afb6828f9b7..520791d7c1be 100644 --- a/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart +++ b/playground/frontend/playground_components/lib/src/controllers/example_loaders/standard_example_loader.dart @@ -19,8 +19,8 @@ import 'dart:async'; import '../../cache/example_cache.dart'; +import '../../exceptions/multiple_exceptions.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'; @@ -35,10 +35,9 @@ class StandardExampleLoader extends ExampleLoader { final ExampleCache exampleCache; final _completer = Completer(); - Sdk? _sdk; @override - Sdk? get sdk => _sdk; + Sdk? get sdk => descriptor.sdk; @override Future get future => _completer.future; @@ -52,31 +51,39 @@ class StandardExampleLoader extends ExampleLoader { Future _load() async { try { - final example = await _loadExampleBase(); - - if (example == null) { - _completer.completeError(Exception('Example not found: $descriptor')); - return; - } + final exampleBase = await exampleCache.getPrecompiledObject( + descriptor.path, + descriptor.sdk, + ); _completer.complete( - exampleCache.loadExampleInfo(example), + await exampleCache.loadExampleInfo(exampleBase), + ); + } on Exception catch (ex, trace) { + await _tryLoadSharedExample( + previousExceptions: [ex], + previousStackTraces: [trace], ); - - // ignore: avoid_catches_without_on_clauses - } catch (ex, trace) { - _completer.completeError(ex, trace); - return; } } - Future _loadExampleBase() async { - _sdk = Sdk.tryParseExamplePath(descriptor.path); - - if (_sdk == null) { - return null; + Future _tryLoadSharedExample({ + required List previousExceptions, + required List previousStackTraces, + }) async { + try { + final example = await exampleCache.loadSharedExample( + descriptor.path, + viewOptions: descriptor.viewOptions, + ); + _completer.complete(example); + } on Exception catch (ex, trace) { + _completer.completeError( + MultipleExceptions( + exceptions: [...previousExceptions, ex], + stackTraces: [...previousStackTraces, trace], + ), + ); } - - return exampleCache.getPrecompiledObject(descriptor.path, _sdk!); } } diff --git a/playground/frontend/playground_components/lib/src/controllers/feedback_controller.dart b/playground/frontend/playground_components/lib/src/controllers/feedback_controller.dart new file mode 100644 index 000000000000..d8145ffc0b5c --- /dev/null +++ b/playground/frontend/playground_components/lib/src/controllers/feedback_controller.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/widgets.dart'; + +import '../enums/feedback_rating.dart'; +import '../models/event_snippet_context.dart'; + +class FeedbackController extends ChangeNotifier { + EventSnippetContext? eventSnippetContext; + Map additionalParams; + final textController = TextEditingController(); + + FeedbackController({ + this.eventSnippetContext, + this.additionalParams = const {}, + }); + + FeedbackRating? _rating; + FeedbackRating? get rating => _rating; + + set rating(FeedbackRating? newValue) { + _rating = newValue; + notifyListeners(); + } +} 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 612e9036c7e2..74eb34b673bb 100644 --- a/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart +++ b/playground/frontend/playground_components/lib/src/controllers/playground_controller.dart @@ -42,6 +42,7 @@ import '../services/symbols/symbols_notifier.dart'; import '../util/logical_keyboard_key.dart'; import 'code_runner.dart'; import 'example_loaders/examples_loader.dart'; +import 'feedback_controller.dart'; import 'result_filter_controller.dart'; import 'snippet_editing_controller.dart'; @@ -207,19 +208,17 @@ class PlaygroundController with ChangeNotifier { }) { if (setCurrentSdk) { _sdk = example.sdk; - final controller = _getOrCreateSnippetEditingController( - example.sdk, - loadDefaultIfNot: false, - ); - - controller.setExample(example, descriptor: descriptor); _ensureSymbolsInitialized(); - } else { - final controller = _getOrCreateSnippetEditingController( - example.sdk, - loadDefaultIfNot: false, - ); - controller.setExample(example, descriptor: descriptor); + } + + final controller = _getOrCreateSnippetEditingController( + example.sdk, + loadDefaultIfNot: false, + ); + controller.setExample(example, descriptor: descriptor); + if (example.sdk == _sdk) { + GetIt.instance.get().eventSnippetContext = + controller.eventSnippetContext; } codeRunner.reset(); @@ -231,10 +230,12 @@ class PlaygroundController with ChangeNotifier { bool notify = true, }) { _sdk = sdk; - _getOrCreateSnippetEditingController( + final controller = _getOrCreateSnippetEditingController( sdk, loadDefaultIfNot: true, ); + GetIt.instance.get().eventSnippetContext = + controller.eventSnippetContext; _ensureSymbolsInitialized(); if (notify) { diff --git a/playground/frontend/playground_components/lib/src/exceptions/multiple_exceptions.dart b/playground/frontend/playground_components/lib/src/exceptions/multiple_exceptions.dart new file mode 100644 index 000000000000..e3fffb8c31ff --- /dev/null +++ b/playground/frontend/playground_components/lib/src/exceptions/multiple_exceptions.dart @@ -0,0 +1,42 @@ +/* + * 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 MultipleExceptions implements Exception { + final List exceptions; + final List stackTraces; + + MultipleExceptions({ + required this.exceptions, + required this.stackTraces, + }); + + @override + String toString() { + final buffer = StringBuffer('Exceptions (${exceptions.length}): '); + for (var i = 0; i < exceptions.length; i++) { + buffer + ..write('Exception #') + ..write(i + 1) + ..writeln(':') + ..writeln(exceptions[i]) + ..writeln('StackTrace:') + ..writeln(stackTraces[i]); + } + return buffer.toString(); + } +} diff --git a/playground/frontend/playground_components/lib/src/locator.dart b/playground/frontend/playground_components/lib/src/locator.dart index c7e28bef6e67..f741383f31ef 100644 --- a/playground/frontend/playground_components/lib/src/locator.dart +++ b/playground/frontend/playground_components/lib/src/locator.dart @@ -19,11 +19,13 @@ import 'package:get_it/get_it.dart'; import 'controllers/build_metadata.dart'; +import 'controllers/feedback_controller.dart'; import 'services/symbols/symbols_notifier.dart'; import 'services/toast_notifier.dart'; Future initializeServiceLocator() async { GetIt.instance.registerSingleton(BuildMetadataController()); + GetIt.instance.registerSingleton(FeedbackController()); GetIt.instance.registerSingleton(SymbolsNotifier()); GetIt.instance.registerSingleton(ToastNotifier()); } diff --git a/playground/frontend/playground_components/lib/src/models/dataset.dart b/playground/frontend/playground_components/lib/src/models/dataset.dart index 5ec79d29112e..b51ba85ed1c6 100644 --- a/playground/frontend/playground_components/lib/src/models/dataset.dart +++ b/playground/frontend/playground_components/lib/src/models/dataset.dart @@ -16,8 +16,13 @@ * limitations under the License. */ +import 'package:json_annotation/json_annotation.dart'; + import '../enums/emulator_type.dart'; +part 'dataset.g.dart'; + +@JsonSerializable() class Dataset { final EmulatorType? type; final Map options; @@ -28,4 +33,9 @@ class Dataset { required this.options, required this.datasetPath, }); + + factory Dataset.fromJson(Map json) => + _$DatasetFromJson(json); + + Map toJson() => _$DatasetToJson(this); } diff --git a/playground/frontend/playground_components/lib/src/models/dataset.g.dart b/playground/frontend/playground_components/lib/src/models/dataset.g.dart new file mode 100644 index 000000000000..85256b1d5d83 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/dataset.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dataset.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Dataset _$DatasetFromJson(Map json) => Dataset( + type: $enumDecodeNullable(_$EmulatorTypeEnumMap, json['type']), + options: Map.from(json['options'] as Map), + datasetPath: json['datasetPath'] as String, + ); + +Map _$DatasetToJson(Dataset instance) => { + 'type': _$EmulatorTypeEnumMap[instance.type], + 'options': instance.options, + 'datasetPath': instance.datasetPath, + }; + +const _$EmulatorTypeEnumMap = { + EmulatorType.kafka: 'kafka', +}; diff --git a/playground/frontend/playground_components/lib/src/models/example.dart b/playground/frontend/playground_components/lib/src/models/example.dart index 97ad54bf8b1c..85858e875717 100644 --- a/playground/frontend/playground_components/lib/src/models/example.dart +++ b/playground/frontend/playground_components/lib/src/models/example.dart @@ -16,11 +16,19 @@ * limitations under the License. */ +import 'package:json_annotation/json_annotation.dart'; + +import '../enums/complexity.dart'; +import 'dataset.dart'; import 'example_base.dart'; +import 'example_view_options.dart'; import 'sdk.dart'; import 'snippet_file.dart'; +part 'example.g.dart'; + /// A [ExampleBase] that also has all large fields fetched. +@JsonSerializable() class Example extends ExampleBase { final List files; final String? graph; @@ -48,6 +56,12 @@ class Example extends ExampleBase { super.viewOptions, }); + factory Example.fromJson(Map json) => + _$ExampleFromJson(json); + + @override + Map toJson() => _$ExampleToJson(this); + Example.fromBase( ExampleBase example, { required this.files, diff --git a/playground/frontend/playground_components/lib/src/models/example.g.dart b/playground/frontend/playground_components/lib/src/models/example.g.dart new file mode 100644 index 000000000000..f46c32c1d0d4 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example.g.dart @@ -0,0 +1,72 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'example.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Example _$ExampleFromJson(Map json) => Example( + files: (json['files'] as List) + .map((e) => SnippetFile.fromJson(e as Map)) + .toList(), + name: json['name'] as String, + sdk: Sdk.fromJson(json['sdk'] as Map), + type: $enumDecode(_$ExampleTypeEnumMap, json['type']), + path: json['path'] as String, + complexity: $enumDecodeNullable(_$ComplexityEnumMap, json['complexity']), + contextLine: json['contextLine'] as int? ?? 1, + datasets: (json['datasets'] as List?) + ?.map((e) => Dataset.fromJson(e as Map)) + .toList() ?? + const [], + description: json['description'] as String? ?? '', + graph: json['graph'] as String?, + isMultiFile: json['isMultiFile'] as bool? ?? false, + logs: json['logs'] as String?, + outputs: json['outputs'] as String?, + pipelineOptions: json['pipelineOptions'] as String? ?? '', + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + urlNotebook: json['urlNotebook'] as String?, + urlVcs: json['urlVcs'] as String?, + viewOptions: json['viewOptions'] == null + ? ExampleViewOptions.empty + : ExampleViewOptions.fromJson( + json['viewOptions'] as Map), + ); + +Map _$ExampleToJson(Example instance) => { + 'complexity': _$ComplexityEnumMap[instance.complexity], + 'contextLine': instance.contextLine, + 'datasets': instance.datasets, + 'description': instance.description, + 'isMultiFile': instance.isMultiFile, + 'name': instance.name, + 'path': instance.path, + 'pipelineOptions': instance.pipelineOptions, + 'sdk': instance.sdk, + 'tags': instance.tags, + 'type': _$ExampleTypeEnumMap[instance.type]!, + 'urlNotebook': instance.urlNotebook, + 'urlVcs': instance.urlVcs, + 'viewOptions': instance.viewOptions, + 'files': instance.files, + 'graph': instance.graph, + 'logs': instance.logs, + 'outputs': instance.outputs, + }; + +const _$ExampleTypeEnumMap = { + ExampleType.all: 'all', + ExampleType.example: 'example', + ExampleType.kata: 'kata', + ExampleType.test: 'test', +}; + +const _$ComplexityEnumMap = { + Complexity.basic: 'BASIC', + Complexity.medium: 'MEDIUM', + Complexity.advanced: 'ADVANCED', +}; 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 908c02a16291..25476de300ce 100644 --- a/playground/frontend/playground_components/lib/src/models/example_base.dart +++ b/playground/frontend/playground_components/lib/src/models/example_base.dart @@ -17,6 +17,7 @@ */ import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; import '../enums/complexity.dart'; import '../repositories/example_repository.dart'; @@ -24,6 +25,8 @@ import 'dataset.dart'; import 'example_view_options.dart'; import 'sdk.dart'; +part 'example_base.g.dart'; + enum ExampleType { all, example, @@ -49,6 +52,7 @@ 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]. +@JsonSerializable() class ExampleBase with Comparable, EquatableMixin { final bool alwaysRun; final Complexity? complexity; @@ -86,6 +90,11 @@ class ExampleBase with Comparable, EquatableMixin { this.viewOptions = ExampleViewOptions.empty, }); + factory ExampleBase.fromJson(Map json) => + _$ExampleBaseFromJson(json); + + Map toJson() => _$ExampleBaseToJson(this); + // TODO(alexeyinkin): Use all fields, https://github.com/apache/beam/issues/23979 @override List get props => [path]; diff --git a/playground/frontend/playground_components/lib/src/models/example_base.g.dart b/playground/frontend/playground_components/lib/src/models/example_base.g.dart new file mode 100644 index 000000000000..0f18242faf2c --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_base.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'example_base.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ExampleBase _$ExampleBaseFromJson(Map json) => ExampleBase( + name: json['name'] as String, + path: json['path'] as String, + sdk: Sdk.fromJson(json['sdk'] as Map), + type: $enumDecode(_$ExampleTypeEnumMap, json['type']), + complexity: $enumDecodeNullable(_$ComplexityEnumMap, json['complexity']), + contextLine: json['contextLine'] as int? ?? 1, + datasets: (json['datasets'] as List?) + ?.map((e) => Dataset.fromJson(e as Map)) + .toList() ?? + const [], + description: json['description'] as String? ?? '', + isMultiFile: json['isMultiFile'] as bool? ?? false, + pipelineOptions: json['pipelineOptions'] as String? ?? '', + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + urlNotebook: json['urlNotebook'] as String?, + urlVcs: json['urlVcs'] as String?, + viewOptions: json['viewOptions'] == null + ? ExampleViewOptions.empty + : ExampleViewOptions.fromJson( + json['viewOptions'] as Map), + ); + +Map _$ExampleBaseToJson(ExampleBase instance) => + { + 'complexity': _$ComplexityEnumMap[instance.complexity], + 'contextLine': instance.contextLine, + 'datasets': instance.datasets, + 'description': instance.description, + 'isMultiFile': instance.isMultiFile, + 'name': instance.name, + 'path': instance.path, + 'pipelineOptions': instance.pipelineOptions, + 'sdk': instance.sdk, + 'tags': instance.tags, + 'type': _$ExampleTypeEnumMap[instance.type]!, + 'urlNotebook': instance.urlNotebook, + 'urlVcs': instance.urlVcs, + 'viewOptions': instance.viewOptions, + }; + +const _$ExampleTypeEnumMap = { + ExampleType.all: 'all', + ExampleType.example: 'example', + ExampleType.kata: 'kata', + ExampleType.test: 'test', +}; + +const _$ComplexityEnumMap = { + Complexity.basic: 'BASIC', + Complexity.medium: 'MEDIUM', + Complexity.advanced: 'ADVANCED', +}; diff --git a/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart new file mode 100644 index 000000000000..f69a9e505914 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart @@ -0,0 +1,78 @@ +/* + * 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_view_options.dart'; +import '../sdk.dart'; +import 'example_loading_descriptor.dart'; + +/// Describes a loadable example saved in Hive. +class HiveExampleLoadingDescriptor extends ExampleLoadingDescriptor { + final String boxName; + final Sdk sdk; + final String snippetId; + + const HiveExampleLoadingDescriptor({ + required this.boxName, + required this.sdk, + required this.snippetId, + super.viewOptions, + }); + + @override + List get props => [ + boxName, + sdk.id, + snippetId, + viewOptions, + ]; + + @override + HiveExampleLoadingDescriptor copyWithoutViewOptions() => + HiveExampleLoadingDescriptor( + boxName: boxName, + sdk: sdk, + snippetId: snippetId, + ); + + @override + Map toJson() => { + 'boxName': boxName, + 'sdk': sdk.id, + 'shared': snippetId, + ...viewOptions.toShortMap(), + }; + + static HiveExampleLoadingDescriptor? tryParse( + Map map, + ) { + final boxName = map['boxName']; + final sdkId = map['sdk']; + final snippetId = map['shared']; + + if (sdkId == null || snippetId == null) { + return null; + } + + return HiveExampleLoadingDescriptor( + boxName: boxName, + sdk: Sdk.parseOrCreate(sdkId), + snippetId: snippetId, + viewOptions: ExampleViewOptions.fromShortMap(map), + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/models/example_view_options.dart b/playground/frontend/playground_components/lib/src/models/example_view_options.dart index be1d15753417..54ced794fc20 100644 --- a/playground/frontend/playground_components/lib/src/models/example_view_options.dart +++ b/playground/frontend/playground_components/lib/src/models/example_view_options.dart @@ -17,9 +17,13 @@ */ import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; import '../util/string.dart'; +part 'example_view_options.g.dart'; + +@JsonSerializable() class ExampleViewOptions with EquatableMixin { final bool foldCommentAtLineZero; final bool foldImports; @@ -35,6 +39,16 @@ class ExampleViewOptions with EquatableMixin { this.foldImports = true, }); + /// Parses a fully normalized map. + factory ExampleViewOptions.fromJson(Map json) => + _$ExampleViewOptionsFromJson(json); + + Map toJson() => _$ExampleViewOptionsToJson(this); + + /// Parses a simplified map that comes from a URL. + /// + /// This map has CSV strings instead of JSON arrays + /// and cannot override folding parameters' defaults. factory ExampleViewOptions.fromShortMap(Map map) { return ExampleViewOptions( readOnlySectionNames: _split(map['readonly']), diff --git a/playground/frontend/playground_components/lib/src/models/example_view_options.g.dart b/playground/frontend/playground_components/lib/src/models/example_view_options.g.dart new file mode 100644 index 000000000000..e0b8d580862b --- /dev/null +++ b/playground/frontend/playground_components/lib/src/models/example_view_options.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'example_view_options.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ExampleViewOptions _$ExampleViewOptionsFromJson(Map json) => + ExampleViewOptions( + readOnlySectionNames: (json['readOnlySectionNames'] as List) + .map((e) => e as String) + .toList(), + showSectionNames: (json['showSectionNames'] as List) + .map((e) => e as String) + .toList(), + unfoldSectionNames: (json['unfoldSectionNames'] as List) + .map((e) => e as String) + .toList(), + foldCommentAtLineZero: json['foldCommentAtLineZero'] as bool? ?? true, + foldImports: json['foldImports'] as bool? ?? true, + ); + +Map _$ExampleViewOptionsToJson(ExampleViewOptions instance) => + { + 'foldCommentAtLineZero': instance.foldCommentAtLineZero, + 'foldImports': instance.foldImports, + 'readOnlySectionNames': instance.readOnlySectionNames, + 'showSectionNames': instance.showSectionNames, + 'unfoldSectionNames': instance.unfoldSectionNames, + }; diff --git a/playground/frontend/playground_components/lib/src/models/sdk.dart b/playground/frontend/playground_components/lib/src/models/sdk.dart index f05f921cb32b..fa821059e6db 100644 --- a/playground/frontend/playground_components/lib/src/models/sdk.dart +++ b/playground/frontend/playground_components/lib/src/models/sdk.dart @@ -27,7 +27,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'sdk.g.dart'; -@JsonSerializable(createToJson: false) +@JsonSerializable() class Sdk with EquatableMixin { final String id; final String title; @@ -117,6 +117,7 @@ class Sdk with EquatableMixin { Mode? get highlightMode => _idToHighlightMode[id]; - factory Sdk.fromJson(Map json) => - _$SdkFromJson(json); + factory Sdk.fromJson(Map json) => _$SdkFromJson(json); + + Map toJson() => _$SdkToJson(this); } diff --git a/playground/frontend/playground_components/lib/src/models/sdk.g.dart b/playground/frontend/playground_components/lib/src/models/sdk.g.dart index 63b1d0978fc6..d43ed04f63a7 100644 --- a/playground/frontend/playground_components/lib/src/models/sdk.g.dart +++ b/playground/frontend/playground_components/lib/src/models/sdk.g.dart @@ -10,3 +10,8 @@ Sdk _$SdkFromJson(Map json) => Sdk( id: json['id'] as String, title: json['title'] as String, ); + +Map _$SdkToJson(Sdk instance) => { + 'id': instance.id, + 'title': instance.title, + }; diff --git a/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart b/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart index a8b5e36c90e8..14461e8c8732 100644 --- a/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart +++ b/playground/frontend/playground_components/lib/src/services/analytics/events/app_rated.dart @@ -25,6 +25,7 @@ class AppRatedAnalyticsEvent extends AnalyticsEventWithSnippetContext { const AppRatedAnalyticsEvent({ required this.rating, required super.snippetContext, + super.additionalParams, }) : super( name: BeamAnalyticsEvents.appRated, ); diff --git a/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart b/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart index c3ae44519b13..65756a15d1b7 100644 --- a/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart +++ b/playground/frontend/playground_components/lib/src/services/analytics/events/feedback_form_sent.dart @@ -28,6 +28,7 @@ class FeedbackFormSentAnalyticsEvent extends AnalyticsEventWithSnippetContext { required this.rating, required this.text, required super.snippetContext, + super.additionalParams, }) : super( name: BeamAnalyticsEvents.feedbackFormSent, ); diff --git a/playground/frontend/playground_components/lib/src/theme/theme.dart b/playground/frontend/playground_components/lib/src/theme/theme.dart index dee3c25f25d6..9709b21492f4 100644 --- a/playground/frontend/playground_components/lib/src/theme/theme.dart +++ b/playground/frontend/playground_components/lib/src/theme/theme.dart @@ -162,6 +162,7 @@ class BeamThemeExtension extends ThemeExtension { final kLightTheme = ThemeData( brightness: Brightness.light, appBarTheme: _getAppBarTheme(BeamLightThemeColors.secondaryBackground), + // TODO(nausharipov): Migrate to Material 3: https://github.com/apache/beam/issues/24610 backgroundColor: BeamLightThemeColors.primaryBackground, canvasColor: BeamLightThemeColors.primaryBackground, dividerColor: BeamLightThemeColors.grey, diff --git a/playground/frontend/playground_components/lib/src/widgets/dialogs/confirm.dart b/playground/frontend/playground_components/lib/src/widgets/dialogs/confirm.dart new file mode 100644 index 000000000000..7f5edeb3b1be --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/dialogs/confirm.dart @@ -0,0 +1,102 @@ +/* + * 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 '../../../playground_components.dart'; + +class ConfirmDialog extends StatelessWidget { + final String confirmButtonText; + + final String title; + final String? subtitle; + + const ConfirmDialog({ + required this.confirmButtonText, + required this.title, + this.subtitle, + }); + + static Future show({ + required BuildContext context, + required String title, + required String confirmButtonText, + String? subtitle, + }) async { + return await showDialog( + context: context, + builder: (context) => ConfirmDialog( + confirmButtonText: confirmButtonText, + title: title, + subtitle: subtitle, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: OverlayBody( + child: Container( + width: BeamSizes.popupWidth, + padding: const EdgeInsets.all(BeamSizes.size16), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // + Text( + title, + style: Theme.of(context).textTheme.headlineMedium, + ), + if (subtitle != null) + Padding( + padding: const EdgeInsets.only(top: BeamSizes.size8), + child: Text(subtitle!), + ), + + const SizedBox(height: BeamSizes.size8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('dialogs.cancel').tr(), + ), + const SizedBox(width: BeamSizes.size8), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: Text(confirmButtonText), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/solution_button.dart b/playground/frontend/playground_components/lib/src/widgets/dialogs/progress.dart similarity index 53% rename from learning/tour-of-beam/frontend/lib/pages/tour/widgets/solution_button.dart rename to playground/frontend/playground_components/lib/src/widgets/dialogs/progress.dart index 5318e6514288..4f513a783bd4 100644 --- a/learning/tour-of-beam/frontend/lib/pages/tour/widgets/solution_button.dart +++ b/playground/frontend/playground_components/lib/src/widgets/dialogs/progress.dart @@ -16,35 +16,43 @@ * limitations under the License. */ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; +import 'dart:async'; -import '../../../assets/assets.gen.dart'; -import '../state.dart'; +import 'package:flutter/material.dart'; -class SolutionButton extends StatelessWidget { - final TourNotifier tourNotifier; +class ProgressDialog extends StatelessWidget { + const ProgressDialog(); - const SolutionButton({ - required this.tourNotifier, - }); + /// Shows a dialog with [CircularProgressIndicator] until [future] completes. + static void show({ + required Future future, + required GlobalKey navigatorKey, + }) { + var shown = true; + unawaited( + showDialog( + barrierDismissible: false, + context: navigatorKey.currentContext!, + builder: (_) => const ProgressDialog(), + ).whenComplete(() { + shown = false; + }), + ); + unawaited( + future.whenComplete(() { + if (shown) { + navigatorKey.currentState!.pop(); + } + }), + ); + } @override Widget build(BuildContext context) { - return AnimatedBuilder( - animation: tourNotifier, - builder: (context, child) => TextButton.icon( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - tourNotifier.isShowingSolution - ? Theme.of(context).splashColor - : null, - ), - ), - onPressed: tourNotifier.toggleShowingSolution, - icon: SvgPicture.asset(Assets.svg.solution), - label: const Text('ui.solution').tr(), + return const Dialog( + backgroundColor: Colors.transparent, + child: Center( + child: CircularProgressIndicator(), ), ); } diff --git a/playground/frontend/playground_components/lib/src/widgets/feedback.dart b/playground/frontend/playground_components/lib/src/widgets/feedback.dart new file mode 100644 index 000000000000..b4d83c2ad7d7 --- /dev/null +++ b/playground/frontend/playground_components/lib/src/widgets/feedback.dart @@ -0,0 +1,223 @@ +/* + * 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 '../../playground_components.dart'; +import '../assets/assets.gen.dart'; + +class FeedbackWidget extends StatelessWidget { + static const positiveRatingButtonKey = Key('positive'); + static const negativeRatingButtonKey = Key('negative'); + + final FeedbackController controller; + final String title; + + const FeedbackWidget({ + required this.controller, + required this.title, + }); + + void _onRatingChanged(BuildContext context, FeedbackRating rating) { + controller.rating = rating; + + PlaygroundComponents.analyticsService.sendUnawaited( + AppRatedAnalyticsEvent( + rating: rating, + snippetContext: controller.eventSnippetContext, + additionalParams: controller.additionalParams, + ), + ); + + final closeNotifier = PublicNotifier(); + showOverlay( + context: context, + closeNotifier: closeNotifier, + positioned: Positioned( + bottom: 50, + left: 20, + child: OverlayBody( + child: FeedbackDropdown( + close: closeNotifier.notifyPublic, + controller: controller, + rating: rating, + title: 'widgets.feedback.title'.tr(), + subtitle: 'widgets.feedback.hint'.tr(), + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller, + builder: (context, child) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(width: BeamSizes.size6), + Tooltip( + message: 'widgets.feedback.positive'.tr(), + child: InkWell( + key: positiveRatingButtonKey, + onTap: () { + _onRatingChanged(context, FeedbackRating.positive); + }, + child: _RatingIcon( + groupValue: controller.rating, + value: FeedbackRating.positive, + ), + ), + ), + const SizedBox(width: BeamSizes.size6), + Tooltip( + message: 'widgets.feedback.negative'.tr(), + child: InkWell( + key: negativeRatingButtonKey, + onTap: () { + _onRatingChanged(context, FeedbackRating.negative); + }, + child: _RatingIcon( + groupValue: controller.rating, + value: FeedbackRating.negative, + ), + ), + ), + ], + ), + ); + } +} + +class _RatingIcon extends StatelessWidget { + final FeedbackRating? groupValue; + final FeedbackRating value; + const _RatingIcon({ + required this.groupValue, + required this.value, + }); + + String _getAsset() { + final isSelected = value == groupValue; + switch (value) { + case FeedbackRating.positive: + return isSelected ? Assets.svg.thumbUpFilled : Assets.svg.thumbUp; + case FeedbackRating.negative: + return isSelected ? Assets.svg.thumbDownFilled : Assets.svg.thumbDown; + } + } + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + _getAsset(), + package: PlaygroundComponents.packageName, + ); + } +} + +class FeedbackDropdown extends StatelessWidget { + static const sendButtonKey = Key('sendFeedbackButtonKey'); + static const textFieldKey = Key('feedbackTextFieldKey'); + + final FeedbackController controller; + final VoidCallback close; + final FeedbackRating rating; + final String title; + final String subtitle; + + const FeedbackDropdown({ + required this.controller, + required this.title, + required this.rating, + required this.close, + required this.subtitle, + }); + + void _sendFeedback() { + PlaygroundComponents.analyticsService.sendUnawaited( + FeedbackFormSentAnalyticsEvent( + rating: rating, + text: controller.textController.text, + snippetContext: controller.eventSnippetContext, + additionalParams: controller.additionalParams, + ), + ); + controller.textController.clear(); + close(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: controller.textController, + builder: (context, child) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(16), + width: 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: Theme.of(context).textTheme.headlineLarge, + ), + const SizedBox(height: BeamSizes.size8), + Text( + subtitle, + ), + const SizedBox(height: BeamSizes.size8), + TextField( + key: textFieldKey, + controller: controller.textController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.multiline, + maxLines: 5, + minLines: 3, + ), + const SizedBox(height: BeamSizes.size8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + key: sendButtonKey, + onPressed: controller.textController.text.isEmpty + ? null + : _sendFeedback, + child: const Text('widgets.feedback.send').tr(), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart b/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart index cb4e107f5f02..a7910261ae90 100644 --- a/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart +++ b/playground/frontend/playground_components/lib/src/widgets/overlay/opener.dart @@ -19,21 +19,23 @@ import 'package:flutter/material.dart'; import '../../controllers/public_notifier.dart'; -import 'dismissible.dart'; +import 'widget.dart'; -void openOverlay({ +void showOverlay({ required BuildContext context, required PublicNotifier closeNotifier, required Positioned positioned, + bool barrierDismissible = true, }) { final overlay = OverlayEntry( builder: (context) { - return DismissibleOverlay( + return BeamOverlay( close: closeNotifier.notifyPublic, + isDismissible: barrierDismissible, child: positioned, ); }, ); closeNotifier.addListener(overlay.remove); - Overlay.of(context)?.insert(overlay); + Overlay.of(context).insert(overlay); } diff --git a/playground/frontend/playground_components/lib/src/widgets/overlay/dismissible.dart b/playground/frontend/playground_components/lib/src/widgets/overlay/widget.dart similarity index 74% rename from playground/frontend/playground_components/lib/src/widgets/overlay/dismissible.dart rename to playground/frontend/playground_components/lib/src/widgets/overlay/widget.dart index e32e55c56a71..b4af931aed5b 100644 --- a/playground/frontend/playground_components/lib/src/widgets/overlay/dismissible.dart +++ b/playground/frontend/playground_components/lib/src/widgets/overlay/widget.dart @@ -18,12 +18,16 @@ import 'package:flutter/material.dart'; -class DismissibleOverlay extends StatelessWidget { +class BeamOverlay extends StatelessWidget { + static const dismissibleAreaKey = Key('overlayDismissibleAreaKey'); + final VoidCallback close; + final bool isDismissible; final Positioned child; - const DismissibleOverlay({ + const BeamOverlay({ required this.close, + required this.isDismissible, required this.child, }); @@ -31,11 +35,13 @@ class DismissibleOverlay extends StatelessWidget { Widget build(BuildContext context) { return Stack( children: [ - Positioned.fill( - child: GestureDetector( - onTap: close, + if (isDismissible) + Positioned.fill( + child: GestureDetector( + key: dismissibleAreaKey, + onTap: close, + ), ), - ), child, ], ); 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 index baf8dcfd1d57..444a33e74363 100644 --- 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 @@ -43,7 +43,6 @@ class RunOrCancelButton extends StatelessWidget { Widget build(BuildContext context) { return RunButton( playgroundController: playgroundController, - isEnabled: !(playgroundController.selectedExample?.isMultiFile ?? false), cancelRun: () async { beforeCancel?.call(playgroundController.codeRunner); await playgroundController.codeRunner.cancelRun().catchError( diff --git a/playground/frontend/playground_components/pubspec.yaml b/playground/frontend/playground_components/pubspec.yaml index f82a0803cc33..52a45c64de1f 100644 --- a/playground/frontend/playground_components/pubspec.yaml +++ b/playground/frontend/playground_components/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: enum_map: ^0.2.1 equatable: ^2.0.5 flutter: { sdk: flutter } - flutter_code_editor: ^0.2.13 + flutter_code_editor: ^0.2.14 flutter_markdown: ^0.6.12 flutter_svg: ^2.0.1 fluttertoast: ^8.1.1 @@ -44,6 +44,8 @@ dependencies: google_fonts: ^4.0.3 grpc: ^3.0.2 highlight: ^0.7.0 + hive: ^2.2.3 + hive_test: ^1.0.1 http: ^0.13.5 json_annotation: ^4.7.0 keyed_collection_widgets: ^0.4.3 diff --git a/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart b/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart index 705d2961946f..67f20cf13fb0 100644 --- a/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart +++ b/playground/frontend/playground_components/test/src/controllers/example_loaders/examples_loader_test.dart @@ -95,7 +95,7 @@ void main() async { }, ); - await examplesLoader.load(descriptor); + await examplesLoader.loadIfNew(descriptor); expect(setExampleTrue, [Sdk.go.id, Sdk.python.id]); expect(setExampleFalse, []); @@ -113,7 +113,7 @@ void main() async { initialSdk: Sdk.python, ); - await examplesLoader.load(descriptor); + await examplesLoader.loadIfNew(descriptor); expect(setExampleTrue, [Sdk.python.id]); expect(setExampleFalse, [Sdk.go.id]); @@ -132,7 +132,7 @@ void main() async { ); try { - await examplesLoader.load(descriptor); + await examplesLoader.loadIfNew(descriptor); } on ExampleLoadingException catch (ex) { thrown = ex; } diff --git a/playground/frontend/playground_components/test/src/controllers/example_loaders/hive_example_loader_test.dart b/playground/frontend/playground_components/test/src/controllers/example_loaders/hive_example_loader_test.dart new file mode 100644 index 000000000000..7975bda8d594 --- /dev/null +++ b/playground/frontend/playground_components/test/src/controllers/example_loaders/hive_example_loader_test.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 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:hive/hive.dart'; +import 'package:hive_test/hive_test.dart'; +import 'package:playground_components/playground_components.dart'; +import 'package:playground_components/src/controllers/example_loaders/hive_example_loader.dart'; + +import '../../common/example_cache.mocks.dart'; + +const _example = Example( + files: [], + name: 'name', + sdk: Sdk.go, + type: ExampleType.example, + path: 'path', +); + +void main() { + test('HiveExampleLoader loads locally stored example', () async { + await setUpTestHive(); + + const descriptor = HiveExampleLoadingDescriptor( + sdk: Sdk.go, + boxName: 'boxName', + snippetId: 'snippetId', + ); + + final box = await Hive.openBox(descriptor.boxName); + await box.put(descriptor.snippetId, jsonEncode(_example.toJson())); + + final loader = HiveExampleLoader( + descriptor: descriptor, + exampleCache: MockExampleCache(), + ); + + final exampleFromHive = await loader.future; + + expect(exampleFromHive.name, _example.name); + expect(exampleFromHive.sdk.id, _example.sdk.id); + + await tearDownTestHive(); + }); +} diff --git a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart index 44d1a3bb05eb..696b887d7f14 100644 --- a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart +++ b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.dart @@ -33,7 +33,7 @@ Future main() async { late PlaygroundController controller; final mockExamplesLoader = MockExamplesLoader(); - when(mockExamplesLoader.load(any)).thenAnswer((_) async => 1); + when(mockExamplesLoader.loadIfNew(any)).thenAnswer((_) async => 1); setUp(() { controller = PlaygroundController( diff --git a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart index 6ab8b1e9dbb7..ae867f7c18d8 100644 --- a/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart +++ b/playground/frontend/playground_components/test/src/controllers/playground_controller_test.mocks.dart @@ -94,6 +94,16 @@ class MockExamplesLoader extends _i1.Mock implements _i5.ExamplesLoader { returnValueForMissingStub: null, ); @override + _i7.Future loadIfNew(_i8.ExamplesLoadingDescriptor? descriptor) => + (super.noSuchMethod( + Invocation.method( + #loadIfNew, + [descriptor], + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); + @override _i7.Future load(_i8.ExamplesLoadingDescriptor? descriptor) => (super.noSuchMethod( Invocation.method( diff --git a/playground/frontend/playground_components/test/src/models/example_loading_descriptors/hive_example_loading_descriptor_test.dart b/playground/frontend/playground_components/test/src/models/example_loading_descriptors/hive_example_loading_descriptor_test.dart new file mode 100644 index 000000000000..fdb9863fd2dd --- /dev/null +++ b/playground/frontend/playground_components/test/src/models/example_loading_descriptors/hive_example_loading_descriptor_test.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_test/flutter_test.dart'; +import 'package:playground_components/src/models/example_loading_descriptors/hive_example_loading_descriptor.dart'; +import 'package:playground_components/src/models/sdk.dart'; + +import 'common.dart'; + +void main() { + group('HiveExampleLoadingDescriptor', () { + const descriptor = HiveExampleLoadingDescriptor( + boxName: 'boxName', + sdk: Sdk.go, + snippetId: 'snippetId', + viewOptions: viewOptions, + ); + + test('toJson -> tryParse', () { + final map = descriptor.toJson(); + final parsed = HiveExampleLoadingDescriptor.tryParse(map); + + expect(parsed, descriptor); + }); + + test('copyWithoutViewOptions', () { + expect( + descriptor.copyWithoutViewOptions(), + HiveExampleLoadingDescriptor( + boxName: descriptor.boxName, + sdk: descriptor.sdk, + snippetId: descriptor.snippetId, + ), + ); + }); + }); +} diff --git a/playground/frontend/pubspec.lock b/playground/frontend/pubspec.lock index dc3febfcbbd2..2891b4dc3309 100644 --- a/playground/frontend/pubspec.lock +++ b/playground/frontend/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 url: "https://pub.dev" source: hosted - version: "52.0.0" + version: "58.0.0" akvelon_flutter_issue_106664_workaround: dependency: "direct main" description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: analyzer - sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.10.0" app_state: dependency: "direct main" description: @@ -101,18 +101,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" build_runner: dependency: "direct dev" description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.4.4" characters: dependency: transitive description: @@ -285,10 +285,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" dbus: dependency: transitive description: @@ -394,10 +394,10 @@ packages: dependency: "direct dev" description: name: flutter_code_editor - sha256: "580ead0408e27f44776ed9b12c02993f6f4fcedfbf25f8fe5ebb4625c03c8c20" + sha256: "85d0d159ce4f33f3aabedea92c0bdc1ad05c6931bba80ac46660f90c7ab6255c" url: "https://pub.dev" source: hosted - version: "0.2.13" + version: "0.2.14" flutter_driver: dependency: transitive description: flutter @@ -444,10 +444,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: b9be7260c1fdbe0090a11d9d356fc2c88e14cf33407fc0c1829d76ab13808035 + sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.5" flutter_test: dependency: "direct dev" description: flutter @@ -462,10 +462,10 @@ packages: dependency: transitive description: name: fluttertoast - sha256: "774fa28b07f3a82c93596bc137be33189fec578ed3447a93a5a11c93435de394" + sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.2.1" frontend_server_client: dependency: transitive description: @@ -507,10 +507,10 @@ packages: dependency: transitive description: name: googleapis_auth - sha256: "127b1bbd32170ab8312f503bd57f1d654d8e4039ddfbc63c027d3f7ade0eff74" + sha256: f59be210ede1e22718962e4ef48a08ae783eb67fe19b0f4c16b204e20df0869c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" graphs: dependency: transitive description: @@ -543,14 +543,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + hive_test: + dependency: transitive + description: + name: hive_test + sha256: dd7a5cf0be7af288566a96180b5d07574023777aa947ef252b69046ec36d8eb2 + url: "https://pub.dev" + source: hosted + version: "1.0.1" html: dependency: transitive description: name: html - sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" url: "https://pub.dev" source: hosted - version: "0.15.1" + version: "0.15.2" http: dependency: transitive description: @@ -656,10 +664,10 @@ packages: dependency: transitive description: name: markdown - sha256: "4ed544d2ce84975b2ab5cbd4268f2d31f47858553ae2295c92fdf5d6e431a927" + sha256: d95a9d12954aafc97f984ca29baaa7690ed4d9ec4140a23ad40580bcdb6c87f5 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.2" matcher: dependency: transitive description: @@ -696,10 +704,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" + sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.4.0" mocktail: dependency: transitive description: @@ -728,10 +736,10 @@ packages: dependency: transitive description: name: node_preamble - sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" onmessage: dependency: "direct main" description: @@ -776,50 +784,50 @@ packages: dependency: transitive description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.14" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: da97262be945a72270513700a92b39dd2f4a54dad55d061687e2e37a6390366a url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.25" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.10" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" petitparser: dependency: transitive description: @@ -854,10 +862,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pool: dependency: transitive description: @@ -902,10 +910,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" rxdart: dependency: transitive description: @@ -926,58 +934,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" + sha256: "858aaa72d8f61637d64e776aca82e1c67e6d9ee07979123c5d17115031c1b13b" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.0" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" + sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" + sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" shelf: dependency: transitive description: @@ -1035,10 +1043,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427" + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" url: "https://pub.dev" source: hosted - version: "0.10.11" + version: "0.10.12" source_span: dependency: transitive description: @@ -1155,18 +1163,18 @@ packages: dependency: transitive description: name: universal_html - sha256: "5ff50b7c14d201421cf5230ec389a0591c4deb5c817c9d7ccca3b26fe5f31e34" + sha256: ed4f24120c9b1b4721d44e439f7a47d09d9f1b7b868bc84c9d6d373a4a8732af url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.2.1" universal_io: dependency: transitive description: name: universal_io - sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.0" url_launcher: dependency: "direct main" description: @@ -1179,58 +1187,58 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + sha256: a52628068d282d01a07cd86e6ba99e497aa45ce8c91159015b2416907d78e411 url: "https://pub.dev" source: hosted - version: "6.0.23" + version: "6.0.27" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" url_strategy: dependency: "direct main" description: @@ -1243,26 +1251,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "09562ef5f47aa84f6567495adb6b9cb2a3192b82c352623b8bd00b300d62603b" + sha256: ea8d3fc7b2e0f35de38a7465063ecfcf03d8217f7962aa2a6717132cb5d43a79 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.5" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "886e57742644ebed024dc3ade29712e37eea1b03d294fb314c0a3386243fe5a6" + sha256: a5eaa5d19e123ad4f61c3718ca1ed921c4e6254238d9145f82aa214955d9aced url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.5" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "5d9010c4a292766c55395b2288532579a85673f8148460d1e233d98ffe10d24e" + sha256: "15edc42f7eaa478ce854eaf1fbb9062a899c0e4e56e775dd73b7f4709c97c4ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.5" vector_math: dependency: transitive description: @@ -1307,10 +1315,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" webdriver: dependency: transitive description: @@ -1331,10 +1339,10 @@ packages: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" xdg_directories: dependency: transitive description: 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 d341e3c0566c..60b85e0fa4b8 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 @@ -64,6 +64,16 @@ class MockExamplesLoader extends _i1.Mock implements _i3.ExamplesLoader { returnValueForMissingStub: null, ); @override + _i5.Future loadIfNew(_i6.ExamplesLoadingDescriptor? descriptor) => + (super.noSuchMethod( + Invocation.method( + #loadIfNew, + [descriptor], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override _i5.Future load(_i6.ExamplesLoadingDescriptor? descriptor) => (super.noSuchMethod( Invocation.method(