mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-11-09 13:09:21 +03:00
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:
parent
00c0934df6
commit
b8983e4466
@ -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 |
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
@ -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, {
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
|
Loading…
Reference in New Issue
Block a user