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 <lucas.xu@appflowy.io>
This commit is contained in:
Yijing Huang 2023-06-14 06:14:41 -05:00 committed by GitHub
parent 00c0934df6
commit b8983e4466
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 262 additions and 56 deletions

View File

@ -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>

After

Width:  |  Height:  |  Size: 831 B

View File

@ -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');
});
});
}

View File

@ -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, {

View File

@ -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(

View File

@ -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,55 +86,12 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
),
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<void> _launchURL(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
throw 'Could not launch $url';
}
}
Future<void> _autoRegister(BuildContext context) async {
final result = await widget.authService.signUpAsGuest();
result.fold(
@ -168,6 +132,140 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
}
}
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<void> _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<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,
);
},
);
},
);
}
}
class GoButton extends StatelessWidget {
final VoidCallback onPressed;

View File

@ -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));
}
}

View File

@ -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,
),