From b8983e44664a7ead0aa978934adf5535eb06d318 Mon Sep 17 00:00:00 2001 From: Yijing Huang Date: Wed, 14 Jun 2023 06:14:41 -0500 Subject: [PATCH] feat: language selector on welcome page (#2796) * feat: add language selector on welcome page * feat: add hover effect and refactor layout * test: add basic languge selector testing * chore: increate place holder width * fix: add catch error for setLocale and finish the testing * chore: update comment * feat: refactor the skip login in page and add tests --------- Co-authored-by: Lucas.Xu --- .../assets/images/login/language.svg | 5 + .../integration_test/language_test.dart | 45 +++++ .../util/common_operations.dart | 52 ++++- .../integration_test/util/expectation.dart | 11 + .../user/presentation/skip_log_in_screen.dart | 190 +++++++++++++----- .../lib/workspace/application/appearance.dart | 7 +- .../widgets/settings_language_view.dart | 8 +- 7 files changed, 262 insertions(+), 56 deletions(-) create mode 100644 frontend/appflowy_flutter/assets/images/login/language.svg create mode 100644 frontend/appflowy_flutter/integration_test/language_test.dart diff --git a/frontend/appflowy_flutter/assets/images/login/language.svg b/frontend/appflowy_flutter/assets/images/login/language.svg new file mode 100644 index 0000000000..ef3e996590 --- /dev/null +++ b/frontend/appflowy_flutter/assets/images/login/language.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_flutter/integration_test/language_test.dart b/frontend/appflowy_flutter/integration_test/language_test.dart new file mode 100644 index 0000000000..40b774a1d9 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/language_test.dart @@ -0,0 +1,45 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('document', () { + const location = 'appflowy'; + + setUpAll(() async { + await TestFolder.setTestLocation(location); + }); + + tearDownAll(() async { + await TestFolder.cleanTestLocation(null); + }); + + testWidgets( + 'change the language successfully when launching the app for the first time', + (tester) async { + await tester.initializeAppFlowy(); + + await tester.tapLanguageSelectorOnWelcomePage(); + expect(find.byType(LanguageItemsListView), findsOneWidget); + + await tester.tapLanguageItem(languageCode: 'zh', countryCode: 'CN'); + tester.expectToSeeText('开始'); + + await tester.tapLanguageItem(languageCode: 'en', scrollDelta: -100); + tester.expectToSeeText('Quick Start'); + + await tester.tapLanguageItem(languageCode: 'it', countryCode: 'IT'); + tester.expectToSeeText('Andiamo'); + }); + + /// Make sure this test is executed after the test above. + testWidgets('check the language after relaunching the app', (tester) async { + await tester.initializeAppFlowy(); + tester.expectToSeeText('Andiamo'); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/util/common_operations.dart b/frontend/appflowy_flutter/integration_test/util/common_operations.dart index 6cd5856e0b..46123f3897 100644 --- a/frontend/appflowy_flutter/integration_test/util/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/util/common_operations.dart @@ -1,11 +1,12 @@ import 'dart:ui'; - +import 'package:appflowy_backend/log.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/header/add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flutter/material.dart'; @@ -52,6 +53,53 @@ extension CommonOperations on WidgetTester { await tapButtonWithName(LocaleKeys.importPanel_textAndMarkdown.tr()); } + /// Tap the LanguageSelectorOnWelcomePage widget on the launch page. + Future tapLanguageSelectorOnWelcomePage() async { + final languageSelector = find.byType(LanguageSelectorOnWelcomePage); + await tapButton(languageSelector); + } + + /// Tap languageItem on LanguageItemsListView. + /// + /// [scrollDelta] is the distance to scroll the ListView. + /// Default value is 100 + /// + /// If it is positive -> scroll down. + /// + /// If it is negative -> scroll up. + Future tapLanguageItem({ + required String languageCode, + String? countryCode, + double? scrollDelta, + }) async { + final languageItemsListView = find.descendant( + of: find.byType(ListView), + matching: find.byType(Scrollable), + ); + + final languageItem = find.byWidgetPredicate( + (widget) => + widget is LanguageItem && + widget.locale.languageCode == languageCode && + widget.locale.countryCode == countryCode, + ); + + // scroll the ListView until zHCNLanguageItem shows on the screen. + await scrollUntilVisible( + languageItem, + scrollDelta ?? 100, + scrollable: languageItemsListView, + // maxHeight of LanguageItemsListView + maxScrolls: 400, + ); + + try { + await tapButton(languageItem); + } on FlutterError catch (e) { + Log.warn('tapLanguageItem error: $e'); + } + } + /// Hover on the widget. Future hoverOnWidget( Finder finder, { diff --git a/frontend/appflowy_flutter/integration_test/util/expectation.dart b/frontend/appflowy_flutter/integration_test/util/expectation.dart index 85862a5f3f..5b3ddbf8d5 100644 --- a/frontend/appflowy_flutter/integration_test/util/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/util/expectation.dart @@ -67,6 +67,17 @@ extension Expectation on WidgetTester { expect(userName, findsOneWidget); } + /// Expect to see a text + void expectToSeeText(String text) { + Finder textWidget = find.textContaining(text, findRichText: true); + if (textWidget.evaluate().isEmpty) { + textWidget = find.byWidgetPredicate( + (widget) => widget is FlowyText && widget.title == text, + ); + } + expect(textWidget, findsOneWidget); + } + /// Find the page name on the home page. Finder findPageName(String name) { return find.byWidgetPredicate( diff --git a/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart index 9bf1892762..07a6172ad5 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/skip_log_in_screen.dart @@ -3,8 +3,13 @@ import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/launch_configuration.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/appearance.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/language.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -13,6 +18,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -49,6 +55,7 @@ class _SkipLogInScreenState extends State { } Widget _renderBody(BuildContext context) { + final size = MediaQuery.of(context).size; return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -70,7 +77,7 @@ class _SkipLogInScreenState extends State { ), const VSpace(32), SizedBox( - width: MediaQuery.of(context).size.width * 0.5, + width: size.width * 0.5, child: FolderWidget( createFolderCallback: () async { _didCustomizeFolder = true; @@ -79,55 +86,12 @@ class _SkipLogInScreenState extends State { ), const Spacer(), const VSpace(48), - _buildSubscribeButtons(context), - const VSpace(24), + const SkipLoginPageFooter(), + const VSpace(20), ], ); } - Widget _buildSubscribeButtons(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText.regular( - LocaleKeys.youCanAlso.tr(), - fontSize: FontSizes.s12, - ), - FlowyTextButton( - LocaleKeys.githubStarText.tr(), - fontWeight: FontWeight.w500, - fontColor: Theme.of(context).colorScheme.primary, - hoverColor: Colors.transparent, - fillColor: Colors.transparent, - onPressed: () => _launchURL( - 'https://github.com/AppFlowy-IO/appflowy', - ), - ), - FlowyText.regular( - LocaleKeys.and.tr(), - fontSize: FontSizes.s12, - ), - FlowyTextButton( - LocaleKeys.subscribeNewsletterText.tr(), - fontWeight: FontWeight.w500, - fontColor: Theme.of(context).colorScheme.primary, - hoverColor: Colors.transparent, - fillColor: Colors.transparent, - onPressed: () => _launchURL('https://www.appflowy.io/blog'), - ), - ], - ); - } - - Future _launchURL(String url) async { - final uri = Uri.parse(url); - if (await canLaunchUrl(uri)) { - await launchUrl(uri); - } else { - throw 'Could not launch $url'; - } - } - Future _autoRegister(BuildContext context) async { final result = await widget.authService.signUpAsGuest(); result.fold( @@ -168,6 +132,140 @@ class _SkipLogInScreenState extends State { } } +class SkipLoginPageFooter extends StatelessWidget { + const SkipLoginPageFooter({ + super.key, + }); + + @override + Widget build(BuildContext context) { + // The placeholderWidth should be greater than the longest width of the LanguageSelectorOnWelcomePage + const double placeholderWidth = 180; + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HSpace(placeholderWidth), + Expanded(child: SubscribeButtons()), + SizedBox( + width: placeholderWidth, + height: 28, + child: Row( + children: [ + Spacer(), + LanguageSelectorOnWelcomePage(), + ], + ), + ), + ], + ), + ); + } +} + +class SubscribeButtons extends StatelessWidget { + const SubscribeButtons({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText.regular( + LocaleKeys.youCanAlso.tr(), + fontSize: FontSizes.s12, + ), + FlowyTextButton( + LocaleKeys.githubStarText.tr(), + fontWeight: FontWeight.w500, + fontColor: Theme.of(context).colorScheme.primary, + hoverColor: Colors.transparent, + fillColor: Colors.transparent, + onPressed: () => _launchURL( + 'https://github.com/AppFlowy-IO/appflowy', + ), + ), + FlowyText.regular( + LocaleKeys.and.tr(), + fontSize: FontSizes.s12, + ), + FlowyTextButton( + LocaleKeys.subscribeNewsletterText.tr(), + overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w500, + fontColor: Theme.of(context).colorScheme.primary, + hoverColor: Colors.transparent, + fillColor: Colors.transparent, + onPressed: () => _launchURL('https://www.appflowy.io/blog'), + ), + ], + ); + } + + Future _launchURL(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + throw 'Could not launch $url'; + } + } +} + +class LanguageSelectorOnWelcomePage extends StatelessWidget { + const LanguageSelectorOnWelcomePage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + offset: const Offset(0, -450), + direction: PopoverDirection.bottomWithRightAligned, + child: FlowyButton( + useIntrinsicWidth: true, + text: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const FlowySvg( + name: 'login/language', + size: Size.square(20), + ), + const HSpace(4), + FlowyText( + languageFromLocale(state.locale), + ), + // const HSpace(4), + const FlowySvg( + name: 'home/drop_down_hide', + size: Size.square(20), + ), + ], + ), + ), + popupBuilder: (BuildContext context) { + final easyLocalization = EasyLocalization.of(context); + if (easyLocalization == null) { + return const SizedBox.shrink(); + } + final allLocales = easyLocalization.supportedLocales; + return LanguageItemsListView( + allLocales: allLocales, + ); + }, + ); + }, + ); + } +} + class GoButton extends StatelessWidget { final VoidCallback onPressed; diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart index a3ef90ab23..6bae98aa25 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance.dart @@ -57,13 +57,14 @@ class AppearanceSettingsCubit extends Cubit { newLocale = const Locale('en'); } - if (state.locale != newLocale) { - context.setLocale(newLocale); + context.setLocale(newLocale).catchError((e) { + Log.warn('Catch error in setLocale: $e}'); + }); + if (state.locale != newLocale) { _setting.locale.languageCode = newLocale.languageCode; _setting.locale.countryCode = newLocale.countryCode ?? ""; _saveAppearanceSettings(); - emit(state.copyWith(locale: newLocale)); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart index 23fb31fbc8..78bf071d0b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -49,10 +49,8 @@ class LanguageSelector extends StatelessWidget { ), popupBuilder: (BuildContext context) { final allLocales = EasyLocalization.of(context)!.supportedLocales; - return LanguageItemsListView( allLocales: allLocales, - currentLocale: currentLocale, ); }, ); @@ -63,20 +61,20 @@ class LanguageItemsListView extends StatelessWidget { const LanguageItemsListView({ super.key, required this.allLocales, - required this.currentLocale, }); final List allLocales; - final Locale currentLocale; @override Widget build(BuildContext context) { + // get current locale from cubit + final state = context.watch().state; return ConstrainedBox( constraints: const BoxConstraints(maxHeight: 400), child: ListView.builder( itemBuilder: (context, index) { final locale = allLocales[index]; - return LanguageItem(locale: locale, currentLocale: currentLocale); + return LanguageItem(locale: locale, currentLocale: state.locale); }, itemCount: allLocales.length, ),