Browse Source

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 <[email protected]>
Yijing Huang 2 years ago
parent
commit
b8983e4466

+ 5 - 0
frontend/appflowy_flutter/assets/images/login/language.svg

@@ -0,0 +1,5 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.99999 15.8333C13.2217 15.8333 15.8333 13.2216 15.8333 9.99996C15.8333 6.7783 13.2217 4.16663 9.99999 4.16663C6.77833 4.16663 4.16666 6.7783 4.16666 9.99996C4.16666 13.2216 6.77833 15.8333 9.99999 15.8333Z" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.16666 10H15.8333" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.0002 4.16663C11.4593 5.764 12.2884 7.83698 12.3335 9.99996C12.2884 12.1629 11.4593 14.2359 10.0002 15.8333C8.54109 14.2359 7.7119 12.1629 7.66684 9.99996C7.7119 7.83698 8.54109 5.764 10.0002 4.16663V4.16663Z" stroke="#333333" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 45 - 0
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');
+    });
+  });
+}

+ 50 - 2
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<void> 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<void> 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<void> hoverOnWidget(
     Finder finder, {

+ 11 - 0
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(

+ 136 - 38
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<SkipLogInScreen> {
   }
 
   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<SkipLogInScreen> {
         ),
         const VSpace(32),
         SizedBox(
-          width: MediaQuery.of(context).size.width * 0.5,
+          width: size.width * 0.5,
           child: FolderWidget(
             createFolderCallback: () async {
               _didCustomizeFolder = true;
@@ -79,15 +86,94 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
         ),
         const Spacer(),
         const VSpace(48),
-        _buildSubscribeButtons(context),
-        const VSpace(24),
+        const SkipLoginPageFooter(),
+        const VSpace(20),
       ],
     );
   }
 
-  Widget _buildSubscribeButtons(BuildContext context) {
+  Future<void> _autoRegister(BuildContext context) async {
+    final result = await widget.authService.signUpAsGuest();
+    result.fold(
+      (error) {
+        Log.error(error);
+      },
+      (user) {
+        FolderEventGetCurrentWorkspace().send().then((result) {
+          _openCurrentWorkspace(context, user, result);
+        });
+      },
+    );
+  }
+
+  Future<void> _relaunchAppAndAutoRegister() async {
+    await FlowyRunner.run(
+      FlowyApp(),
+      config: const LaunchConfiguration(
+        autoRegistrationSupported: true,
+      ),
+    );
+  }
+
+  void _openCurrentWorkspace(
+    BuildContext context,
+    UserProfilePB user,
+    dartz.Either<WorkspaceSettingPB, FlowyError> workspacesOrError,
+  ) {
+    workspacesOrError.fold(
+      (workspaceSetting) {
+        widget.router
+            .pushHomeScreenWithWorkSpace(context, user, workspaceSetting);
+      },
+      (error) {
+        Log.error(error);
+      },
+    );
+  }
+}
+
+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(),
@@ -109,6 +195,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
         ),
         FlowyTextButton(
           LocaleKeys.subscribeNewsletterText.tr(),
+          overflow: TextOverflow.ellipsis,
           fontWeight: FontWeight.w500,
           fontColor: Theme.of(context).colorScheme.primary,
           hoverColor: Colors.transparent,
@@ -127,42 +214,53 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
       throw 'Could not launch $url';
     }
   }
+}
 
-  Future<void> _autoRegister(BuildContext context) async {
-    final result = await widget.authService.signUpAsGuest();
-    result.fold(
-      (error) {
-        Log.error(error);
-      },
-      (user) {
-        FolderEventGetCurrentWorkspace().send().then((result) {
-          _openCurrentWorkspace(context, user, result);
-        });
-      },
-    );
-  }
-
-  Future<void> _relaunchAppAndAutoRegister() async {
-    await FlowyRunner.run(
-      FlowyApp(),
-      config: const LaunchConfiguration(
-        autoRegistrationSupported: true,
-      ),
-    );
-  }
+class LanguageSelectorOnWelcomePage extends StatelessWidget {
+  const LanguageSelectorOnWelcomePage({
+    super.key,
+  });
 
-  void _openCurrentWorkspace(
-    BuildContext context,
-    UserProfilePB user,
-    dartz.Either<WorkspaceSettingPB, FlowyError> workspacesOrError,
-  ) {
-    workspacesOrError.fold(
-      (workspaceSetting) {
-        widget.router
-            .pushHomeScreenWithWorkSpace(context, user, workspaceSetting);
-      },
-      (error) {
-        Log.error(error);
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
+      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,
+            );
+          },
+        );
       },
     );
   }

+ 4 - 3
frontend/appflowy_flutter/lib/workspace/application/appearance.dart

@@ -57,13 +57,14 @@ class AppearanceSettingsCubit extends Cubit<AppearanceSettingsState> {
       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));
     }
   }

+ 3 - 5
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<Locale> allLocales;
-  final Locale currentLocale;
 
   @override
   Widget build(BuildContext context) {
+    // get current locale from cubit
+    final state = context.watch<AppearanceSettingsCubit>().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,
       ),