feat: settings my account (#5223)

* feat: my account settings page

* test: amend tests

* chore: remove unused code

* test: remove widget tests

* fix: text color on select buttons

* test: clean and remove unused test helpers

* test: fix test after refactor
This commit is contained in:
Mathias Mogensen 2024-04-30 14:09:08 +02:00 committed by GitHub
parent f3544375c9
commit 4981baac13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
124 changed files with 2315 additions and 4662 deletions

View File

@ -3,21 +3,28 @@
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.dart';
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
@ -37,22 +44,35 @@ void main() {
// reanme the name of the anon user
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userNameFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserNameInput),
);
await tester.enterText(userNameFinder, 'local_user');
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
await tester.pumpAndSettle();
await tester.enterUserName('local_user');
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
// sign up with Google
await tester.tapGoogleLoginInButton();
// sign out
await tester.expectToSeeHomePage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.logout();
await tester.pumpAndSettle();
@ -63,8 +83,9 @@ void main() {
// New anon user name
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
final userNameInput = tester.widget(userNameFinder) as UserNameInput;
await tester.openSettingsPage(SettingsPage.account);
final userNameInput =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(userNameInput.name, 'Me');
});
});

View File

@ -6,8 +6,8 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
@ -37,11 +37,18 @@ void main() {
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.tapButton(find.byType(SettingLogoutButton));
await tester.openSettingsPage(SettingsPage.account);
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeText(LocaleKeys.button_confirm.tr());
await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1));
@ -56,7 +63,16 @@ void main() {
// should not see the sync setting page when sign in as anonymous
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-in
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeGoogleLoginButton();
});

View File

@ -8,14 +8,15 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
@ -50,7 +51,7 @@ void main() {
await tester.waitForSeconds(6);
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
await tester.logout();
});

View File

@ -1,9 +1,7 @@
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -25,11 +23,8 @@ void main() {
// Open the setting page and sign out
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.tapButton(find.byType(SettingLogoutButton));
tester.expectToSeeText(LocaleKeys.button_ok.tr());
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
await tester.openSettingsPage(SettingsPage.account);
await tester.logout();
// Go to the sign in page again
await tester.pumpAndSettle(const Duration(seconds: 1));
@ -42,7 +37,16 @@ void main() {
// should not see the sync setting page when sign in as anonymous
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// Scroll to sign-out
await tester.scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
await tester.tapButton(find.byType(SignInOutButton));
tester.expectToSeeGoogleLoginButton();
});

View File

@ -2,22 +2,27 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import '../desktop/board/board_hide_groups_test.dart';
import '../shared/database_test_op.dart';
import '../shared/dir.dart';
import '../shared/emoji.dart';
@ -39,28 +44,9 @@ void main() {
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
// final userAvatarFinder = find.descendant(
// of: find.byType(SettingsUserView),
// matching: find.byType(UserAvatar),
// );
await tester.openSettingsPage(SettingsPage.account);
// Open icon picker dialog and select emoji
// await tester.tap(userAvatarFinder);
// await tester.pumpAndSettle();
// await tester.tapEmoji('😁');
// await tester.pumpAndSettle();
// final UserAvatar userAvatar =
// tester.widget(userAvatarFinder) as UserAvatar;
// expect(userAvatar.iconUrl, '😁');
// enter user name
final userNameFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserNameInput),
);
await tester.enterText(userNameFinder, name);
await tester.pumpAndSettle();
await tester.enterUserName(name);
await tester.tapEscButton();
// wait 2 seconds for the sync to finish
@ -78,23 +64,12 @@ void main() {
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.user);
await tester.openSettingsPage(SettingsPage.account);
// verify icon
// final userAvatarFinder = find.descendant(
// of: find.byType(SettingsUserView),
// matching: find.byType(UserAvatar),
// );
// final UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
// expect(userAvatar.iconUrl, '😁');
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
// verify name
final userNameFinder = find.descendant(
of: find.byType(SettingsUserView),
matching: find.byType(UserNameInput),
);
final UserNameInput userNameInput =
tester.widget(userNameFinder) as UserNameInput;
expect(userNameInput.name, name);
expect(profileSetting.name, name);
});
}

View File

@ -9,7 +9,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';

View File

@ -2,6 +2,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
@ -14,12 +16,10 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_worksp
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;

View File

@ -14,7 +14,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';

View File

@ -11,7 +11,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';

View File

@ -1,25 +1,33 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'base.dart';
import 'expectation.dart';
import 'util.dart';
extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
}
/// Requires being on the SettingsPage.account of the SettingsDialog
Future<void> logout() async {
await tapButton(find.byType(SettingLogoutButton));
final scrollable = find.findSettingsScrollable();
await scrollUntilVisible(
find.byType(SignInOutButton),
100,
scrollable: scrollable,
);
expectToSeeText(LocaleKeys.button_ok.tr());
await tapButtonWithName(LocaleKeys.button_ok.tr());
await tapButton(find.byType(SignInOutButton));
expectToSeeText(LocaleKeys.button_confirm.tr());
await tapButtonWithName(LocaleKeys.button_confirm.tr());
}
Future<void> tapSignInAsGuest() async {

View File

@ -1,6 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/cloud_env_test.dart';
import 'package:appflowy/startup/entry_point.dart';
@ -13,16 +16,12 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
class FlowyTestContext {
FlowyTestContext({
required this.applicationDataDirectory,
});
FlowyTestContext({required this.applicationDataDirectory});
final String applicationDataDirectory;
}
@ -75,7 +74,7 @@ extension AppFlowyTestBase on WidgetTester {
if (cloudType != null) {
switch (cloudType) {
case AuthenticatorType.local:
await useLocal();
await useLocalServer();
break;
case AuthenticatorType.supabase:
await useTestSupabaseCloud();
@ -187,37 +186,14 @@ extension AppFlowyTestBase on WidgetTester {
);
}
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,
}) async {
Finder button = find.text(
tr,
findRichText: true,
skipOffstage: false,
);
Future<void> tapButtonWithName(String tr, {int milliseconds = 500}) async {
Finder button = find.text(tr, findRichText: true, skipOffstage: false);
if (button.evaluate().isEmpty) {
button = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == tr,
);
}
await tapButton(
button,
milliseconds: milliseconds,
);
return;
}
Future<void> tapButtonWithTooltip(
String tr, {
int milliseconds = 500,
}) async {
final button = find.byTooltip(tr);
await tapButton(
button,
milliseconds: milliseconds,
);
return;
await tapButton(button, milliseconds: milliseconds);
}
Future<void> doubleTapAt(
@ -232,34 +208,8 @@ extension AppFlowyTestBase on WidgetTester {
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> doubleTapButton(
Finder finder, {
int? pointer,
int buttons = kPrimaryButton,
bool warnIfMissed = true,
int milliseconds = 500,
}) async {
await tap(
finder,
pointer: pointer,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pump(kDoubleTapMinTime);
await tap(
finder,
buttons: buttons,
pointer: pointer,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(Duration(milliseconds: milliseconds));
}
Future<void> wait(int milliseconds) async {
await pumpAndSettle(Duration(milliseconds: milliseconds));
return;
}
}
@ -271,10 +221,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
}
}
Future<void> useLocal() async {
await useLocalServer();
}
Future<void> useTestSupabaseCloud() async {
await useSupabaseCloud(
url: TestEnv.supabaseUrl,

View File

@ -24,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_
import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/log.dart';
@ -72,27 +73,6 @@ extension CommonOperations on WidgetTester {
await tapButton(newPageButton);
}
/// Tap the create document button.
///
/// Must call [tapAddViewButton] first.
Future<void> tapCreateDocumentButton() async {
await tapButtonWithName(LocaleKeys.document_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddViewButton] first.
Future<void> tapCreateGridButton() async {
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
}
/// Tap the create grid button.
///
/// Must call [tapAddViewButton] first.
Future<void> tapCreateCalendarButton() async {
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
}
/// Tap the import button.
///
/// Must call [tapAddViewButton] first.
@ -181,15 +161,9 @@ extension CommonOperations on WidgetTester {
}) async {
final pageNames = findPageName(name, layout: layout);
if (useLast) {
await hoverOnWidget(
pageNames.last,
onHover: onHover,
);
await hoverOnWidget(pageNames.last, onHover: onHover);
} else {
await hoverOnWidget(
pageNames.first,
onHover: onHover,
);
await hoverOnWidget(pageNames.first, onHover: onHover);
}
}
@ -497,9 +471,7 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> openNotificationHub({
int tabIndex = 0,
}) async {
Future<void> openNotificationHub({int tabIndex = 0}) async {
final finder = find.descendant(
of: find.byType(NotificationButton),
matching: find.byWidgetPredicate(
@ -542,15 +514,6 @@ extension CommonOperations on WidgetTester {
await tapButton(workspace, milliseconds: 2000);
}
Future<void> closeCollaborativeWorkspaceMenu() async {
if (!FeatureFlag.collaborativeWorkspace.isOn) {
throw UnsupportedError('Collaborative workspace is not enabled');
}
await tapAt(Offset.zero);
await pumpAndSettle();
}
Future<void> createCollaborativeWorkspace(String name) async {
if (!FeatureFlag.collaborativeWorkspace.isOn) {
throw UnsupportedError('Collaborative workspace is not enabled');
@ -576,6 +539,20 @@ extension CommonOperations on WidgetTester {
}
}
extension SettingsFinder on CommonFinders {
Finder findSettingsScrollable() => find
.descendant(
of: find
.descendant(
of: find.byType(SettingsBody),
matching: find.byType(SingleChildScrollView),
)
.first,
matching: find.byType(Scrollable),
)
.first;
}
extension ViewLayoutPBTest on ViewLayoutPB {
String get menuName {
switch (this) {

View File

@ -1,8 +1,9 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:archive/archive_io.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -51,11 +52,7 @@ class TestWorkspaceService {
Future<void> setUpAll() async {
final root = await workspace.root;
final path = root.path;
SharedPreferences.setMockInitialValues(
{
KVKeys.pathLocation: path,
},
);
SharedPreferences.setMockInitialValues({KVKeys.pathLocation: path});
}
/// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control.

View File

@ -1,7 +1,5 @@
import 'dart:io';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -31,8 +29,6 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/discl
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';
@ -57,6 +53,10 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
@ -71,7 +71,6 @@ import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
@ -343,16 +342,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
return w.isToday;
}
Future<void> toggleDateRange() async {
final findDateEditor = find.byType(EndTimeButton);
final findToggle = find.byType(Toggle);
final finder = find.descendant(
of: findDateEditor,
matching: findToggle,
);
await tapButton(finder);
}
Future<void> tapChangeDateTimeFormatButton() async {
await tapButton(find.byType(DateTypeOptionButton));
}
@ -403,9 +392,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// The [SelectOptionCellEditor] must be opened first.
Future<void> createOption({
required String name,
}) async {
Future<void> createOption({required String name}) async {
final findEditor = find.byType(SelectOptionCellEditor);
expect(findEditor, findsOneWidget);
@ -419,9 +406,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
Future<void> selectOption({
required String name,
}) async {
Future<void> selectOption({required String name}) async {
final option = find.byWidgetPredicate(
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
);
@ -440,11 +425,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget.name == name || widget.option?.name == name),
);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: option,
);
final cell = find.descendant(of: findRow.at(rowIndex), matching: option);
expect(cell, findsOneWidget);
}
@ -458,11 +439,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget) => widget is SelectOptionTag,
);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: options,
);
final cell = find.descendant(of: findRow.at(rowIndex), matching: options);
expect(cell, matcher);
}
@ -470,21 +447,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
final findRow = find.byType(GridRow);
final findCell = finderForFieldType(FieldType.Checklist);
final cell = find.descendant(
of: findRow.at(rowIndex),
matching: findCell,
);
final cell = find.descendant(of: findRow.at(rowIndex), matching: findCell);
await tapButton(cell);
}
void assertChecklistEditorVisible({required bool visible}) {
final editor = find.byType(ChecklistCellEditor);
if (visible) {
expect(editor, findsOneWidget);
} else {
expect(editor, findsNothing);
return expect(editor, findsOneWidget);
}
expect(editor, findsNothing);
}
Future<void> createNewChecklistTask({
@ -519,7 +491,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
required bool isChecked,
}) {
final task = find.byType(ChecklistItem).at(index);
final widget = this.widget<ChecklistItem>(task);
assert(
widget.task.data.name == name && widget.task.isSelected == isChecked,
@ -591,27 +562,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
Future<void> editTitleInRowDetailPage(String title) async {
final titleField = find.byType(EditableTextCell);
await enterText(titleField, title);
await pumpAndSettle();
}
Future<void> hoverRowBanner() async {
final banner = find.byType(RowBanner);
expect(banner, findsOneWidget);
await startGesture(
getCenter(banner),
kind: PointerDeviceKind.mouse,
);
await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse);
await pumpAndSettle();
}
Future<void> openEmojiPicker() async {
await tapButton(find.byType(AddEmojiButton));
}
Future<void> openEmojiPicker() async =>
tapButton(find.byType(AddEmojiButton));
Future<void> tapDateCellInRowDetailPage() async {
final findDateCell = find.byType(EditableDateCell);
@ -630,25 +590,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
}
Future<void> duplicateRowInRowDetailPage() async {
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
await tapButton(duplicateButton);
}
Future<void> deleteRowInRowDetailPage() async {
final deleteButton = find.byType(RowDetailPageDeleteButton);
await tapButton(deleteButton);
}
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
final fieldButtons = find.byType(FieldCellButton);
final button = find
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
.at(index);
return startGesture(
getCenter(button),
kind: PointerDeviceKind.mouse,
);
return startGesture(getCenter(button), kind: PointerDeviceKind.mouse);
}
Future<void> reorderFieldInRowDetail({required double offset}) async {
@ -657,11 +604,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget) => widget is ReorderableDragStartListener && widget.enabled,
)
.first;
await drag(
thumb,
Offset(0, offset),
kind: PointerDeviceKind.mouse,
);
await drag(thumb, Offset(0, offset), kind: PointerDeviceKind.mouse);
await pumpAndSettle();
}
@ -681,8 +624,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
Future<void> tapDeletePropertyInFieldEditor() async {
final deleteButton = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.delete,
(w) => w is FieldActionCell && w.action == FieldAction.delete,
);
await tapButton(deleteButton);
@ -693,11 +635,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(confirmButton);
}
Future<void> scrollGridByOffset(Offset offset) async {
await drag(find.byType(GridPage), offset);
await pumpAndSettle();
}
Future<void> scrollRowDetailByOffset(Offset offset) async {
await drag(find.byType(RowDetailPage), offset);
await pumpAndSettle();
@ -756,8 +693,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapGridFieldWithName] first.
Future<void> tapDeletePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.delete,
(w) => w is FieldActionCell && w.action == FieldAction.delete,
);
await tapButton(field);
}
@ -765,9 +701,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// A SimpleDialog must be shown first, e.g. when deleting a field.
Future<void> tapDialogOkButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is PrimaryTextButton &&
widget.label == LocaleKeys.button_ok.tr(),
(w) => w is PrimaryTextButton && w.label == LocaleKeys.button_ok.tr(),
);
await tapButton(field);
}
@ -775,8 +709,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapGridFieldWithName] first.
Future<void> tapDuplicatePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell && widget.action == FieldAction.duplicate,
(w) => w is FieldActionCell && w.action == FieldAction.duplicate,
);
await tapButton(field);
}
@ -798,45 +731,34 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Should call [tapGridFieldWithName] first.
Future<void> tapHidePropertyButton() async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
);
await tapButton(field);
}
Future<void> tapHidePropertyButtonInFieldEditor() async {
final button = find.byWidgetPredicate(
(widget) =>
widget is FieldActionCell &&
widget.action == FieldAction.toggleVisibility,
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
);
await tapButton(button);
}
Future<void> tapRowDetailPageRowActionButton() async {
await tapButton(find.byType(RowActionButton));
}
Future<void> tapRowDetailPageRowActionButton() async =>
tapButton(find.byType(RowActionButton));
Future<void> tapRowDetailPageCreatePropertyButton() async {
await tapButton(find.byType(CreateRowFieldButton));
}
Future<void> tapRowDetailPageCreatePropertyButton() async =>
tapButton(find.byType(CreateRowFieldButton));
Future<void> tapRowDetailPageDeleteRowButton() async {
await tapButton(find.byType(RowDetailPageDeleteButton));
}
Future<void> tapRowDetailPageDeleteRowButton() async =>
tapButton(find.byType(RowDetailPageDeleteButton));
Future<void> tapRowDetailPageDuplicateRowButton() async {
await tapButton(find.byType(RowDetailPageDuplicateButton));
}
Future<void> tapRowDetailPageDuplicateRowButton() async =>
tapButton(find.byType(RowDetailPageDuplicateButton));
Future<void> tapSwitchFieldTypeButton() async {
await tapButton(find.byType(SwitchFieldButton));
}
Future<void> tapSwitchFieldTypeButton() async =>
tapButton(find.byType(SwitchFieldButton));
Future<void> tapEscButton() async {
await sendKeyEvent(LogicalKeyboardKey.escape);
}
Future<void> tapEscButton() async => sendKeyEvent(LogicalKeyboardKey.escape);
/// Must call [tapSwitchFieldTypeButton] first.
Future<void> selectFieldType(FieldType fieldType) async {
@ -851,15 +773,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
// Use in edit mode of FieldEditor
void expectEmptyTypeOptionEditor() {
expect(
find.descendant(
of: find.byType(FieldTypeOptionEditor),
matching: find.byType(TypeOptionSeparator),
),
findsNothing,
);
}
void expectEmptyTypeOptionEditor() => expect(
find.descendant(
of: find.byType(FieldTypeOptionEditor),
matching: find.byType(TypeOptionSeparator),
),
findsNothing,
);
/// Each field has its own cell, so we can find the corresponding cell by
/// the field type after create a new field.
@ -868,10 +788,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(finder, findsWidgets);
}
Future<void> assertNumberOfFieldsInGridPage(int num) async {
expect(find.byType(GridFieldCell), findsNWidgets(num));
}
Future<void> assertNumberOfRowsInGridPage(int num) async {
expect(
find.byType(GridRow, skipOffstage: false),
@ -884,14 +800,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
/// Check the field type of the [FieldCellButton] is the same as the name.
Future<void> assertFieldTypeWithFieldName(
String name,
FieldType fieldType,
) async {
Future<void> assertFieldTypeWithFieldName(String name, FieldType type) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is FieldCellButton &&
widget.field.fieldType == fieldType &&
widget.field.fieldType == type &&
widget.field.name == name,
);
@ -936,11 +849,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle(const Duration(milliseconds: 200));
}
Future<void> findFieldEditor(dynamic matcher) async {
final finder = find.byType(FieldEditor);
expect(finder, matcher);
}
Future<void> findDateEditor(dynamic matcher) async {
final finder = find.byType(DateCellEditor);
expect(finder, matcher);
@ -994,41 +902,29 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(find.byType(SortButton));
}
Future<void> tapCreateFilterByFieldType(
FieldType fieldType,
String title,
) async {
Future<void> tapCreateFilterByFieldType(FieldType type, String title) async {
final findFilter = find.byWidgetPredicate(
(widget) =>
widget is GridFilterPropertyCell &&
widget.fieldInfo.fieldType == fieldType &&
widget.fieldInfo.fieldType == type &&
widget.fieldInfo.name == title,
);
await tapButton(findFilter);
}
Future<void> tapFilterButtonInGrid(String filterName) async {
Future<void> tapFilterButtonInGrid(String name) async {
final findFilter = find.byType(FilterMenuItem);
final button = find.descendant(
of: findFilter,
matching: find.text(filterName),
);
final button = find.descendant(of: findFilter, matching: find.text(name));
await tapButton(button);
}
Future<void> tapCreateSortByFieldType(
FieldType fieldType,
String title,
) async {
Future<void> tapCreateSortByFieldType(FieldType type, String title) async {
final findSort = find.byWidgetPredicate(
(widget) =>
widget is GridSortPropertyCell &&
widget.fieldInfo.fieldType == fieldType &&
widget.fieldInfo.fieldType == type &&
widget.fieldInfo.name == title,
);
await tapButton(findSort);
}
@ -1085,10 +981,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
of: fromSortItem,
matching: find.byType(ReorderableDragStartListener),
);
await drag(
dragElement,
getCenter(toSortItem) - getCenter(fromSortItem),
);
await drag(dragElement, getCenter(toSortItem) - getCenter(fromSortItem));
await pumpAndSettle(const Duration(milliseconds: 200));
}
@ -1166,15 +1059,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(findCell);
}
Future<void> tapCheckedButtonOnCheckboxFilter() async {
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()),
);
await tapButton(button);
}
Future<void> tapUnCheckedButtonOnCheckboxFilter() async {
final button = find.descendant(
of: find.byType(HoverButton),
@ -1193,15 +1077,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> tapUnCompletedButtonOnChecklistFilter() async {
final button = find.descendant(
of: find.byType(HoverButton),
matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()),
);
await tapButton(button);
}
/// Should call [tapDatabaseSettingButton] first.
Future<void> tapViewPropertiesButton() async {
final findSettingItem = find.byType(DatabaseSettingsList);
@ -1252,16 +1127,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(button);
}
Future<void> tapFirstDayOfWeek() async {
await tapButton(find.byType(FirstDayOfWeek));
}
Future<void> tapFirstDayOfWeekStartFromSunday() async {
final finder = find.byWidgetPredicate(
(widget) => widget is StartFromButton && widget.dayIndex == 0,
);
await tapButton(finder);
}
Future<void> tapFirstDayOfWeek() async =>
tapButton(find.byType(FirstDayOfWeek));
Future<void> tapFirstDayOfWeekStartFromMonday() async {
final finder = find.byWidgetPredicate(
@ -1277,20 +1144,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
void assertFirstDayOfWeekStartFromMonday() {
final finder = find.byWidgetPredicate(
(widget) =>
widget is StartFromButton &&
widget.dayIndex == 1 &&
widget.isSelected == true,
(w) => w is StartFromButton && w.dayIndex == 1 && w.isSelected == true,
);
expect(finder, findsOneWidget);
}
void assertFirstDayOfWeekStartFromSunday() {
final finder = find.byWidgetPredicate(
(widget) =>
widget is StartFromButton &&
widget.dayIndex == 0 &&
widget.isSelected == true,
(w) => w is StartFromButton && w.dayIndex == 0 && w.isSelected == true,
);
expect(finder, findsOneWidget);
}
@ -1307,11 +1168,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
),
)
.first;
await scrollUntilVisible(
todayCell,
300,
scrollable: scrollable,
);
await scrollUntilVisible(todayCell, 300, scrollable: scrollable);
await pumpAndSettle(const Duration(milliseconds: 300));
}
@ -1351,12 +1208,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
String? title,
}) {
final findDayCell = find.byWidgetPredicate(
(widget) =>
widget is CalendarDayCard &&
isSameDay(
widget.date,
date,
),
(widget) => widget is CalendarDayCard && isSameDay(widget.date, date),
);
Finder findEvents = find.descendant(
of: findDayCell,
@ -1390,13 +1242,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(cards.at(index));
}
void assertEventEditorOpen() {
expect(find.byType(CalendarEventEditor), findsOneWidget);
}
void assertEventEditorOpen() =>
expect(find.byType(CalendarEventEditor), findsOneWidget);
Future<void> dismissEventEditor() async {
await simulateKeyEvent(LogicalKeyboardKey.escape);
}
Future<void> dismissEventEditor() async =>
simulateKeyEvent(LogicalKeyboardKey.escape);
Future<void> editEventTitle(String title) async {
final textField = find.descendant(
@ -1507,10 +1357,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
matching: find.byType(TextField),
);
if (isVisible) {
expect(textField, findsOneWidget);
} else {
expect(textField, findsNothing);
return expect(textField, findsOneWidget);
}
expect(textField, findsNothing);
}
Future<void> enterNewGroupName(String name, {required bool submit}) async {
@ -1612,21 +1461,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(okButton);
}
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
expect(find.byType(BoardPage), findsOneWidget);
break;
case DatabaseLayoutPB.Calendar:
expect(find.byType(CalendarPage), findsOneWidget);
break;
case DatabaseLayoutPB.Grid:
expect(find.byType(GridPage), findsOneWidget);
break;
default:
throw Exception('Unknown database layout type: $layout');
}
}
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
DatabaseLayoutPB.Board =>
expect(find.byType(BoardPage), findsOneWidget),
DatabaseLayoutPB.Calendar =>
expect(find.byType(CalendarPage), findsOneWidget),
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
_ => throw Exception('Unknown database layout type: $layout'),
};
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
@ -1634,11 +1476,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(widget) => widget is FlowyText && widget.text == layout.layoutName,
);
final button = find.descendant(
of: findLayoutCell,
matching: findText,
);
final button = find.descendant(of: findLayoutCell, matching: findText);
await tapButton(button);
}
@ -1660,8 +1498,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButton(
find.byWidgetPredicate(
(widget) =>
widget is NumberFormatCell && widget.format == NumberFormatPB.USD,
(w) => w is NumberFormatCell && w.format == NumberFormatPB.USD,
),
);
}
@ -1675,8 +1512,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
String fieldName,
) async {
final field = find.byWidgetPredicate(
(widget) =>
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
(w) => w is DatabasePropertyCell && w.fieldInfo.name == fieldName,
);
final toggleVisibilityButton =
find.descendant(of: field, matching: find.byType(FlowyIconButton));
@ -1684,18 +1520,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
}
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
return find.byType(BoardPage);
case DatabaseLayoutPB.Calendar:
return find.byType(CalendarPage);
case DatabaseLayoutPB.Grid:
return find.byType(GridPage);
default:
throw Exception('Unknown database layout type: $layout');
}
}
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
DatabaseLayoutPB.Board => find.byType(BoardPage),
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
DatabaseLayoutPB.Grid => find.byType(GridPage),
_ => throw Exception('Unknown database layout type: $layout'),
};
Finder finderForFieldType(FieldType fieldType) {
switch (fieldType) {

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:archive/archive.dart';
import 'package:path/path.dart' as p;
Future<void> deleteDirectoriesWithSameBaseNameAsPrefix(
String path,

View File

@ -30,11 +30,8 @@ class EditorOperations {
final WidgetTester tester;
EditorState getCurrentEditorState() {
return tester
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
.editorState;
}
EditorState getCurrentEditorState() =>
tester.widget<AppFlowyEditor>(find.byType(AppFlowyEditor)).editorState;
/// Tap the line of editor at [index]
Future<void> tapLineOfEditorAt(int index) async {
@ -144,16 +141,8 @@ class EditorOperations {
);
}
Future<void> switchNetworkImageCover(String imageUrl) async {
final image = find.byWidgetPredicate(
(widget) => widget is ImageGridItem,
);
await tester.tapButton(image);
}
Future<void> tapOnRemoveCover() async {
await tester.tapButton(find.byType(DeleteCoverButton));
}
Future<void> tapOnRemoveCover() async =>
tester.tapButton(find.byType(DeleteCoverButton));
/// A cover must be present in the document to function properly since this
/// catches all cover types collectively

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
import 'package:appflowy/plugins/document/presentation/banner.dart';
@ -12,7 +14,6 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
@ -89,18 +90,6 @@ extension Expectation on WidgetTester {
expect(exportSuccess, findsOneWidget);
}
/// Expect to see the add button and icon button in the cover toolbar
void expectToSeePluginAddCoverAndIconButton() {
final addCover = find.textContaining(
LocaleKeys.document_plugins_cover_addCover.tr(),
);
final addIcon = find.textContaining(
LocaleKeys.document_plugins_cover_addIcon.tr(),
);
expect(addCover, findsOneWidget);
expect(addIcon, findsOneWidget);
}
/// Expect to see the document header toolbar empty
void expectToSeeEmptyDocumentHeaderToolbar() {
final addCover = find.textContaining(
@ -153,14 +142,6 @@ extension Expectation on WidgetTester {
expect(findRemoveIcon, findsOneWidget);
}
/// Expect to see the user name on the home page
void expectToSeeUserName(String name) {
final userName = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == name,
);
expect(userName, findsOneWidget);
}
/// Expect to see a text
void expectToSeeText(String text) {
Finder textWidget = find.textContaining(text, findRichText: true);
@ -178,26 +159,23 @@ extension Expectation on WidgetTester {
ViewLayoutPB layout = ViewLayoutPB.Document,
String? parentName,
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
}) {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
}
}) =>
find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite &&
widget.view.name == name &&
widget.view.layout == layout,
skipOffstage: false,
);
Finder findAllFavoritePages() {
return find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
);
}
Finder findAllFavoritePages() => find.byWidgetPredicate(
(widget) =>
widget is SingleInnerViewItem &&
widget.view.isFavorite &&
widget.categoryType == FolderCategoryType.favorite,
);
Finder findPageName(
String name, {

View File

@ -11,9 +11,7 @@ class MockFilePicker implements FilePickerService {
final List<String> mockPaths;
@override
Future<String?> getDirectoryPath({String? title}) {
return Future.value(mockPath);
}
Future<String?> getDirectoryPath({String? title}) => Future.value(mockPath);
@override
Future<String?> saveFile({
@ -23,9 +21,8 @@ class MockFilePicker implements FilePickerService {
FileType type = FileType.any,
List<String>? allowedExtensions,
bool lockParentWindow = false,
}) {
return Future.value(mockPath);
}
}) =>
Future.value(mockPath);
@override
Future<FilePickerResult?> pickFiles({
@ -42,34 +39,21 @@ class MockFilePicker implements FilePickerService {
}) {
final platformFiles =
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
return Future.value(
FilePickerResult(
platformFiles,
),
);
return Future.value(FilePickerResult(platformFiles));
}
}
Future<void> mockGetDirectoryPath(
String path,
) async {
Future<void> mockGetDirectoryPath(String path) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPath: path,
),
() => MockFilePicker(mockPath: path),
);
return;
}
Future<String> mockSaveFilePath(
String path,
) async {
Future<String> mockSaveFilePath(String path) async {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPath: path,
),
() => MockFilePicker(mockPath: path),
);
return path;
}
@ -77,9 +61,7 @@ Future<String> mockSaveFilePath(
List<String> mockPickFilePaths({required List<String> paths}) {
getIt.unregister<FilePickerService>();
getIt.registerFactory<FilePickerService>(
() => MockFilePicker(
mockPaths: paths,
),
() => MockFilePicker(mockPaths: paths),
);
return paths;
}

View File

@ -1,10 +1,11 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:mocktail/mocktail.dart';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
class MyMockClient extends Mock implements http.Client {
@override
@ -52,7 +53,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
final response = await client.send(request);
var previousSyntax = '';
String previousSyntax = '';
if (response.statusCode == 200) {
await for (final chunk in response.stream
.transform(const Utf8Decoder())
@ -76,6 +77,5 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
}
}
}
return;
}
}

View File

@ -27,9 +27,7 @@ class MockUrlLauncher extends Fake
bool launchCalled = false;
// ignore: use_setters_to_change_properties
void setCanLaunchExpectations(String url) {
this.url = url;
}
void setCanLaunchExpectations(String url) => this.url = url;
void setLaunchExpectations({
required String url,
@ -53,10 +51,7 @@ class MockUrlLauncher extends Fake
this.webOnlyWindowName = webOnlyWindowName;
}
// ignore: use_setters_to_change_properties
void setResponse(bool response) {
this.response = response;
}
void setResponse(bool response) => this.response = response;
@override
LinkDelegate? get linkDelegate => null;
@ -104,7 +99,5 @@ class MockUrlLauncher extends Fake
}
@override
Future<void> closeWebView() async {
closeWebViewCalled = true;
}
Future<void> closeWebView() async => closeWebViewCalled = true;
}

View File

@ -1,13 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
extension AppFlowySettings on WidgetTester {
@ -31,14 +35,6 @@ extension AppFlowySettings on WidgetTester {
return;
}
Future<void> expectNoSettingsPage(SettingsPage page) async {
final button = find.byWidgetPredicate(
(widget) => widget is SettingsMenuElement && widget.page == page,
);
expect(button, findsNothing);
return;
}
/// Restore the AppFlowy data storage location
Future<void> restoreLocation() async {
final button =
@ -48,13 +44,6 @@ extension AppFlowySettings on WidgetTester {
return;
}
Future<void> tapOpenFolderButton() async {
final button = find.text(LocaleKeys.settings_files_open.tr());
expect(button, findsOneWidget);
await tapButton(button);
return;
}
Future<void> tapCustomLocationButton() async {
final button = find.byTooltip(
LocaleKeys.settings_files_changeLocationTooltips.tr(),
@ -66,12 +55,22 @@ extension AppFlowySettings on WidgetTester {
/// Enter user name
Future<void> enterUserName(String name) async {
final uni = find.byType(UserNameInput);
expect(uni, findsOneWidget);
await tap(uni);
await enterText(uni, name);
await wait(300); //
await testTextInput.receiveAction(TextInputAction.done);
// Enable editing username
final editUsernameFinder = find.descendant(
of: find.byType(UserProfileSetting),
matching: find.byFlowySvg(FlowySvgs.edit_s),
);
await tap(editUsernameFinder);
await pumpAndSettle();
final userNameFinder = find.descendant(
of: find.byType(UserProfileSetting),
matching: find.byType(FlowyTextField),
);
await enterText(userNameFinder, name);
await pumpAndSettle();
await tap(find.text(LocaleKeys.button_save.tr()));
await pumpAndSettle();
}

View File

@ -1,18 +1,10 @@
import 'dart:typed_data';
import 'package:appflowy/core/notification/notification_helper.dart';
import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value
const String _source = 'Document';
typedef DocumentNotificationCallback = void Function(
DocumentNotification,
FlowyResult<Uint8List, FlowyError>,
);
class DocumentNotificationParser
extends NotificationParser<DocumentNotification, FlowyError> {
DocumentNotificationParser({

View File

@ -12,12 +12,6 @@ import 'notification_helper.dart';
// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value
const String _source = 'Workspace';
// Folder
typedef FolderNotificationCallback = void Function(
FolderNotification,
FlowyResult<Uint8List, FlowyError>,
);
class FolderNotificationParser
extends NotificationParser<FolderNotification, FlowyError> {
FolderNotificationParser({

View File

@ -12,12 +12,6 @@ import 'notification_helper.dart';
// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value
const String _source = 'Database';
// DatabasePB
typedef DatabaseNotificationCallback = void Function(
DatabaseNotification,
FlowyResult<Uint8List, FlowyError>,
);
class DatabaseNotificationParser
extends NotificationParser<DatabaseNotification, FlowyError> {
DatabaseNotificationParser({

View File

@ -13,11 +13,6 @@ import 'notification_helper.dart';
// This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE)
const _source = 'Search';
typedef SearchNotificationCallback = void Function(
SearchNotification,
FlowyResult<Uint8List, FlowyError>,
);
class SearchNotificationParser
extends NotificationParser<SearchNotification, FlowyError> {
SearchNotificationParser({

View File

@ -1,23 +1,11 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
// This value should be the same as the USER_OBSERVABLE_SOURCE value
const String _source = 'User';
// User
typedef UserNotificationCallback = void Function(
UserNotification,
FlowyResult<Uint8List, FlowyError>,
);
class UserNotificationParser
extends NotificationParser<UserNotification, FlowyError> {
UserNotificationParser({
@ -29,26 +17,3 @@ class UserNotificationParser
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
typedef UserNotificationHandler = Function(
UserNotification ty,
FlowyResult<Uint8List, FlowyError> result,
);
class UserNotificationListener {
UserNotificationListener({
required String objectId,
required UserNotificationHandler handler,
}) : _parser = UserNotificationParser(id: objectId, callback: handler) {
_subscription =
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
}
UserNotificationParser? _parser;
StreamSubscription<SubscribeObject>? _subscription;
Future<void> stop() async {
_parser = null;
await _subscription?.cancel();
}
}

View File

@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
class FlowyBoxContainer extends StatelessWidget {
const FlowyBoxContainer({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 6.0,
vertical: 8.0,
),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.onSecondary,
),
borderRadius: BorderRadius.circular(8.0),
),
child: child,
);
}
}

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@ -6,14 +8,13 @@ import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bott
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/built_in_svgs.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';

View File

@ -1,41 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
class FlowyEmojiPickerI18n extends EmojiPickerI18n {
@override
String get activity => LocaleKeys.emoji_categories_activities.tr();
@override
String get flags => LocaleKeys.emoji_categories_flags.tr();
@override
String get foods => LocaleKeys.emoji_categories_food.tr();
@override
String get frequent => LocaleKeys.emoji_categories_frequentlyUsed.tr();
@override
String get nature => LocaleKeys.emoji_categories_nature.tr();
@override
String get objects => LocaleKeys.emoji_categories_objects.tr();
@override
String get people => LocaleKeys.emoji_categories_smileys.tr();
@override
String get places => LocaleKeys.emoji_categories_places.tr();
@override
String get search => LocaleKeys.emoji_search.tr();
@override
String get symbols => LocaleKeys.emoji_categories_symbols.tr();
@override
String get searchHintText => LocaleKeys.emoji_search.tr();
@override
String get searchNoResult => LocaleKeys.emoji_noEmojiFound.tr();
}

View File

@ -6,11 +6,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'cell_controller.dart';
abstract class IGridCellDataConfig {
// The cell data will reload if it receives the field's change notification.
bool get reloadOnFieldChanged;
}
abstract class CellDataParser<T> {
T? parserData(List<int> data);
}

View File

@ -31,8 +31,6 @@ typedef OnNumOfRowsChanged = void Function(
ChangedReason reason,
);
typedef OnError = void Function(FlowyError);
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.idle() = _Idle;

View File

@ -1,13 +1,15 @@
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/setting/setting_listener.dart';
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
import 'package:appflowy/plugins/database/domain/field_listener.dart';
import 'package:appflowy/plugins/database/domain/field_settings_listener.dart';
import 'package:appflowy/plugins/database/domain/field_settings_service.dart';
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
import 'package:appflowy/plugins/database/application/setting/setting_listener.dart';
import 'package:appflowy/plugins/database/domain/sort_listener.dart';
import 'package:appflowy/plugins/database/domain/sort_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
@ -17,9 +19,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import '../setting/setting_service.dart';
import 'field_info.dart';
class _GridFieldNotifier extends ChangeNotifier {
@ -73,7 +75,7 @@ typedef OnReceiveField = void Function(FieldInfo);
typedef OnReceiveFields = void Function(List<FieldInfo>);
typedef OnReceiveFilters = void Function(List<FilterInfo>);
typedef OnReceiveSorts = void Function(List<SortInfo>);
typedef OnReceiveFieldSettings = void Function(List<FieldInfo>);
class FieldController {
FieldController({required this.viewId})

View File

@ -4,13 +4,7 @@ abstract class TypeOptionParser<T> {
T fromBuffer(List<int> buffer);
}
class RichTextTypeOptionDataParser
extends TypeOptionParser<RichTextTypeOptionPB> {
@override
RichTextTypeOptionPB fromBuffer(List<int> buffer) {
return RichTextTypeOptionPB.fromBuffer(buffer);
}
}
class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
@override
@ -19,21 +13,6 @@ class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
}
}
class CheckboxTypeOptionDataParser
extends TypeOptionParser<CheckboxTypeOptionPB> {
@override
CheckboxTypeOptionPB fromBuffer(List<int> buffer) {
return CheckboxTypeOptionPB.fromBuffer(buffer);
}
}
class URLTypeOptionDataParser extends TypeOptionParser<URLTypeOptionPB> {
@override
URLTypeOptionPB fromBuffer(List<int> buffer) {
return URLTypeOptionPB.fromBuffer(buffer);
}
}
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
@override
DateTypeOptionPB fromBuffer(List<int> buffer) {
@ -65,14 +44,6 @@ class MultiSelectTypeOptionDataParser
}
}
class ChecklistTypeOptionDataParser
extends TypeOptionParser<ChecklistTypeOptionPB> {
@override
ChecklistTypeOptionPB fromBuffer(List<int> buffer) {
return ChecklistTypeOptionPB.fromBuffer(buffer);
}
}
class RelationTypeOptionDataParser
extends TypeOptionParser<RelationTypeOptionPB> {
@override

View File

@ -9,8 +9,6 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
import 'package:protobuf/protobuf.dart';
typedef OnGroupError = void Function(FlowyError);
abstract class GroupControllerDelegate {
bool hasGroup(String groupId);
void removeRow(GroupPB group, RowId rowId);

View File

@ -1,43 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'board_setting_bloc.freezed.dart';
class BoardSettingBloc extends Bloc<BoardSettingEvent, BoardSettingState> {
BoardSettingBloc({required this.viewId})
: super(BoardSettingState.initial()) {
on<BoardSettingEvent>(
(event, emit) async {
event.when(
performAction: (action) {
emit(state.copyWith(selectedAction: action));
},
);
},
);
}
final String viewId;
}
@freezed
class BoardSettingEvent with _$BoardSettingEvent {
const factory BoardSettingEvent.performAction(BoardSettingAction action) =
_PerformAction;
}
@freezed
class BoardSettingState with _$BoardSettingState {
const factory BoardSettingState({
required BoardSettingAction? selectedAction,
}) = _BoardSettingState;
factory BoardSettingState.initial() => const BoardSettingState(
selectedAction: null,
);
}
enum BoardSettingAction {
properties,
groups,
}

View File

@ -456,16 +456,6 @@ class CalendarState with _$CalendarState {
);
}
class CalendarEditingRow {
CalendarEditingRow({
required this.row,
required this.index,
});
RowPB row;
int? index;
}
@freezed
class CalendarDayEvent with _$CalendarDayEvent {
const factory CalendarDayEvent({

View File

@ -8,8 +8,6 @@ import 'package:protobuf/protobuf.dart';
part 'calendar_setting_bloc.freezed.dart';
typedef DayOfWeek = int;
class CalendarSettingBloc
extends Bloc<CalendarSettingEvent, CalendarSettingState> {
CalendarSettingBloc({required DatabaseController databaseController})

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
@ -19,12 +21,12 @@ import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../application/row/row_controller.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_setting_bar.dart';
@ -265,6 +267,7 @@ class _CalendarPageState extends State<CalendarPage> {
fillColor: Colors.transparent,
fontWeight: FontWeight.w400,
fontSize: 10,
fontColor: AFThemeExtension.of(context).textColor,
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
hoverColor: AFThemeExtension.of(context).lightGreyHover,

View File

@ -1,16 +0,0 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
class DatabaseBackendService {
static Future<FlowyResult<List<DatabaseMetaPB>, FlowyError>>
getAllDatabases() {
return DatabaseEventGetDatabases().send().then((result) {
return result.fold(
(l) => FlowyResult.success(l.items),
(r) => FlowyResult.failure(r),
);
});
}
}

View File

@ -8,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef GroupConfigurationUpdateValue
= FlowyResult<List<GroupSettingPB>, FlowyError>;
typedef GroupUpdateValue = FlowyResult<GroupChangesPB, FlowyError>;
typedef GroupByNewFieldValue = FlowyResult<List<GroupPB>, FlowyError>;

View File

@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
@ -8,14 +9,9 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart';
import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/mobile_cell_container.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
import "package:appflowy/generated/locale_keys.g.dart";
import 'package:easy_localization/easy_localization.dart';
class MobileGridRow extends StatefulWidget {
const MobileGridRow({
@ -90,26 +86,6 @@ class _MobileGridRowState extends State<MobileGridRow> {
}
}
class InsertRowButton extends StatelessWidget {
const InsertRowButton({super.key});
@override
Widget build(BuildContext context) {
return FlowyIconButton(
tooltipText: LocaleKeys.tooltip_addNewRow.tr(),
hoverColor: AFThemeExtension.of(context).lightGreyHover,
width: 20,
height: 30,
onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
iconPadding: const EdgeInsets.all(3),
icon: FlowySvg(
FlowySvgs.add_s,
color: Theme.of(context).colorScheme.tertiary,
),
);
}
}
class RowContent extends StatelessWidget {
const RowContent({
super.key,

View File

@ -23,27 +23,12 @@ Map<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) {
return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};
}
Map<Type, Action<Intent>> bindActions() {
return {
KeyboardKeyIdent: KeyboardBindingAction(),
};
}
class KeyboardKeyIdent extends Intent {
const KeyboardKeyIdent(this.key);
final KeyboardKey key;
}
class KeyboardBindingAction extends Action<KeyboardKeyIdent> {
KeyboardBindingAction();
@override
void invoke(covariant KeyboardKeyIdent intent) {
// print(intent);
}
}
class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(

View File

@ -1,6 +1,7 @@
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
enum AccessoryType {
edit,
more,
@ -11,10 +12,6 @@ abstract mixin class CardAccessory implements Widget {
void onTap(BuildContext context) {}
}
typedef CardAccessoryBuilder = List<CardAccessory> Function(
BuildContext buildContext,
);
class CardAccessoryContainer extends StatelessWidget {
const CardAccessoryContainer({
super.key,

View File

@ -1,10 +0,0 @@
import 'package:flutter/material.dart';
class CellDecoration {
static BoxDecoration box({required Color color}) {
return BoxDecoration(
border: Border.all(color: Colors.black26, width: 0.2),
color: color,
);
}
}

View File

@ -96,21 +96,3 @@ class GridCellCopyAction extends Action<GridCellCopyIntent> {
}
}
}
class GridCellPasteIntent extends Intent {
const GridCellPasteIntent();
}
class GridCellPasteAction extends Action<GridCellPasteIntent> {
GridCellPasteAction({required this.child});
final CellShortcuts child;
@override
void invoke(covariant GridCellPasteIntent intent) {
final callback = child.shortcutHandlers[CellKeyboardKey.onInsert];
if (callback != null) {
callback();
}
}
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart';
@ -8,7 +11,6 @@ import 'package:appflowy/plugins/database/domain/database_view_service.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database_document/database_document_plugin.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@ -19,10 +21,8 @@ import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/em
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
typedef OnSubmittedEmoji = void Function(String emoji);
const _kBannerActionHeight = 40.0;
class RowBanner extends StatefulWidget {

View File

@ -1,147 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart';
class OptionActionList extends StatelessWidget {
const OptionActionList({
super.key,
required this.blockComponentContext,
required this.blockComponentState,
required this.actions,
required this.editorState,
});
final BlockComponentContext blockComponentContext;
final BlockComponentActionState blockComponentState;
final List<OptionAction> actions;
final EditorState editorState;
@override
Widget build(BuildContext context) {
final popoverActions = actions.map((e) {
if (e == OptionAction.divider) {
return DividerOptionAction();
} else if (e == OptionAction.color) {
return ColorOptionAction(
editorState: editorState,
);
} else if (e == OptionAction.depth) {
return DepthOptionAction(
editorState: editorState,
);
} else {
return OptionActionWrapper(e);
}
}).toList();
return PopoverActionList<PopoverAction>(
popoverMutex: PopoverMutex(),
direction: PopoverDirection.leftWithCenterAligned,
actions: popoverActions,
onPopupBuilder: () => blockComponentState.alwaysShowActions = true,
onClosed: () {
editorState.selectionType = null;
editorState.selection = null;
blockComponentState.alwaysShowActions = false;
},
onSelected: (action, controller) {
if (action is OptionActionWrapper) {
_onSelectAction(action.inner);
controller.close();
}
},
buildChild: (controller) => OptionActionButton(
onTap: () {
controller.show();
// update selection
_updateBlockSelection();
},
),
);
}
void _updateBlockSelection() {
final startNode = blockComponentContext.node;
var endNode = startNode;
while (endNode.children.isNotEmpty) {
endNode = endNode.children.last;
}
final start = Position(path: startNode.path);
final end = endNode.selectable?.end() ??
Position(
path: endNode.path,
offset: endNode.delta?.length ?? 0,
);
editorState.selectionType = SelectionType.block;
editorState.selection = Selection(
start: start,
end: end,
);
}
void _onSelectAction(OptionAction action) {
final node = blockComponentContext.node;
final transaction = editorState.transaction;
switch (action) {
case OptionAction.delete:
transaction.deleteNode(node);
break;
case OptionAction.duplicate:
transaction.insertNode(
node.path.next,
node.copyWith(),
);
break;
case OptionAction.turnInto:
break;
case OptionAction.moveUp:
transaction.moveNode(node.path.previous, node);
break;
case OptionAction.moveDown:
transaction.moveNode(node.path.next.next, node);
break;
case OptionAction.align:
case OptionAction.color:
case OptionAction.divider:
case OptionAction.depth:
throw UnimplementedError();
}
editorState.apply(transaction);
}
}
class OptionActionButton extends StatelessWidget {
const OptionActionButton({
super.key,
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Align(
child: MouseRegion(
cursor: SystemMouseCursors.grab,
child: IgnoreParentGestureWidget(
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.deferToChild,
child: FlowySvg(
FlowySvgs.drag_element_s,
size: const Size.square(24.0),
color: Theme.of(context).iconTheme.color,
),
),
),
),
);
}
}

View File

@ -1,19 +1,8 @@
import 'dart:io';
import 'dart:ui';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
const String kLocalImagesKey = 'local_images';
@ -22,302 +11,6 @@ List<String> get builtInAssetImages => [
"assets/images/app_flowy_abstract_cover_2.jpg",
];
class ChangeCoverPopover extends StatefulWidget {
const ChangeCoverPopover({
super.key,
required this.editorState,
required this.node,
required this.onCoverChanged,
});
final EditorState editorState;
final Node node;
final Function(
CoverType selectionType,
String selection,
) onCoverChanged;
@override
State<ChangeCoverPopover> createState() => _ChangeCoverPopoverState();
}
class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
bool isAddingImage = false;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ChangeCoverPopoverBloc(
editorState: widget.editorState,
node: widget.node,
)..add(const ChangeCoverPopoverEvent.fetchPickedImagePaths()),
child: BlocConsumer<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
listener: (context, state) {
if (state is Loaded && state.selectLatestImage) {
widget.onCoverChanged(
CoverType.file,
state.imageNames.last,
);
}
},
builder: (context, state) {
return Padding(
padding: const EdgeInsets.all(12),
child: SingleChildScrollView(
child: isAddingImage
? CoverImagePicker(
onBackPressed: () => setState(() {
isAddingImage = false;
}),
onFileSubmit: (_) {
context.read<ChangeCoverPopoverBloc>().add(
const ChangeCoverPopoverEvent
.fetchPickedImagePaths(
selectLatestImage: true,
),
);
setState(() => isAddingImage = false);
},
)
: _buildCoverSelection(),
),
);
},
),
);
}
Widget _buildCoverSelection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(
LocaleKeys.document_plugins_cover_colors.tr(),
color: Theme.of(context).colorScheme.tertiary,
),
const VSpace(10),
_buildColorPickerList(),
const VSpace(10),
_buildImageHeader(),
const VSpace(10),
_buildFileImagePicker(),
const VSpace(10),
FlowyText.semibold(
LocaleKeys.document_plugins_cover_abstract.tr(),
color: Theme.of(context).colorScheme.tertiary,
),
const VSpace(10),
_buildAbstractImagePicker(),
],
);
}
Widget _buildImageHeader() {
return BlocBuilder<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
builder: (context, state) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FlowyText.semibold(
LocaleKeys.document_plugins_cover_images.tr(),
color: Theme.of(context).colorScheme.tertiary,
),
FlowyTextButton(
fillColor: Theme.of(context).cardColor,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
LocaleKeys.document_plugins_cover_clearAll.tr(),
fontColor: Theme.of(context).colorScheme.tertiary,
onPressed: () async {
final hasFileImageCover = CoverType.fromString(
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
) ==
CoverType.file;
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
if (hasFileImageCover) {
await showDialog(
context: context,
builder: (context) {
return DeleteImageAlertDialog(
onSubmit: () {
changeCoverBloc.add(
const ChangeCoverPopoverEvent.clearAllImages(),
);
Navigator.pop(context);
},
);
},
);
} else {
context
.read<ChangeCoverPopoverBloc>()
.add(const ChangeCoverPopoverEvent.clearAllImages());
}
},
mainAxisAlignment: MainAxisAlignment.end,
),
],
);
},
);
}
Widget _buildAbstractImagePicker() {
return GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1 / 0.65,
crossAxisSpacing: 7,
mainAxisSpacing: 7,
),
itemCount: builtInAssetImages.length,
itemBuilder: (BuildContext ctx, index) {
return InkWell(
onTap: () {
widget.onCoverChanged(
CoverType.asset,
builtInAssetImages[index],
);
},
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(builtInAssetImages[index]),
fit: BoxFit.cover,
),
borderRadius: Corners.s8Border,
),
),
);
},
);
}
Widget _buildColorPickerList() {
final theme = Theme.of(context);
return CoverColorPicker(
pickerBackgroundColor: theme.cardColor,
pickerItemHoverColor: theme.hoverColor,
selectedBackgroundColorHex:
widget.node.attributes[DocumentHeaderBlockKeys.coverType] ==
CoverType.color.toString()
? widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]
: 'ffffff',
backgroundColorOptions:
_generateBackgroundColorOptions(widget.editorState),
onSubmittedBackgroundColorHex: (color) {
widget.onCoverChanged(CoverType.color, color);
setState(() {});
},
);
}
Widget _buildFileImagePicker() {
return BlocBuilder<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
builder: (context, state) {
if (state is Loaded) {
final List<String> images = state.imageNames;
return GridView.builder(
shrinkWrap: true,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1 / 0.65,
crossAxisSpacing: 7,
mainAxisSpacing: 7,
),
itemCount: images.length + 1,
itemBuilder: (BuildContext ctx, index) {
if (index == 0) {
return NewCustomCoverButton(
onPressed: () => setState(
() => isAddingImage = true,
),
);
}
return ImageGridItem(
onImageSelect: () {
widget.onCoverChanged(
CoverType.file,
images[index - 1],
);
},
onImageDelete: () async {
final changeCoverBloc =
context.read<ChangeCoverPopoverBloc>();
final deletingCurrentCover = widget.node
.attributes[DocumentHeaderBlockKeys.coverDetails] ==
images[index - 1];
if (deletingCurrentCover) {
await showDialog(
context: context,
builder: (context) {
return DeleteImageAlertDialog(
onSubmit: () {
changeCoverBloc.add(
ChangeCoverPopoverEvent.deleteImage(
images[index - 1],
),
);
Navigator.pop(context);
},
);
},
);
} else {
changeCoverBloc.add(DeleteImage(images[index - 1]));
}
},
imagePath: images[index - 1],
);
},
);
}
return const SizedBox.shrink();
},
);
}
List<ColorOption> _generateBackgroundColorOptions(EditorState editorState) {
return FlowyTint.values
.map(
(t) => ColorOption(
colorHex: t.color(context).toHex(),
name: t.tintName(AppFlowyEditorL10n.current),
),
)
.toList();
}
}
@visibleForTesting
class NewCustomCoverButton extends StatelessWidget {
const NewCustomCoverButton({super.key, required this.onPressed});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
borderRadius: Corners.s8Border,
),
child: FlowyIconButton(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.primary,
),
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.15),
onPressed: onPressed,
),
);
}
}
class ColorOption {
const ColorOption({
required this.colorHex,
@ -398,122 +91,6 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
}
}
class DeleteImageAlertDialog extends StatelessWidget {
const DeleteImageAlertDialog({
super.key,
required this.onSubmit,
});
final Function() onSubmit;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: FlowyText.semibold(
"Image is used in cover",
fontSize: 20,
color: Theme.of(context).colorScheme.tertiary,
),
content: Container(
constraints: const BoxConstraints(minHeight: 100),
padding: const EdgeInsets.symmetric(
vertical: 20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(LocaleKeys.document_plugins_cover_coverRemoveAlert).tr(),
const SizedBox(
height: 4,
),
const Text(
LocaleKeys.document_plugins_cover_alertDialogConfirmation,
).tr(),
],
),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 20.0,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(LocaleKeys.button_cancel).tr(),
),
TextButton(
onPressed: onSubmit,
child: const Text(LocaleKeys.button_ok).tr(),
),
],
);
}
}
class ImageGridItem extends StatefulWidget {
const ImageGridItem({
super.key,
required this.onImageSelect,
required this.onImageDelete,
required this.imagePath,
});
final Function() onImageSelect;
final Function() onImageDelete;
final String imagePath;
@override
State<ImageGridItem> createState() => _ImageGridItemState();
}
class _ImageGridItemState extends State<ImageGridItem> {
bool showDeleteButton = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) {
setState(() {
showDeleteButton = true;
});
},
onExit: (_) {
setState(() {
showDeleteButton = false;
});
},
child: Stack(
children: [
InkWell(
onTap: widget.onImageSelect,
child: ClipRRect(
borderRadius: Corners.s8Border,
child: Image.file(File(widget.imagePath), fit: BoxFit.cover),
),
),
if (showDeleteButton)
Positioned(
right: 2,
top: 2,
child: FlowyIconButton(
fillColor:
Theme.of(context).colorScheme.surface.withOpacity(0.8),
hoverColor:
Theme.of(context).colorScheme.surface.withOpacity(0.8),
iconPadding: const EdgeInsets.all(5),
width: 28,
icon: FlowySvg(
FlowySvgs.delete_s,
color: Theme.of(context).colorScheme.tertiary,
),
onPressed: widget.onImageDelete,
),
),
],
),
);
}
}
@visibleForTesting
class ColorItem extends StatelessWidget {
const ColorItem({

View File

@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class Svg extends StatelessWidget {
const Svg({
super.key,
this.name,
this.width,
this.height,
this.color,
this.number,
this.padding,
});
final String? name;
final double? width;
final double? height;
final Color? color;
final int? number;
final EdgeInsets? padding;
final _defaultWidth = 20.0;
final _defaultHeight = 20.0;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.all(0),
child: _buildSvg(),
);
}
Widget _buildSvg() {
if (name != null) {
return SvgPicture.asset(
'assets/images/$name.svg',
colorFilter:
color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null,
fit: BoxFit.fill,
height: height,
width: width,
package: 'appflowy_editor_plugins',
);
} else if (number != null) {
final numberText =
'<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg"><text x="30" y="150" fill="black" font-size="160">$number.</text></svg>';
return SvgPicture.string(
numberText,
width: width ?? _defaultWidth,
height: height ?? _defaultHeight,
);
}
return const SizedBox.shrink();
}
}

View File

@ -1,271 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
// convert the current block to other block types
// only show in single selection and text type
final mobileAddBlockToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, editorState, ___) {
if (!onlyShowInSingleSelectionAndTextType(editorState)) {
return null;
}
return const FlowySvg(
FlowySvgs.add_m,
size: Size.square(48),
);
},
itemMenuBuilder: (_, editorState, service) {
final selection = editorState.selection;
if (selection == null) {
return null;
}
return BlocksMenu(
items: _addBlockMenuItems,
editorState: editorState,
service: service,
);
},
);
final _addBlockMenuItems = [
// paragraph
BlockMenuItem(
blockType: ParagraphBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_text_decoration_m),
label: LocaleKeys.editor_text.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
paragraphNode(),
);
},
),
// to-do list
BlockMenuItem(
blockType: TodoListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_checkbox_m),
label: LocaleKeys.editor_checkbox.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
todoListNode(checked: false),
);
},
),
// heading 1 - 3
BlockMenuItem(
blockType: HeadingBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_h1_m),
label: LocaleKeys.editor_heading1.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
headingNode(level: 1),
);
},
),
BlockMenuItem(
blockType: HeadingBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_h2_m),
label: LocaleKeys.editor_heading2.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
headingNode(level: 2),
);
},
),
BlockMenuItem(
blockType: HeadingBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_h3_m),
label: LocaleKeys.editor_heading3.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
headingNode(level: 3),
);
},
),
// bulleted list
BlockMenuItem(
blockType: BulletedListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_bulleted_list_m),
label: LocaleKeys.editor_bulletedList.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
bulletedListNode(),
);
},
),
// numbered list
BlockMenuItem(
blockType: NumberedListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_numbered_list_m),
label: LocaleKeys.editor_numberedList.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
numberedListNode(),
);
},
),
// toggle list
BlockMenuItem(
blockType: ToggleListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_toggle_list_m),
label: LocaleKeys.document_plugins_toggleList.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
toggleListBlockNode(),
);
},
),
// quote
BlockMenuItem(
blockType: QuoteBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_quote_m),
label: LocaleKeys.editor_quote.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
quoteNode(),
);
},
),
// callout
BlockMenuItem(
blockType: CalloutBlockKeys.type,
icon: const Icon(Icons.note_rounded),
label: LocaleKeys.document_plugins_callout.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
calloutNode(),
);
},
),
// code
BlockMenuItem(
blockType: CodeBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_code_m),
label: LocaleKeys.document_selectionMenu_codeBlock.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertBlockOrReplaceCurrentBlock(
selection,
codeBlockNode(),
);
},
),
// divider
BlockMenuItem(
blockType: DividerBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_divider_m),
label: LocaleKeys.editor_divider.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertDivider(selection);
},
),
// math equation
BlockMenuItem(
blockType: MathEquationBlockKeys.type,
icon: const FlowySvg(
FlowySvgs.math_lg,
size: Size.square(22),
),
label: LocaleKeys.document_plugins_mathEquation_name.tr(),
isSelected: _unSelectable,
onTap: (editorState, selection, service) async {
service.closeItemMenu();
await editorState.insertMathEquation(selection);
},
),
];
bool _unSelectable(
EditorState editorState,
Selection selection,
) {
return false;
}
extension EditorStateAddBlock on EditorState {
Future<void> insertBlockOrReplaceCurrentBlock(
Selection selection,
Node insertedNode,
) async {
// If the current block is not an empty paragraph block,
// then insert a new block below the current block.
final node = getNodeAtPath(selection.start.path);
if (node == null) {
return;
}
final transaction = this.transaction;
if (node.type != ParagraphBlockKeys.type ||
(node.delta?.isNotEmpty ?? true)) {
final path = node.path.next;
// insert the block below the current empty paragraph block
transaction
..insertNode(path, insertedNode)
..afterSelection = Selection.collapsed(
Position(path: path),
);
} else {
final path = node.path;
// replace the current empty paragraph block with the inserted block
transaction
..insertNode(path, insertedNode)
..deleteNode(node)
..afterSelection = Selection.collapsed(
Position(path: path),
)
..selectionExtraInfo = null;
}
await apply(transaction);
service.keyboardService?.enableKeyBoard(selection);
}
Future<void> insertMathEquation(
Selection selection,
) async {

View File

@ -1,108 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
final mobileAlignToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, editorState, __) {
return onlyShowInTextType(editorState)
? const FlowySvg(
FlowySvgs.toolbar_align_center_s,
size: Size.square(32),
)
: null;
},
itemMenuBuilder: (_, editorState, ___) {
final selection = editorState.selection;
if (selection == null) {
return null;
}
return _MobileAlignMenu(
editorState: editorState,
selection: selection,
);
},
);
class _MobileAlignMenu extends StatelessWidget {
const _MobileAlignMenu({
required this.editorState,
required this.selection,
});
final Selection selection;
final EditorState editorState;
@override
Widget build(BuildContext context) {
return GridView.count(
padding: EdgeInsets.zero,
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 3,
shrinkWrap: true,
children: [
_buildAlignmentButton(
context,
'left',
LocaleKeys.document_plugins_optionAction_left.tr(),
),
_buildAlignmentButton(
context,
'center',
LocaleKeys.document_plugins_optionAction_center.tr(),
),
_buildAlignmentButton(
context,
'right',
LocaleKeys.document_plugins_optionAction_right.tr(),
),
],
);
}
Widget _buildAlignmentButton(
BuildContext context,
String alignment,
String label,
) {
final nodes = editorState.getNodesInSelection(selection);
if (nodes.isEmpty) {
const SizedBox.shrink();
}
bool isSatisfyCondition(bool Function(Object? value) test) {
return nodes.every(
(n) => test(n.attributes[blockComponentAlign]),
);
}
final data = switch (alignment) {
'left' => FlowySvgs.toolbar_align_left_s,
'center' => FlowySvgs.toolbar_align_center_s,
'right' => FlowySvgs.toolbar_align_right_s,
_ => throw UnimplementedError(),
};
final isSelected = isSatisfyCondition((value) => value == alignment);
return MobileToolbarItemMenuBtn(
icon: FlowySvg(data, size: const Size.square(28)),
label: FlowyText(label),
isSelected: isSelected,
onPressed: () async {
await editorState.updateNode(
selection,
(node) => node.copyWith(
attributes: {
...node.attributes,
blockComponentAlign: alignment,
},
),
);
},
);
}
}

View File

@ -1,134 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart';
import 'package:appflowy/plugins/base/color/color_picker_screen.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final mobileBlockSettingsToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, editorState, __) {
return onlyShowInSingleSelectionAndTextType(editorState)
? const FlowySvg(FlowySvgs.three_dots_s)
: null;
},
actionHandler: (_, editorState) async {
// show the settings page
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
final context = node?.context;
if (node == null || context == null) {
return;
}
await _showBlockActionSheet(
context,
editorState,
node,
selection,
);
},
);
Future<void> _showBlockActionSheet(
BuildContext context,
EditorState editorState,
Node node,
Selection selection,
) async {
final result = await showMobileBottomSheet<bool>(
context,
showDragHandle: true,
showCloseButton: true,
showHeader: true,
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
title: LocaleKeys.document_plugins_action.tr(),
builder: (context) {
return BlockActionBottomSheet(
extendActionWidgets: [
Row(
children: [
Expanded(
child: BottomSheetActionWidget(
svg: FlowySvgs.m_color_m,
text: LocaleKeys.document_plugins_optionAction_color.tr(),
onTap: () async {
final option = await context.push<FlowyColorOption?>(
Uri(
path: MobileColorPickerScreen.routeName,
queryParameters: {
MobileColorPickerScreen.pageTitle: LocaleKeys
.document_plugins_optionAction_color
.tr(),
},
).toString(),
);
if (option != null) {
final transaction = editorState.transaction;
transaction.updateNode(node, {
blockComponentBackgroundColor: option.id,
});
await editorState.apply(transaction);
}
if (context.mounted) {
context.pop(true);
}
},
),
),
// more options ...
],
),
],
onAction: (action) async {
context.pop(true);
final transaction = editorState.transaction;
switch (action) {
case BlockActionBottomSheetType.delete:
transaction.deleteNode(node);
break;
case BlockActionBottomSheetType.duplicate:
transaction.insertNode(
node.path.next,
node.copyWith(),
);
break;
case BlockActionBottomSheetType.insertAbove:
case BlockActionBottomSheetType.insertBelow:
final path = action == BlockActionBottomSheetType.insertAbove
? node.path
: node.path.next;
transaction
..insertNode(
path,
paragraphNode(),
)
..afterSelection = Selection.collapsed(
Position(
path: path,
),
);
break;
default:
}
if (transaction.operations.isNotEmpty) {
await editorState.apply(transaction);
}
},
);
},
);
if (result != true) {
// restore the selection
editorState.selection = selection;
}
}

View File

@ -1,90 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class BlockMenuItem {
const BlockMenuItem({
required this.blockType,
required this.icon,
required this.label,
required this.onTap,
this.isSelected,
});
// block type
final String blockType;
final Widget icon;
final String label;
// callback
final void Function(
EditorState editorState,
Selection selection,
// used to control the open or close the menu
MobileToolbarWidgetService service,
) onTap;
final bool Function(
EditorState editorState,
Selection selection,
)? isSelected;
}
class BlocksMenu extends StatelessWidget {
const BlocksMenu({
super.key,
required this.editorState,
required this.items,
required this.service,
});
final EditorState editorState;
final List<BlockMenuItem> items;
final MobileToolbarWidgetService service;
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 4,
padding: const EdgeInsets.only(
bottom: 36.0,
),
shrinkWrap: true,
children: items.map((item) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
bool isSelected = false;
if (item.isSelected != null) {
isSelected = item.isSelected!(editorState, selection);
} else {
isSelected = _isSelected(editorState, selection, item.blockType);
}
return MobileToolbarItemMenuBtn(
icon: item.icon,
label: FlowyText(item.label),
isSelected: isSelected,
onPressed: () async {
item.onTap(editorState, selection, service);
},
);
}).toList(),
);
}
bool _isSelected(
EditorState editorState,
Selection selection,
String blockType,
) {
final node = editorState.getNodeAtPath(selection.start.path);
final type = node?.type;
if (node == null || type == null) {
return false;
}
return type == blockType;
}
}

View File

@ -1,222 +0,0 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
// convert the current block to other block types
// only show in single selection and text type
final mobileConvertBlockToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, editorState, ___) {
if (!onlyShowInSingleSelectionAndTextType(editorState)) {
return null;
}
return const FlowySvg(
FlowySvgs.convert_s,
size: Size.square(22),
);
},
itemMenuBuilder: (_, editorState, service) {
final selection = editorState.selection;
if (selection == null) {
return null;
}
return BlocksMenu(
items: _convertToBlockMenuItems,
editorState: editorState,
service: service,
);
},
);
final _convertToBlockMenuItems = [
// paragraph
BlockMenuItem(
blockType: ParagraphBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_text_decoration_m),
label: LocaleKeys.editor_text.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
ParagraphBlockKeys.type,
selection: selection,
),
),
// to-do list
BlockMenuItem(
blockType: TodoListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_checkbox_m),
label: LocaleKeys.editor_checkbox.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
TodoListBlockKeys.type,
selection: selection,
extraAttributes: {
TodoListBlockKeys.checked: false,
},
),
),
// heading 1 - 3
BlockMenuItem(
blockType: HeadingBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_h1_m),
label: LocaleKeys.editor_heading1.tr(),
isSelected: (editorState, selection) => _isHeadingSelected(
editorState,
selection,
1,
),
onTap: (editorState, selection, _) {
final isSelected = _isHeadingSelected(
editorState,
selection,
1,
);
editorState.convertBlockType(
HeadingBlockKeys.type,
selection: selection,
isSelected: isSelected,
extraAttributes: {
HeadingBlockKeys.level: 1,
},
);
},
),
BlockMenuItem(
blockType: HeadingBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_h2_m),
label: LocaleKeys.editor_heading2.tr(),
isSelected: (editorState, selection) => _isHeadingSelected(
editorState,
selection,
2,
),
onTap: (editorState, selection, _) {
final isSelected = _isHeadingSelected(
editorState,
selection,
2,
);
editorState.convertBlockType(
HeadingBlockKeys.type,
selection: selection,
isSelected: isSelected,
extraAttributes: {
HeadingBlockKeys.level: 2,
},
);
},
),
BlockMenuItem(
blockType: HeadingBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_h3_m),
label: LocaleKeys.editor_heading3.tr(),
isSelected: (editorState, selection) => _isHeadingSelected(
editorState,
selection,
3,
),
onTap: (editorState, selection, _) {
final isSelected = _isHeadingSelected(
editorState,
selection,
3,
);
editorState.convertBlockType(
HeadingBlockKeys.type,
selection: selection,
isSelected: isSelected,
extraAttributes: {
HeadingBlockKeys.level: 3,
},
);
},
),
// bulleted list
BlockMenuItem(
blockType: BulletedListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_bulleted_list_m),
label: LocaleKeys.editor_bulletedList.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
BulletedListBlockKeys.type,
selection: selection,
),
),
// numbered list
BlockMenuItem(
blockType: NumberedListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_numbered_list_m),
label: LocaleKeys.editor_numberedList.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
NumberedListBlockKeys.type,
selection: selection,
),
),
// toggle list
BlockMenuItem(
blockType: ToggleListBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_toggle_list_m),
label: LocaleKeys.document_plugins_toggleList.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
selection: selection,
ToggleListBlockKeys.type,
),
),
// quote
BlockMenuItem(
blockType: QuoteBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_quote_m),
label: LocaleKeys.editor_quote.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
selection: selection,
QuoteBlockKeys.type,
),
),
// callout
BlockMenuItem(
blockType: CalloutBlockKeys.type,
// FIXME: update icon
icon: const Icon(Icons.note_rounded),
label: LocaleKeys.document_plugins_callout.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
CalloutBlockKeys.type,
selection: selection,
extraAttributes: {
CalloutBlockKeys.icon: '📌',
},
),
),
// code
BlockMenuItem(
blockType: CodeBlockKeys.type,
icon: const FlowySvg(FlowySvgs.m_code_m),
label: LocaleKeys.document_selectionMenu_codeBlock.tr(),
onTap: (editorState, selection, _) => editorState.convertBlockType(
CodeBlockKeys.type,
selection: selection,
),
),
];
bool _isHeadingSelected(
EditorState editorState,
Selection selection,
int level,
) {
final node = editorState.getNodeAtPath(selection.start.path);
final type = node?.type;
if (node == null || type == null) {
return false;
}
return type == HeadingBlockKeys.type &&
node.attributes[HeadingBlockKeys.level] == level;
}

View File

@ -1,24 +0,0 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final mobileIndentToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, editorState, __) {
return onlyShowInTextType(editorState)
? const Icon(Icons.format_indent_increase_rounded)
: null;
},
actionHandler: (_, editorState) {
indentCommand.execute(editorState);
},
);
final mobileOutdentToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, editorState, __) {
return onlyShowInTextType(editorState)
? const Icon(Icons.format_indent_decrease_rounded)
: null;
},
actionHandler: (_, editorState) {
outdentCommand.execute(editorState);
},
);

View File

@ -1,217 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, __, ___) => const FlowySvg(
FlowySvgs.text_s,
size: Size.square(24),
),
itemMenuBuilder: (_, editorState, service) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
return _TextDecorationMenu(
editorState,
selection,
service,
);
},
);
class _TextDecorationMenu extends StatefulWidget {
const _TextDecorationMenu(
this.editorState,
this.selection,
this.service,
);
final EditorState editorState;
final Selection selection;
final MobileToolbarWidgetService service;
@override
State<_TextDecorationMenu> createState() => _TextDecorationMenuState();
}
class _TextDecorationMenuState extends State<_TextDecorationMenu> {
EditorState get editorState => widget.editorState;
final textDecorations = [
// BIUS
TextDecorationUnit(
icon: AFMobileIcons.bold,
label: AppFlowyEditorL10n.current.bold,
name: AppFlowyRichTextKeys.bold,
),
TextDecorationUnit(
icon: AFMobileIcons.italic,
label: AppFlowyEditorL10n.current.italic,
name: AppFlowyRichTextKeys.italic,
),
TextDecorationUnit(
icon: AFMobileIcons.underline,
label: AppFlowyEditorL10n.current.underline,
name: AppFlowyRichTextKeys.underline,
),
TextDecorationUnit(
icon: AFMobileIcons.strikethrough,
label: AppFlowyEditorL10n.current.strikethrough,
name: AppFlowyRichTextKeys.strikethrough,
),
// Code
TextDecorationUnit(
icon: AFMobileIcons.code,
label: AppFlowyEditorL10n.current.embedCode,
name: AppFlowyRichTextKeys.code,
),
// link
TextDecorationUnit(
icon: AFMobileIcons.link,
label: AppFlowyEditorL10n.current.link,
name: AppFlowyRichTextKeys.href,
),
];
@override
void dispose() {
widget.editorState.selectionExtraInfo = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final children = textDecorations
.map((currentDecoration) {
// Check current decoration is active or not
final selection = widget.selection;
// only show edit link bottom sheet when selection is not collapsed
if (selection.isCollapsed &&
currentDecoration.name == AppFlowyRichTextKeys.href) {
return null;
}
final nodes = editorState.getNodesInSelection(selection);
final bool isSelected;
if (selection.isCollapsed) {
isSelected = editorState.toggledStyle.containsKey(
currentDecoration.name,
);
} else {
isSelected = nodes.allSatisfyInSelection(selection, (delta) {
return delta.everyAttributes(
(attributes) => attributes[currentDecoration.name] == true,
);
});
}
return MobileToolbarItemMenuBtn(
icon: AFMobileIcon(
afMobileIcons: currentDecoration.icon,
color: MobileToolbarTheme.of(context).iconColor,
),
label: FlowyText(currentDecoration.label),
isSelected: isSelected,
onPressed: () {
if (currentDecoration.name == AppFlowyRichTextKeys.href) {
if (selection.isCollapsed) {
return;
}
_closeKeyboard();
// show edit link bottom sheet
final context = nodes.firstOrNull?.context;
if (context != null) {
final text = editorState
.getTextInSelection(
widget.selection,
)
.join();
final href =
editorState.getDeltaAttributeValueInSelection<String>(
AppFlowyRichTextKeys.href,
widget.selection,
);
showEditLinkBottomSheet(
context,
text,
href,
(context, newText, newHref) {
_updateTextAndHref(text, href, newText, newHref);
context.pop();
},
);
}
} else {
setState(() {
editorState.toggleAttribute(currentDecoration.name);
});
}
},
);
})
.nonNulls
.toList();
return GridView.count(
shrinkWrap: true,
padding: EdgeInsets.zero,
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 4,
children: children,
);
}
void _closeKeyboard() {
editorState.updateSelectionWithReason(
widget.selection,
extraInfo: {
selectionExtraInfoDisableMobileToolbarKey: true,
},
);
editorState.service.keyboardService?.closeKeyboard();
}
void _updateTextAndHref(
String prevText,
String? prevHref,
String text,
String href,
) async {
final selection = widget.selection;
if (!selection.isSingle) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null) {
return;
}
final transaction = editorState.transaction;
if (prevText != text) {
transaction.replaceText(
node,
selection.startIndex,
selection.length,
text,
);
}
// if the text is empty, it means the user wants to remove the text
if (text.isNotEmpty && prevHref != href) {
transaction.formatText(node, selection.startIndex, text.length, {
AppFlowyRichTextKeys.href: href.isEmpty ? null : href,
});
}
await editorState.apply(transaction);
}
}

View File

@ -1,217 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu(
itemIconBuilder: (_, __, ___) => const FlowySvg(
FlowySvgs.text_s,
size: Size.square(24),
),
itemMenuBuilder: (_, editorState, service) {
final selection = editorState.selection;
if (selection == null) {
return const SizedBox.shrink();
}
return _TextDecorationMenu(
editorState,
selection,
service,
);
},
);
class _TextDecorationMenu extends StatefulWidget {
const _TextDecorationMenu(
this.editorState,
this.selection,
this.service,
);
final EditorState editorState;
final Selection selection;
final MobileToolbarWidgetService service;
@override
State<_TextDecorationMenu> createState() => _TextDecorationMenuState();
}
class _TextDecorationMenuState extends State<_TextDecorationMenu> {
EditorState get editorState => widget.editorState;
final textDecorations = [
// BIUS
TextDecorationUnit(
icon: AFMobileIcons.bold,
label: AppFlowyEditorL10n.current.bold,
name: AppFlowyRichTextKeys.bold,
),
TextDecorationUnit(
icon: AFMobileIcons.italic,
label: AppFlowyEditorL10n.current.italic,
name: AppFlowyRichTextKeys.italic,
),
TextDecorationUnit(
icon: AFMobileIcons.underline,
label: AppFlowyEditorL10n.current.underline,
name: AppFlowyRichTextKeys.underline,
),
TextDecorationUnit(
icon: AFMobileIcons.strikethrough,
label: AppFlowyEditorL10n.current.strikethrough,
name: AppFlowyRichTextKeys.strikethrough,
),
// Code
TextDecorationUnit(
icon: AFMobileIcons.code,
label: AppFlowyEditorL10n.current.embedCode,
name: AppFlowyRichTextKeys.code,
),
// link
TextDecorationUnit(
icon: AFMobileIcons.link,
label: AppFlowyEditorL10n.current.link,
name: AppFlowyRichTextKeys.href,
),
];
@override
void dispose() {
widget.editorState.selectionExtraInfo = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final children = textDecorations
.map((currentDecoration) {
// Check current decoration is active or not
final selection = widget.selection;
// only show edit link bottom sheet when selection is not collapsed
if (selection.isCollapsed &&
currentDecoration.name == AppFlowyRichTextKeys.href) {
return null;
}
final nodes = editorState.getNodesInSelection(selection);
final bool isSelected;
if (selection.isCollapsed) {
isSelected = editorState.toggledStyle.containsKey(
currentDecoration.name,
);
} else {
isSelected = nodes.allSatisfyInSelection(selection, (delta) {
return delta.everyAttributes(
(attributes) => attributes[currentDecoration.name] == true,
);
});
}
return MobileToolbarItemMenuBtn(
icon: AFMobileIcon(
afMobileIcons: currentDecoration.icon,
color: MobileToolbarTheme.of(context).iconColor,
),
label: FlowyText(currentDecoration.label),
isSelected: isSelected,
onPressed: () {
if (currentDecoration.name == AppFlowyRichTextKeys.href) {
if (selection.isCollapsed) {
return;
}
_closeKeyboard();
// show edit link bottom sheet
final context = nodes.firstOrNull?.context;
if (context != null) {
final text = editorState
.getTextInSelection(
widget.selection,
)
.join();
final href =
editorState.getDeltaAttributeValueInSelection<String>(
AppFlowyRichTextKeys.href,
widget.selection,
);
showEditLinkBottomSheet(
context,
text,
href,
(context, newText, newHref) {
_updateTextAndHref(text, href, newText, newHref);
context.pop();
},
);
}
} else {
setState(() {
editorState.toggleAttribute(currentDecoration.name);
});
}
},
);
})
.nonNulls
.toList();
return GridView.count(
shrinkWrap: true,
padding: EdgeInsets.zero,
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 4,
children: children,
);
}
void _closeKeyboard() {
editorState.updateSelectionWithReason(
widget.selection,
extraInfo: {
selectionExtraInfoDisableMobileToolbarKey: true,
},
);
editorState.service.keyboardService?.closeKeyboard();
}
void _updateTextAndHref(
String prevText,
String? prevHref,
String text,
String href,
) async {
final selection = widget.selection;
if (!selection.isSingle) {
return;
}
final node = editorState.getNodeAtPath(selection.start.path);
if (node == null) {
return;
}
final transaction = editorState.transaction;
if (prevText != text) {
transaction.replaceText(
node,
selection.startIndex,
selection.length,
text,
);
}
// if the text is empty, it means the user wants to remove the text
if (text.isNotEmpty && prevHref != href) {
transaction.formatText(node, selection.startIndex, text.length, {
AppFlowyRichTextKeys.href: href.isEmpty ? null : href,
});
}
await editorState.apply(transaction);
}
}

View File

@ -1,11 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
final redoMobileToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, __, ___) => const FlowySvg(
FlowySvgs.m_redo_m,
),
actionHandler: (_, editorState) async {
editorState.undoManager.redo();
},
);

View File

@ -1,11 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
final undoMobileToolbarItem = MobileToolbarItem.action(
itemIconBuilder: (_, __, ___) => const FlowySvg(
FlowySvgs.m_undo_m,
),
actionHandler: (_, editorState) async {
editorState.undoManager.undo();
},
);

View File

@ -1,38 +0,0 @@
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class ExportPageWidget extends StatelessWidget {
const ExportPageWidget({
super.key,
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const FlowyText.medium(
'Open document failed',
fontSize: 18.0,
),
const VSpace(5),
const FlowyText.regular(
'Please try to export the page and contact us.',
fontSize: 12.0,
),
const VSpace(20),
RoundedTextButton(
title: 'Export page',
width: 100,
height: 30,
onPressed: onTap,
),
],
);
}
}

View File

@ -1,60 +0,0 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
class MenuTrash extends StatelessWidget {
const MenuTrash({super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: getIt<MenuSharedState>().notifier,
builder: (context, value, child) {
return FlowyHover(
style: HoverStyle(
hoverColor: AFThemeExtension.of(context).greySelect,
),
isSelected: () => getIt<MenuSharedState>().latestOpenView == null,
child: SizedBox(
height: 26,
child: InkWell(
onTap: () {
getIt<MenuSharedState>().latestOpenView = null;
getIt<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: makePlugin(pluginType: PluginType.trash),
),
);
},
child: _render(context),
),
).padding(horizontal: Insets.l),
).padding(horizontal: 8);
},
);
}
Widget _render(BuildContext context) {
return Row(
children: [
const FlowySvg(
FlowySvgs.trash_m,
size: Size(16, 16),
),
const HSpace(6),
FlowyText.medium(LocaleKeys.trash_text.tr()),
],
);
}
}

View File

@ -1,20 +0,0 @@
import 'dart:convert';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:http/http.dart' as http;
Future<bool> isImageExistOnCloud({
required String url,
required UserProfilePB userProfilePB,
}) async {
final header = <String, String>{};
final token = userProfilePB.token;
try {
final decodedToken = jsonDecode(token);
header['Authorization'] = 'Bearer ${decodedToken['access_token']}';
final response = await http.get(Uri.http(url), headers: header);
return response.statusCode == 200;
} catch (_) {
return false;
}
}

View File

@ -79,15 +79,10 @@ void _resolveCommonService(
IntegrationMode mode,
) async {
getIt.registerFactory<FilePickerService>(() => FilePicker());
if (mode.isTest) {
getIt.registerFactory<ApplicationDataStorage>(
() => MockApplicationDataStorage(),
);
} else {
getIt.registerFactory<ApplicationDataStorage>(
() => ApplicationDataStorage(),
);
}
getIt.registerFactory<ApplicationDataStorage>(
() => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(),
);
getIt.registerFactoryAsync<OpenAIRepository>(
() async {

View File

@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:appflowy/core/notification/folder_notification.dart';
import 'package:appflowy/core/notification/user_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -18,7 +20,6 @@ typedef DidUserWorkspaceUpdateCallback = void Function(
RepeatedUserWorkspacePB workspaces,
);
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
typedef AuthNotifyValue = FlowyResult<void, FlowyError>;
class UserListener {
UserListener({

View File

@ -1,5 +0,0 @@
import 'dart:convert';
extension Base64Encode on String {
String get base64 => base64Encode(utf8.encode(this));
}

View File

@ -0,0 +1,13 @@
final builtInSVGIcons = [
'1F9CC',
'1F9DB',
'1F9DD-200D-2642-FE0F',
'1F9DE-200D-2642-FE0F',
'1F9DF',
'1F42F',
'1F43A',
'1F431',
'1F435',
'1F600',
'1F984',
];

View File

@ -4,7 +4,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
show WorkspaceSettingPB;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'home_bloc.freezed.dart';
@ -65,22 +64,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
enum MenuResizeType {
slide,
drag,
}
extension MenuResizeTypeExtension on MenuResizeType {
Duration duration() {
switch (this) {
case MenuResizeType.drag:
return 30.milliseconds;
case MenuResizeType.slide:
return 350.milliseconds;
}
}
}
@freezed
class HomeEvent with _$HomeEvent {
const factory HomeEvent.initial() = _Initial;

View File

@ -39,9 +39,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
_userListener.start(onProfileUpdated: _profileUpdated);
await _initUser();
},
fetchWorkspaces: () async {
//
},
didReceiveUserProfile: (UserProfilePB newUserProfile) {
emit(state.copyWith(userProfile: newUserProfile));
},
@ -70,9 +67,7 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
return;
}
userProfileOrFailed.fold(
(newUserProfile) => add(
MenuUserEvent.didReceiveUserProfile(newUserProfile),
),
(profile) => add(MenuUserEvent.didReceiveUserProfile(profile)),
(err) => Log.error(err),
);
}
@ -81,7 +76,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
@freezed
class MenuUserEvent with _$MenuUserEvent {
const factory MenuUserEvent.initial() = _Initial;
const factory MenuUserEvent.fetchWorkspaces() = _FetchWorkspaces;
const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName;
const factory MenuUserEvent.didReceiveUserProfile(
UserProfilePB newUserProfile,

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
class DesktopAppearance extends BaseAppearance {
@override
@ -119,6 +120,7 @@ class DesktopAppearance extends BaseAppearance {
tint8: theme.tint8,
tint9: theme.tint9,
textColor: theme.text,
secondaryTextColor: theme.secondaryText,
greyHover: theme.hoverBG1,
greySelect: theme.bg3,
lightGreyHover: theme.hoverBG3,

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
// ThemeData in mobile
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
class MobileAppearance extends BaseAppearance {
static const _primaryColor = Color(0xFF00BCF0); //primary 100
@ -28,9 +29,7 @@ class MobileAppearance extends BaseAppearance {
fontWeight: FontWeight.w400,
);
final codeFontStyle = getFontStyle(
fontFamily: codeFontFamily,
);
final codeFontStyle = getFontStyle(fontFamily: codeFontFamily);
final theme = brightness == Brightness.light
? appTheme.lightTheme
@ -81,9 +80,7 @@ class MobileAppearance extends BaseAppearance {
: _hintColorInDarkMode;
return ThemeData(
// color
useMaterial3: false,
primaryColor: colorTheme.primary, //primary 100
primaryColorLight: const Color(0xFF57B5F8), //primary 80
dividerColor: colorTheme.outline, //caption
@ -124,6 +121,7 @@ class MobileAppearance extends BaseAppearance {
),
),
shadowColor: MaterialStateProperty.all(null),
foregroundColor: MaterialStateProperty.all(Colors.white),
backgroundColor: MaterialStateProperty.resolveWith<Color>(
(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
@ -132,7 +130,6 @@ class MobileAppearance extends BaseAppearance {
return colorTheme.primary;
},
),
foregroundColor: MaterialStateProperty.all(Colors.white),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
@ -144,20 +141,13 @@ class MobileAppearance extends BaseAppearance {
fontWeight: FontWeight.w500,
),
),
foregroundColor: MaterialStateProperty.all(
colorTheme.onBackground,
),
foregroundColor: MaterialStateProperty.all(colorTheme.onBackground),
backgroundColor: MaterialStateProperty.all(colorTheme.background),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
side: MaterialStateProperty.all(
BorderSide(
color: colorTheme.outline,
width: 0.5,
),
BorderSide(color: colorTheme.outline, width: 0.5),
),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
@ -166,9 +156,7 @@ class MobileAppearance extends BaseAppearance {
),
textButtonTheme: TextButtonThemeData(
style: ButtonStyle(
textStyle: MaterialStateProperty.all(
fontStyle,
),
textStyle: MaterialStateProperty.all(fontStyle),
),
),
// text
@ -262,6 +250,7 @@ class MobileAppearance extends BaseAppearance {
tint8: theme.tint8,
tint9: theme.tint9,
textColor: theme.text,
secondaryTextColor: theme.secondaryText,
greyHover: theme.hoverBG1,
greySelect: theme.bg3,
lightGreyHover: theme.hoverBG3,

View File

@ -9,10 +9,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'settings_dialog_bloc.freezed.dart';
enum SettingsPage {
// NEW
account,
// OLD
appearance,
language,
files,
user,
// user,
notifications,
cloud,
shortcuts,
@ -88,6 +91,6 @@ class SettingsDialogState with _$SettingsDialogState {
SettingsDialogState(
userProfile: userProfile,
successOrFailure: FlowyResult.success(null),
page: SettingsPage.appearance,
page: SettingsPage.account,
);
}

View File

@ -111,14 +111,12 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
void _profileUpdated(
FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,
) {
userProfileOrFailed.fold(
(newUserProfile) {
add(SettingsUserEvent.didReceiveUserProfile(newUserProfile));
},
(err) => Log.error(err),
);
}
) =>
userProfileOrFailed.fold(
(newUserProfile) =>
add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)),
(err) => Log.error(err),
);
}
@freezed

View File

@ -1,7 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class FolderHeader extends StatefulWidget {
const FolderHeader({
super.key,
@ -40,6 +42,7 @@ class _FolderHeaderState extends State<FolderHeader> {
constraints: const BoxConstraints(
minHeight: iconSize + textPadding * 2,
),
fontColor: AFThemeExtension.of(context).textColor,
padding: const EdgeInsets.all(textPadding),
fillColor: Colors.transparent,
onPressed: widget.onPressed,

View File

@ -62,14 +62,10 @@ class UserSettingButton extends StatelessWidget {
}
}
void showSettingsDialog(
BuildContext context,
UserProfilePB userProfile,
) {
showDialog(
context: context,
builder: (dialogContext) {
return BlocProvider<DocumentAppearanceCubit>.value(
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) =>
showDialog(
context: context,
builder: (dialogContext) => BlocProvider<DocumentAppearanceCubit>.value(
key: _settingsDialogKey,
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
child: SettingsDialog(
@ -81,10 +77,9 @@ void showSettingsDialog(
},
dismissDialog: () {
if (Navigator.of(dialogContext).canPop()) {
Navigator.of(dialogContext).pop();
} else {
Log.warn("Can't pop dialog context");
return Navigator.of(dialogContext).pop();
}
Log.warn("Can't pop dialog context");
},
restartApp: () async {
// Pop the dialog using the dialog context
@ -92,7 +87,5 @@ void showSettingsDialog(
await runAppFlowy();
},
),
);
},
);
}
),
);

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
@ -8,7 +10,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// keep this widget in case we need to roll back (lucas.xu)
@ -23,10 +24,8 @@ class SidebarUser extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<MenuUserBloc>(
create: (context) => MenuUserBloc(userProfile)
..add(
const MenuUserEvent.initial(),
),
create: (_) =>
MenuUserBloc(userProfile)..add(const MenuUserEvent.initial()),
child: BlocBuilder<MenuUserBloc, MenuUserState>(
builder: (context, state) => Row(
children: [
@ -35,9 +34,7 @@ class SidebarUser extends StatelessWidget {
name: state.userProfile.name,
),
const HSpace(8),
Expanded(
child: _buildUserName(context, state),
),
Expanded(child: _buildUserName(context, state)),
UserSettingButton(userProfile: state.userProfile),
const HSpace(4),
const NotificationButton(),

View File

@ -15,8 +15,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
typedef NaviAction = void Function();
class NavigationNotifier with ChangeNotifier {
NavigationNotifier({required this.navigationItems});
@ -145,19 +143,6 @@ class NaviItemWidget extends StatelessWidget {
}
}
class NaviItemDivider extends StatelessWidget {
const NaviItemDivider({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Row(
children: [child, const Text('/')],
);
}
}
class EllipsisNaviItem extends NavigationItem {
EllipsisNaviItem({required this.items});

View File

@ -0,0 +1,473 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';
import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsAccountView extends StatefulWidget {
const SettingsAccountView({
super.key,
required this.userProfile,
required this.didLogin,
required this.didLogout,
});
final UserProfilePB userProfile;
// Called when the user signs in from the setting dialog
final VoidCallback didLogin;
// Called when the user logout in the setting dialog
final VoidCallback didLogout;
@override
State<SettingsAccountView> createState() => _SettingsAccountViewState();
}
class _SettingsAccountViewState extends State<SettingsAccountView> {
late String userName = widget.userProfile.name;
late final TextEditingController _emailController = TextEditingController(
text: widget.userProfile.email,
);
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsUserViewBloc>(
create: (context) =>
getIt<SettingsUserViewBloc>(param1: widget.userProfile)
..add(const SettingsUserEvent.initial()),
child: BlocConsumer<SettingsUserViewBloc, SettingsUserState>(
listenWhen: (previous, current) =>
previous.userProfile.email != current.userProfile.email,
listener: (context, state) =>
_emailController.text = state.userProfile.email,
builder: (context, state) {
return SettingsBody(
children: [
SettingsHeader(
title: LocaleKeys.settings_accountPage_title.tr(),
description: LocaleKeys.settings_accountPage_description.tr(),
),
SettingsCategory(
title: LocaleKeys.settings_accountPage_general_title.tr(),
children: [
UserProfileSetting(
name: userName,
iconUrl: state.userProfile.iconUrl,
onSave: (newName) {
// Pseudo change the name to update the UI before the backend
// processes the request. This is to give the user a sense of
// immediate feedback, and avoid UI flickering.
setState(() => userName = newName);
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserName(newName));
},
),
],
),
// Only show change email if the user is authenticated and not using local auth
if (isAuthEnabled &&
state.userProfile.authenticator != AuthenticatorPB.Local) ...[
const SettingsCategorySpacer(),
SettingsCategory(
title: LocaleKeys.settings_accountPage_email_title.tr(),
children: [
SingleSettingAction(
label: state.userProfile.email,
buttonLabel: LocaleKeys
.settings_accountPage_email_actions_change
.tr(),
onPressed: () => SettingsAlertDialog(
title: LocaleKeys
.settings_accountPage_email_actions_change
.tr(),
confirmLabel: LocaleKeys.button_save.tr(),
confirm: () {
context.read<SettingsUserViewBloc>().add(
SettingsUserEvent.updateUserEmail(
_emailController.text,
),
);
Navigator.of(context).pop();
},
children: [
SettingsInputField(
label: LocaleKeys.settings_accountPage_email_title
.tr(),
value: state.userProfile.email,
hideActions: true,
textController: _emailController,
),
],
).show(context),
),
],
),
],
/// Enable when we have change password feature and 2FA
// const SettingsCategorySpacer(),
// SettingsCategory(
// title: 'Account & security',
// children: [
// SingleSettingAction(
// label: '**********',
// buttonLabel: 'Change password',
// onPressed: () {},
// ),
// SingleSettingAction(
// label: '2-step authentication',
// buttonLabel: 'Enable 2FA',
// onPressed: () {},
// ),
// ],
// ),
const SettingsCategorySpacer(),
SettingsCategory(
title: LocaleKeys.settings_accountPage_keys_title.tr(),
children: [
SettingsInputField(
label:
LocaleKeys.settings_accountPage_keys_openAILabel.tr(),
tooltip:
LocaleKeys.settings_accountPage_keys_openAITooltip.tr(),
placeholder:
LocaleKeys.settings_accountPage_keys_openAIHint.tr(),
value: state.userProfile.openaiKey,
obscureText: true,
onSave: (key) => context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserOpenAIKey(key)),
),
SettingsInputField(
label: LocaleKeys.settings_accountPage_keys_stabilityAILabel
.tr(),
tooltip: LocaleKeys
.settings_accountPage_keys_stabilityAITooltip
.tr(),
placeholder: LocaleKeys
.settings_accountPage_keys_stabilityAIHint
.tr(),
value: state.userProfile.stabilityAiKey,
obscureText: true,
onSave: (key) => context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserStabilityAIKey(key)),
),
],
),
const SettingsCategorySpacer(),
SettingsCategory(
title: LocaleKeys.settings_accountPage_login_title.tr(),
children: [
SignInOutButton(
userProfile: state.userProfile,
onAction:
state.userProfile.authenticator == AuthenticatorPB.Local
? widget.didLogin
: widget.didLogout,
signIn: state.userProfile.authenticator ==
AuthenticatorPB.Local,
),
],
),
/// Enable when we can delete accounts
// const SettingsCategorySpacer(),
// SettingsSubcategory(
// title: 'Delete account',
// children: [
// SingleSettingAction(
// label:
// 'Permanently delete your account and remove access from all teamspaces.',
// labelMaxLines: 4,
// onPressed: () {},
// buttonLabel: 'Delete my account',
// isDangerous: true,
// fontSize: 12,
// ),
// ],
// ),
],
);
},
),
);
}
}
@visibleForTesting
class SignInOutButton extends StatelessWidget {
const SignInOutButton({
super.key,
required this.userProfile,
required this.onAction,
this.signIn = true,
});
final UserProfilePB userProfile;
final VoidCallback onAction;
final bool signIn;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 48,
child: FlowyTextButton(
signIn
? LocaleKeys.settings_accountPage_login_loginLabel.tr()
: LocaleKeys.settings_accountPage_login_logoutLabel.tr(),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
fontWeight: FontWeight.w600,
radius: BorderRadius.circular(12),
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: const Color(0xFF005483),
fontHoverColor: Colors.white,
onPressed: () => SettingsAlertDialog(
title: LocaleKeys.settings_accountPage_login_loginLabel.tr(),
subtitle: signIn
? null
: switch (userProfile.encryptionType) {
EncryptionTypePB.Symmetric => LocaleKeys
.settings_menu_selfEncryptionLogoutPrompt
.tr(),
_ => LocaleKeys.settings_menu_logoutPrompt.tr(),
},
implyLeading: signIn,
confirm: !signIn
? () async {
await getIt<AuthService>().signOut();
onAction();
}
: null,
children:
signIn ? [SettingThirdPartyLogin(didLogin: onAction)] : null,
).show(context),
),
),
],
);
}
}
@visibleForTesting
class UserProfileSetting extends StatefulWidget {
const UserProfileSetting({
super.key,
required this.name,
required this.iconUrl,
this.onSave,
});
final String name;
final String iconUrl;
final void Function(String)? onSave;
@override
State<UserProfileSetting> createState() => _UserProfileSettingState();
}
class _UserProfileSettingState extends State<UserProfileSetting> {
late final FocusNode focusNode;
bool isEditing = false;
bool isHovering = false;
@override
void initState() {
super.initState();
focusNode = FocusNode(
onKeyEvent: (_, event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape &&
isEditing) {
setState(() => isEditing = false);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
);
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showIconPickerDialog(context),
child: FlowyHover(
resetHoverOnRebuild: false,
onHover: (state) => setState(() => isHovering = state),
style: HoverStyle(
hoverColor: Colors.transparent,
borderRadius: BorderRadius.circular(100),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
UserAvatar(
iconUrl: widget.iconUrl,
name: widget.name,
isLarge: true,
isHovering: isHovering,
),
const VSpace(4),
FlowyText.regular(
LocaleKeys
.settings_accountPage_general_changeProfilePicture
.tr(),
color: AFThemeExtension.of(context).textColor,
),
],
),
),
),
if (widget.iconUrl.isNotEmpty)
Positioned(
right: 0,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => context
.read<SettingsUserViewBloc>()
.add(const SettingsUserEvent.removeUserIcon()),
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: FlowyHover(
resetHoverOnRebuild: false,
style: const HoverStyle(
borderRadius: BorderRadius.all(Radius.circular(24)),
hoverColor: Color(0xFF005483),
),
builder: (_, __) => Padding(
padding: const EdgeInsets.all(4),
child: FlowySvg(
FlowySvgs.close_s,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
),
),
],
),
const HSpace(16),
if (!isEditing) ...[
Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: FlowyText.medium(
widget.name,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(4),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => setState(() => isEditing = true),
child: const FlowyHover(
resetHoverOnRebuild: false,
child: Padding(
padding: EdgeInsets.all(4),
child: FlowySvg(FlowySvgs.edit_s),
),
),
),
],
),
),
] else ...[
Flexible(
child: SettingsInputField(
value: widget.name,
focusNode: focusNode..requestFocus(),
onCancel: () => setState(() => isEditing = false),
onSave: (val) {
widget.onSave?.call(val);
setState(() => isEditing = false);
},
),
),
],
],
);
}
Future<void> _showIconPickerDialog(BuildContext context) {
return showDialog(
context: context,
builder: (dialogContext) => SimpleDialog(
title: FlowyText.medium(
LocaleKeys.settings_user_selectAnIcon.tr(),
fontSize: FontSizes.s16,
),
children: [
Container(
height: 380,
width: 360,
margin: const EdgeInsets.symmetric(horizontal: 12),
child: FlowyEmojiPicker(
onEmojiSelected: (_, emoji) {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserIcon(iconUrl: emoji));
Navigator.of(dialogContext).pop();
},
),
),
],
),
);
}
}

View File

@ -1,6 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
@ -9,18 +11,12 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_s
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'widgets/setting_cloud.dart';
const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12);
const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0);
class SettingsDialog extends StatelessWidget {
SettingsDialog(
this.user, {
@ -41,49 +37,31 @@ class SettingsDialog extends StatelessWidget {
..add(const SettingsDialogEvent.initial()),
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
builder: (context, state) => FlowyDialog(
title: Padding(
padding: _dialogHorizontalPadding + _contentInsetPadding,
child: FlowyText(
LocaleKeys.settings_title.tr(),
fontSize: 20,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.tertiary,
),
),
width: MediaQuery.of(context).size.width * 0.7,
child: ScaffoldMessenger(
child: Scaffold(
backgroundColor: Colors.transparent,
body: Padding(
padding: _dialogHorizontalPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 200,
child: SettingsMenu(
userProfile: user,
changeSelectedPage: (index) {
context
.read<SettingsDialogBloc>()
.add(SettingsDialogEvent.setSelectedPage(index));
},
currentPage:
context.read<SettingsDialogBloc>().state.page,
),
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 200,
child: SettingsMenu(
userProfile: user,
changeSelectedPage: (index) => context
.read<SettingsDialogBloc>()
.add(SettingsDialogEvent.setSelectedPage(index)),
currentPage:
context.read<SettingsDialogBloc>().state.page,
),
VerticalDivider(
color: Theme.of(context).dividerColor,
),
Expanded(
child: getSettingsView(
context.read<SettingsDialogBloc>().state.page,
context.read<SettingsDialogBloc>().state.userProfile,
),
const SizedBox(width: 10),
Expanded(
child: getSettingsView(
context.read<SettingsDialogBloc>().state.page,
context.read<SettingsDialogBloc>().state.userProfile,
),
),
],
),
),
],
),
),
),
@ -94,27 +72,24 @@ class SettingsDialog extends StatelessWidget {
Widget getSettingsView(SettingsPage page, UserProfilePB user) {
switch (page) {
case SettingsPage.account:
return SettingsAccountView(
userProfile: user,
didLogout: didLogout,
didLogin: dismissDialog,
);
case SettingsPage.appearance:
return const SettingsAppearanceView();
case SettingsPage.language:
return const SettingsLanguageView();
case SettingsPage.files:
return const SettingsFileSystemView();
case SettingsPage.user:
return SettingsUserView(
user,
didLogin: () => dismissDialog(),
didLogout: didLogout,
didOpenUser: restartApp,
);
case SettingsPage.notifications:
return const SettingsNotificationsView();
case SettingsPage.cloud:
return SettingCloud(
restartAppFlowy: () => restartApp(),
);
return SettingCloud(restartAppFlowy: () => restartApp());
case SettingsPage.shortcuts:
return const SettingsCustomizeShortcutsWrapper();
return const SettingsShortcutsView();
case SettingsPage.member:
return WorkspaceMembersPage(userProfile: user);
case SettingsPage.featureFlags:

View File

@ -0,0 +1,205 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
class SettingsAlertDialog extends StatefulWidget {
const SettingsAlertDialog({
super.key,
required this.title,
this.subtitle,
this.children,
this.cancel,
this.confirm,
this.confirmLabel,
this.hideCancelButton = false,
this.isDangerous = false,
this.implyLeading = false,
});
final String title;
final String? subtitle;
final List<Widget>? children;
final void Function()? cancel;
final void Function()? confirm;
final String? confirmLabel;
final bool hideCancelButton;
final bool isDangerous;
/// If true, a back button will show in the top left corner
final bool implyLeading;
@override
State<SettingsAlertDialog> createState() => _SettingsAlertDialogState();
}
class _SettingsAlertDialogState extends State<SettingsAlertDialog> {
@override
Widget build(BuildContext context) {
return StyledDialog(
maxHeight: 600,
maxWidth: 600,
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (widget.implyLeading) ...[
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
children: [
const FlowySvg(
FlowySvgs.arrow_back_m,
size: Size.square(24),
),
const HSpace(8),
FlowyText.semibold(
LocaleKeys.button_back.tr(),
fontSize: 16,
),
],
),
),
),
],
const Spacer(),
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: FlowySvg(
FlowySvgs.m_close_m,
size: const Size.square(20),
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: FlowyText.medium(
widget.title,
fontSize: 22,
color: Theme.of(context).colorScheme.tertiary,
maxLines: null,
),
),
],
),
if (widget.subtitle?.isNotEmpty ?? false) ...[
const VSpace(16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: FlowyText.regular(
widget.subtitle!,
fontSize: 16,
color: Theme.of(context).colorScheme.tertiary,
textAlign: TextAlign.center,
maxLines: null,
),
),
],
),
],
if (widget.children?.isNotEmpty ?? false) ...[
const VSpace(16),
...widget.children!,
],
if (widget.confirm != null || !widget.hideCancelButton) ...[
const VSpace(20),
],
_Actions(
hideCancelButton: widget.hideCancelButton,
confirmLabel: widget.confirmLabel,
cancel: widget.cancel,
confirm: widget.confirm,
isDangerous: widget.isDangerous,
),
],
),
);
}
}
class _Actions extends StatelessWidget {
const _Actions({
required this.hideCancelButton,
this.confirmLabel,
this.cancel,
this.confirm,
this.isDangerous = false,
});
final bool hideCancelButton;
final String? confirmLabel;
final VoidCallback? cancel;
final VoidCallback? confirm;
final bool isDangerous;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (!hideCancelButton) ...[
SizedBox(
height: 24,
child: FlowyTextButton(
LocaleKeys.button_cancel.tr(),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
fontColor: AFThemeExtension.of(context).textColor,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
onPressed: () {
cancel?.call();
Navigator.of(context).pop();
},
),
),
],
if (confirm != null && !hideCancelButton) ...[
const HSpace(8),
],
if (confirm != null) ...[
SizedBox(
height: 48,
child: FlowyTextButton(
confirmLabel ?? LocaleKeys.button_confirm.tr(),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
fontColor: isDangerous ? Colors.white : null,
fontHoverColor: Colors.white,
fillColor: isDangerous
? Theme.of(context).colorScheme.error
: Theme.of(context).colorScheme.primary,
hoverColor: isDangerous
? Theme.of(context).colorScheme.error
: const Color(0xFF005483),
onPressed: confirm,
),
),
],
],
);
}
}

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class SettingsBody extends StatelessWidget {
const SettingsBody({
super.key,
required this.children,
});
final List<Widget> children;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
/// Renders a simple category taking a title and the list
/// of children (settings) to be rendered.
///
class SettingsCategory extends StatelessWidget {
const SettingsCategory({
super.key,
required this.title,
this.description,
this.tooltip,
this.actions,
required this.children,
});
final String title;
final String? description;
final String? tooltip;
final List<Widget>? actions;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
FlowyText.semibold(
title,
maxLines: 2,
fontSize: 16,
overflow: TextOverflow.ellipsis,
),
if (tooltip != null) ...[
const HSpace(4),
FlowyTooltip(
message: tooltip,
child: const FlowySvg(FlowySvgs.information_s),
),
],
const Spacer(),
if (actions != null) ...actions!,
],
),
const VSpace(8),
if (description?.isNotEmpty ?? false) ...[
FlowyText.regular(
description!,
maxLines: 4,
fontSize: 12,
overflow: TextOverflow.ellipsis,
),
const VSpace(8),
],
SeparatedColumn(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () =>
children.length > 1 ? const VSpace(16) : const SizedBox.shrink(),
children: children,
),
],
);
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
/// This is used to create a uniform space and divider
/// between categories in settings.
///
class SettingsCategorySpacer extends StatelessWidget {
const SettingsCategorySpacer({super.key});
@override
Widget build(BuildContext context) =>
const Divider(height: 32, color: Color(0xFFF2F2F2));
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
/// Renders a simple header for the settings view
///
class SettingsHeader extends StatelessWidget {
const SettingsHeader({super.key, required this.title, this.description});
final String title;
final String? description;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.semibold(title, fontSize: 24),
if (description?.isNotEmpty == true) ...[
const VSpace(8),
FlowyText(
description!,
maxLines: 4,
fontSize: 12,
color: AFThemeExtension.of(context).secondaryTextColor,
),
],
const VSpace(16),
],
);
}
}

View File

@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
/// This is used to describe a settings input field
///
/// The input will have secondary action of "save" and "cancel"
/// which will only be shown when the input has changed.
///
/// _Note: The label can overflow and will be ellipsized._
///
class SettingsInputField extends StatefulWidget {
const SettingsInputField({
super.key,
this.label,
this.textController,
this.focusNode,
this.obscureText = false,
this.value,
this.placeholder,
this.tooltip,
this.onSave,
this.onCancel,
this.hideActions = false,
});
final String? label;
final TextEditingController? textController;
final FocusNode? focusNode;
/// If true, the input field will be obscured
/// and an option to toggle to show the text will be provided.
///
final bool obscureText;
final String? value;
final String? placeholder;
final String? tooltip;
/// If true the save and cancel options will not show below the
/// input field.
///
final bool hideActions;
final Function(String)? onSave;
/// The action to be performed when the cancel button is pressed.
///
/// If null the button will **NOT** be disabled! Instead it will
/// reset the input to the original value.
///
final Function()? onCancel;
@override
State<SettingsInputField> createState() => _SettingsInputFieldState();
}
class _SettingsInputFieldState extends State<SettingsInputField> {
late final controller =
widget.textController ?? TextEditingController(text: widget.value);
late final FocusNode focusNode = widget.focusNode ?? FocusNode();
late bool obscureText = widget.obscureText;
@override
void dispose() {
if (widget.focusNode == null) {
focusNode.dispose();
}
if (widget.textController == null) {
controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
if (widget.label?.isNotEmpty == true) ...[
Flexible(
child: FlowyText.medium(
widget.label!,
color: AFThemeExtension.of(context).secondaryTextColor,
),
),
],
if (widget.tooltip != null) ...[
const HSpace(4),
FlowyTooltip(
message: widget.tooltip,
child: const FlowySvg(FlowySvgs.information_s),
),
],
],
),
const VSpace(8),
SizedBox(
height: 48,
child: FlowyTextField(
focusNode: focusNode,
hintText: widget.placeholder,
controller: controller,
autoFocus: false,
obscureText: obscureText,
isDense: false,
suffixIconConstraints:
BoxConstraints.tight(const Size(23 + 18, 24)),
suffixIcon: !widget.obscureText
? null
: GestureDetector(
onTap: () => setState(() => obscureText = !obscureText),
child: Padding(
padding: const EdgeInsets.only(right: 18),
child: FlowySvg(
obscureText ? FlowySvgs.show_m : FlowySvgs.hide_m,
size: const Size(12, 15),
color: Theme.of(context).colorScheme.outline,
),
),
),
onSubmitted: widget.onSave,
onChanged: (_) => setState(() {}),
),
),
if (!widget.hideActions &&
((widget.value == null && controller.text.isNotEmpty) ||
widget.value != null && widget.value != controller.text)) ...[
const VSpace(8),
Row(
children: [
const Spacer(),
SizedBox(
height: 21,
child: FlowyTextButton(
LocaleKeys.button_save.tr(),
fontWeight: FontWeight.normal,
padding: EdgeInsets.zero,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
fontColor: AFThemeExtension.of(context).textColor,
onPressed: () => widget.onSave?.call(controller.text),
),
),
const HSpace(24),
SizedBox(
height: 21,
child: FlowyTextButton(
LocaleKeys.button_cancel.tr(),
fontWeight: FontWeight.normal,
padding: EdgeInsets.zero,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
fontColor: AFThemeExtension.of(context).textColor,
onPressed: () {
setState(() => controller.text = widget.value ?? '');
widget.onCancel?.call();
},
),
),
],
),
],
],
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
/// Renders a simple category taking a title and the list
/// of children (settings) to be rendered.
///
class SettingsSubcategory extends StatelessWidget {
const SettingsSubcategory({
super.key,
required this.title,
required this.children,
});
final String title;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.medium(
title,
color: AFThemeExtension.of(context).secondaryTextColor,
maxLines: 2,
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
const VSpace(8),
SeparatedColumn(
mainAxisSize: MainAxisSize.min,
separatorBuilder: () =>
children.length > 1 ? const VSpace(16) : const SizedBox.shrink(),
children: children,
),
],
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
/// This is used to describe a single setting action
///
/// This will render a simple action that takes the title,
/// the button label, and the button action.
///
/// _Note: The label can overflow and will be ellipsized,
/// unless maxLines is overriden._
///
class SingleSettingAction extends StatelessWidget {
const SingleSettingAction({
super.key,
required this.label,
this.labelMaxLines,
required this.buttonLabel,
this.onPressed,
this.isDangerous = false,
this.fontSize = 14,
this.fontWeight = FontWeight.normal,
});
final String label;
final int? labelMaxLines;
final String buttonLabel;
/// The action to be performed when the button is pressed
///
/// If null the button will be rendered as disabled.
///
final VoidCallback? onPressed;
/// If isDangerous is true, the button will be rendered as a dangerous
/// action, with a red outline.
///
final bool isDangerous;
final double fontSize;
final FontWeight fontWeight;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: FlowyText(
label,
fontSize: fontSize,
fontWeight: fontWeight,
maxLines: labelMaxLines,
overflow: TextOverflow.ellipsis,
color: AFThemeExtension.of(context).secondaryTextColor,
),
),
const HSpace(24),
SizedBox(
height: 32,
child: FlowyTextButton(
buttonLabel,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
fillColor:
isDangerous ? null : Theme.of(context).colorScheme.primary,
hoverColor: isDangerous ? null : const Color(0xFF005483),
fontColor: isDangerous ? Theme.of(context).colorScheme.error : null,
fontHoverColor: Colors.white,
fontSize: 12,
isDangerous: isDangerous,
onPressed: onPressed,
),
),
],
);
}
}

View File

@ -1,7 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class FeatureFlagsPage extends StatelessWidget {
const FeatureFlagsPage({
@ -10,36 +14,30 @@ class FeatureFlagsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: SeparatedColumn(
children: [
...FeatureFlag.data.entries
return SettingsBody(
children: [
const SettingsHeader(title: 'Feature flags'),
SeparatedColumn(
children: FeatureFlag.data.entries
.where((e) => e.key != FeatureFlag.unknown)
.map(
(e) => _FeatureFlagItem(featureFlag: e.key),
),
FlowyTextButton(
'Restart the app to apply changes',
fontSize: 16.0,
fontColor: Colors.red,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12.0,
),
onPressed: () async {
await runAppFlowy();
},
),
],
),
.map((e) => _FeatureFlagItem(featureFlag: e.key))
.toList(),
),
const SettingsCategorySpacer(),
FlowyTextButton(
'Restart the app to apply changes',
fontSize: 16.0,
fontColor: Colors.red,
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
onPressed: () async => runAppFlowy(),
),
],
);
}
}
class _FeatureFlagItem extends StatefulWidget {
const _FeatureFlagItem({
required this.featureFlag,
});
const _FeatureFlagItem({required this.featureFlag});
final FeatureFlag featureFlag;
@ -51,21 +49,11 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> {
@override
Widget build(BuildContext context) {
return ListTile(
title: FlowyText(
widget.featureFlag.name,
fontSize: 16.0,
),
subtitle: FlowyText.small(
widget.featureFlag.description,
maxLines: 3,
),
title: FlowyText(widget.featureFlag.name, fontSize: 16.0),
subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3),
trailing: Switch.adaptive(
value: widget.featureFlag.isOn,
onChanged: (value) {
setState(() {
widget.featureFlag.update(value);
});
},
onChanged: (value) => setState(() => widget.featureFlag.update(value)),
),
);
}

View File

@ -1,5 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
@ -9,8 +12,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:styled_widget/styled_widget.dart';
@ -19,9 +20,7 @@ import '../../../../../startup/startup.dart';
import '../../../../../startup/tasks/prelude.dart';
class SettingsFileLocationCustomizer extends StatefulWidget {
const SettingsFileLocationCustomizer({
super.key,
});
const SettingsFileLocationCustomizer({super.key});
@override
State<SettingsFileLocationCustomizer> createState() =>

View File

@ -1,7 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
@ -13,15 +18,11 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
class WorkspaceMembersPage extends StatelessWidget {
const WorkspaceMembersPage({
super.key,
required this.userProfile,
});
const WorkspaceMembersPage({super.key, required this.userProfile});
final UserProfilePB userProfile;
@ -33,25 +34,22 @@ class WorkspaceMembersPage extends StatelessWidget {
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
listener: _showResultDialog,
builder: (context, state) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// title
FlowyText.semibold(
LocaleKeys.settings_appearance_members_title.tr(),
fontSize: 20,
return SettingsBody(
children: [
// title
SettingsHeader(
title: LocaleKeys.settings_appearance_members_title.tr(),
),
if (state.myRole.canInvite) const _InviteMember(),
if (state.myRole.canInvite && state.members.isNotEmpty)
const SettingsCategorySpacer(),
if (state.members.isNotEmpty)
_MemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
if (state.myRole.canInvite) const _InviteMember(),
if (state.members.isNotEmpty)
_MemberList(
members: state.members,
userProfile: userProfile,
myRole: state.myRole,
),
const VSpace(48.0),
],
),
],
);
},
),
@ -117,7 +115,6 @@ class _InviteMemberState extends State<_InviteMember> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const VSpace(12.0),
FlowyText.semibold(
LocaleKeys.settings_appearance_members_inviteMembers.tr(),
fontSize: 16.0,
@ -151,7 +148,6 @@ class _InviteMemberState extends State<_InviteMember> {
),
],
),
const VSpace(16.0),
/* Enable this when the feature is ready
PrimaryButton(
backgroundColor: const Color(0xFFE0E0E0),
@ -183,10 +179,6 @@ class _InviteMemberState extends State<_InviteMember> {
),
const VSpace(16.0),
*/
const Divider(
height: 1.0,
thickness: 1.0,
),
],
);
}
@ -194,11 +186,10 @@ class _InviteMemberState extends State<_InviteMember> {
void _inviteMember() {
final email = _emailController.text;
if (!isEmail(email)) {
showSnackBarMessage(
return showSnackBarMessage(
context,
LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
);
return;
}
context
.read<WorkspaceMemberBloc>()
@ -219,22 +210,17 @@ class _MemberList extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
return SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () => const Divider(),
children: [
const VSpace(16.0),
SeparatedColumn(
crossAxisAlignment: CrossAxisAlignment.start,
separatorBuilder: () => const Divider(),
children: [
const _MemberListHeader(),
...members.map(
(member) => _MemberItem(
member: member,
myRole: myRole,
userProfile: userProfile,
),
),
],
const _MemberListHeader(),
...members.map(
(member) => _MemberItem(
member: member,
myRole: myRole,
userProfile: userProfile,
),
),
],
);

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@ -6,6 +8,8 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -13,7 +17,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@ -37,8 +40,11 @@ class SettingCloud extends StatelessWidget {
create: (context) => CloudSettingBloc(cloudType),
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
builder: (context, state) {
return Column(
return SettingsBody(
children: [
SettingsHeader(
title: LocaleKeys.settings_menu_cloudSettings.tr(),
),
if (Env.enableCustomCloud)
Row(
children: [

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
@ -10,7 +12,6 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingThirdPartyLogin extends StatelessWidget {
@ -42,24 +43,12 @@ class SettingThirdPartyLogin extends StatelessWidget {
: const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
FlowyText.medium(
LocaleKeys.signIn_signInWith.tr(),
fontSize: 16,
),
const HSpace(6),
],
),
const VSpace(6),
promptMessage,
const VSpace(6),
indicator,
const VSpace(6),
if (isAuthEnabled) const ThirdPartySignInButtons(),
const VSpace(6),
],
);
},

View File

@ -1,10 +1,15 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/create_file_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/time_format_setting.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'settings_appearance/settings_appearance.dart';
@ -14,59 +19,57 @@ class SettingsAppearanceView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: BlocProvider<DynamicPluginBloc>(
create: (_) => DynamicPluginBloc(),
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) {
return Column(
children: [
ColorSchemeSetting(
currentTheme: state.appTheme.themeName,
bloc: context.read<DynamicPluginBloc>(),
),
BrightnessSetting(
currentThemeMode: state.themeMode,
),
const Divider(),
ThemeFontFamilySetting(
currentFontFamily: state.font,
),
const Divider(),
DocumentCursorColorSetting(
currentCursorColor: state.documentCursorColor ??
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
context,
),
),
DocumentSelectionColorSetting(
currentSelectionColor: state.documentSelectionColor ??
DefaultAppearanceSettings
.getDefaultDocumentSelectionColor(
context,
),
),
const Divider(),
LayoutDirectionSetting(
currentLayoutDirection: state.layoutDirection,
),
TextDirectionSetting(
currentTextDirection: state.textDirection,
),
const EnableRTLToolbarItemsSetting(),
const Divider(),
DateFormatSetting(
currentFormat: state.dateFormat,
),
TimeFormatSetting(
currentFormat: state.timeFormat,
),
const Divider(),
CreateFileSettings(),
],
);
},
),
return BlocProvider<DynamicPluginBloc>(
create: (_) => DynamicPluginBloc(),
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) {
return SettingsBody(
children: [
SettingsHeader(title: LocaleKeys.settings_menu_appearance.tr()),
ColorSchemeSetting(
currentTheme: state.appTheme.themeName,
bloc: context.read<DynamicPluginBloc>(),
),
BrightnessSetting(
currentThemeMode: state.themeMode,
),
const Divider(),
ThemeFontFamilySetting(
currentFontFamily: state.font,
),
const Divider(),
DocumentCursorColorSetting(
currentCursorColor: state.documentCursorColor ??
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
context,
),
),
DocumentSelectionColorSetting(
currentSelectionColor: state.documentSelectionColor ??
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(
context,
),
),
const Divider(),
LayoutDirectionSetting(
currentLayoutDirection: state.layoutDirection,
),
TextDirectionSetting(
currentTextDirection: state.textDirection,
),
const EnableRTLToolbarItemsSetting(),
const Divider(),
DateFormatSetting(
currentFormat: state.dateFormat,
),
TimeFormatSetting(
currentFormat: state.timeFormat,
),
const Divider(),
CreateFileSettings(),
],
);
},
),
);
}

View File

@ -1,55 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart';
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsCustomizeShortcutsWrapper extends StatelessWidget {
const SettingsCustomizeShortcutsWrapper({super.key});
class SettingsShortcutsView extends StatelessWidget {
const SettingsShortcutsView({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider<ShortcutsCubit>(
create: (_) =>
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
child: const SettingsCustomizeShortcutsView(),
);
}
}
class SettingsCustomizeShortcutsView extends StatelessWidget {
const SettingsCustomizeShortcutsView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ShortcutsCubit, ShortcutsState>(
builder: (context, state) {
switch (state.status) {
case ShortcutsStatus.initial:
case ShortcutsStatus.updating:
return const Center(child: CircularProgressIndicator());
case ShortcutsStatus.success:
return ShortcutsListView(shortcuts: state.commandShortcutEvents);
case ShortcutsStatus.failure:
return ShortcutsErrorView(
errorMessage: state.error,
);
}
},
child: SettingsBody(
children: [
SettingsHeader(
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
),
BlocBuilder<ShortcutsCubit, ShortcutsState>(
builder: (_, state) => switch (state.status) {
ShortcutsStatus.initial ||
ShortcutsStatus.updating =>
const Center(child: CircularProgressIndicator()),
ShortcutsStatus.success =>
ShortcutsListView(shortcuts: state.commandShortcutEvents),
ShortcutsStatus.failure =>
ShortcutsErrorView(errorMessage: state.error),
},
),
],
),
);
}
}
class ShortcutsListView extends StatelessWidget {
const ShortcutsListView({
super.key,
required this.shortcuts,
});
const ShortcutsListView({super.key, required this.shortcuts});
final List<CommandShortcutEvent> shortcuts;
@ -73,14 +67,7 @@ class ShortcutsListView extends StatelessWidget {
],
),
const VSpace(10),
Expanded(
child: ListView.builder(
itemCount: shortcuts.length,
itemBuilder: (context, index) => ShortcutsListTile(
shortcutEvent: shortcuts[index],
),
),
),
...shortcuts.map((e) => ShortcutsListTile(shortcutEvent: e)),
const VSpace(10),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
@ -88,9 +75,7 @@ class ShortcutsListView extends StatelessWidget {
const Spacer(),
FlowyTextButton(
LocaleKeys.settings_shortcuts_resetToDefault.tr(),
onPressed: () {
context.read<ShortcutsCubit>().resetToDefault();
},
onPressed: () => context.read<ShortcutsCubit>().resetToDefault(),
),
],
),
@ -248,9 +233,7 @@ class ShortcutsErrorView extends StatelessWidget {
),
FlowyIconButton(
icon: const Icon(Icons.replay_outlined),
onPressed: () {
BlocProvider.of<ShortcutsCubit>(context).fetchShortcuts();
},
onPressed: () => context.read<ShortcutsCubit>().fetchShortcuts(),
),
],
);

View File

@ -1,35 +1,33 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
class SettingsFileSystemView extends StatefulWidget {
const SettingsFileSystemView({
super.key,
});
@override
State<SettingsFileSystemView> createState() => _SettingsFileSystemViewState();
}
class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
late final _items = [
const SettingsFileLocationCustomizer(),
// disable export data for v0.2.0 in release mode.
if (kDebugMode) const SettingsExportFileWidget(),
const ImportAppFlowyData(),
// clear the cache
const SettingsFileCacheWidget(),
];
class SettingsFileSystemView extends StatelessWidget {
const SettingsFileSystemView({super.key});
@override
Widget build(BuildContext context) {
return SeparatedColumn(
separatorBuilder: () => const Divider(),
children: _items,
return SettingsBody(
children: [
SettingsHeader(title: LocaleKeys.settings_menu_files.tr()),
const SettingsFileLocationCustomizer(),
const SettingsCategorySpacer(),
if (kDebugMode) ...[
const SettingsExportFileWidget(),
],
const ImportAppFlowyData(),
const SettingsCategorySpacer(),
const SettingsFileCacheWidget(),
],
);
}
}

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flowy_infra/language.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsLanguageView extends StatelessWidget {
@ -13,18 +16,21 @@ class SettingsLanguageView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) => Row(
children: [
Expanded(
child: FlowyText.medium(
LocaleKeys.settings_menu_language.tr(),
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) => SettingsBody(
children: [
SettingsHeader(title: LocaleKeys.settings_menu_language.tr()),
Row(
children: [
Expanded(
child: FlowyText.medium(
LocaleKeys.settings_menu_language.tr(),
),
),
),
LanguageSelector(currentLocale: state.locale),
],
),
LanguageSelector(currentLocale: state.locale),
],
),
],
),
);
}

View File

@ -1,12 +1,15 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class SettingsMenu extends StatelessWidget {
const SettingsMenu({
@ -22,79 +25,127 @@ class SettingsMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: SeparatedColumn(
separatorBuilder: () => const SizedBox(height: 10),
children: [
SettingsMenuElement(
page: SettingsPage.appearance,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_appearance.tr(),
icon: Icons.brightness_4,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.language,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_language.tr(),
icon: Icons.translate,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.files,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_files.tr(),
icon: Icons.file_present_outlined,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.user,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_user.tr(),
icon: Icons.account_box_outlined,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.notifications,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_notifications.tr(),
icon: Icons.notifications_outlined,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.cloud,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_cloudSettings.tr(),
icon: Icons.sync,
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.shortcuts,
selectedPage: currentPage,
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
icon: Icons.cut,
changeSelectedPage: changeSelectedPage,
),
if (FeatureFlag.membersSettings.isOn &&
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud)
SettingsMenuElement(
page: SettingsPage.member,
selectedPage: currentPage,
label: LocaleKeys.settings_appearance_members_label.tr(),
icon: Icons.people,
changeSelectedPage: changeSelectedPage,
// Column > Expanded for full size no matter the content
return Column(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8) +
const EdgeInsets.only(left: 8, right: 4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
bottomLeft: Radius.circular(8),
),
),
if (kDebugMode)
SettingsMenuElement(
// no need to translate this page
page: SettingsPage.featureFlags,
selectedPage: currentPage,
label: 'Feature Flags',
icon: Icons.flag,
changeSelectedPage: changeSelectedPage,
child: SingleChildScrollView(
// Right padding is added to make the scrollbar centered
// in the space between the menu and the content
padding: const EdgeInsets.only(right: 4) +
const EdgeInsets.symmetric(vertical: 16),
physics: const ClampingScrollPhysics(),
child: SeparatedColumn(
separatorBuilder: () => const VSpace(16),
children: [
SettingsMenuElement(
page: SettingsPage.account,
selectedPage: currentPage,
label: LocaleKeys.settings_accountPage_menuLabel.tr(),
icon: const FlowySvg(FlowySvgs.settings_account_m),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.appearance,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_appearance.tr(),
icon: Icon(
Icons.brightness_4,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.language,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_language.tr(),
icon: Icon(
Icons.translate,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.files,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_files.tr(),
icon: Icon(
Icons.file_present_outlined,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.notifications,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_notifications.tr(),
icon: Icon(
Icons.notifications_outlined,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.cloud,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_cloudSettings.tr(),
icon: Icon(
Icons.sync,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.shortcuts,
selectedPage: currentPage,
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
icon: Icon(
Icons.cut,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
if (FeatureFlag.membersSettings.isOn &&
userProfile.authenticator ==
AuthenticatorPB.AppFlowyCloud)
SettingsMenuElement(
page: SettingsPage.member,
selectedPage: currentPage,
label: LocaleKeys.settings_appearance_members_label.tr(),
icon: Icon(
Icons.people,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
if (kDebugMode)
SettingsMenuElement(
// no need to translate this page
page: SettingsPage.featureFlags,
selectedPage: currentPage,
label: 'Feature Flags',
icon: Icon(
Icons.flag,
color: AFThemeExtension.of(context).textColor,
),
changeSelectedPage: changeSelectedPage,
),
],
),
),
],
),
),
),
],
);
}
}

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
class SettingsMenuElement extends StatelessWidget {
const SettingsMenuElement({
@ -17,27 +19,22 @@ class SettingsMenuElement extends StatelessWidget {
final SettingsPage page;
final SettingsPage selectedPage;
final String label;
final IconData icon;
final Widget icon;
final Function changeSelectedPage;
@override
Widget build(BuildContext context) {
return FlowyHover(
isSelected: () => page == selectedPage,
resetHoverOnRebuild: false,
style: HoverStyle(
hoverColor: Theme.of(context).colorScheme.primary,
hoverColor: AFThemeExtension.of(context).greySelect,
borderRadius: BorderRadius.circular(4),
),
child: ListTile(
leading: Icon(
icon,
size: 16,
color: page == selectedPage
? Theme.of(context).colorScheme.onSurface
: null,
),
onTap: () {
changeSelectedPage(page);
},
dense: true,
leading: icon,
onTap: () => changeSelectedPage(page),
selected: page == selectedPage,
selectedColor: Theme.of(context).colorScheme.onSurface,
selectedTileColor: Theme.of(context).colorScheme.primary,
@ -45,7 +42,7 @@ class SettingsMenuElement extends StatelessWidget {
borderRadius: BorderRadius.circular(5),
),
minLeadingWidth: 0,
title: FlowyText.semibold(
title: FlowyText.medium(
label,
fontSize: FontSizes.s14,
overflow: TextOverflow.ellipsis,

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsNotificationsView extends StatelessWidget {
@ -12,32 +15,26 @@ class SettingsNotificationsView extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
builder: (context, state) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FlowySettingListTile(
label: LocaleKeys
.settings_notifications_enableNotifications_label
.tr(),
hint: LocaleKeys.settings_notifications_enableNotifications_hint
.tr(),
trailing: [
Switch(
value: state.isNotificationsEnabled,
splashRadius: 0,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (value) {
context
.read<NotificationSettingsCubit>()
.toggleNotificationsEnabled();
},
),
],
),
],
),
return SettingsBody(
children: [
SettingsHeader(title: LocaleKeys.settings_menu_notifications.tr()),
FlowySettingListTile(
label: LocaleKeys.settings_notifications_enableNotifications_label
.tr(),
hint: LocaleKeys.settings_notifications_enableNotifications_hint
.tr(),
trailing: [
Switch(
value: state.isNotificationsEnabled,
splashRadius: 0,
activeColor: Theme.of(context).colorScheme.primary,
onChanged: (value) => context
.read<NotificationSettingsCubit>()
.toggleNotificationsEnabled(),
),
],
),
],
);
},
);

View File

@ -1,559 +0,0 @@
import 'dart:async';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'setting_third_party_login.dart';
const defaultUserAvatar = '1F600';
const _iconSize = Size(60, 60);
class SettingsUserView extends StatelessWidget {
SettingsUserView(
this.user, {
required this.didLogin,
required this.didLogout,
required this.didOpenUser,
}) : super(key: ValueKey(user.id));
// Called when the user login in the setting dialog
final VoidCallback didLogin;
// Called when the user logout in the setting dialog
final VoidCallback didLogout;
// Called when the user open a historical user in the setting dialog
final VoidCallback didOpenUser;
final UserProfilePB user;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsUserViewBloc>(
create: (context) => getIt<SettingsUserViewBloc>(param1: user)
..add(const SettingsUserEvent.initial()),
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
builder: (context, state) => SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
_buildUserIconSetting(context),
if (isAuthEnabled &&
user.authenticator != AuthenticatorPB.Local) ...[
const VSpace(12),
UserEmailInput(user.email),
],
const VSpace(12),
_renderCurrentOpenaiKey(context),
const VSpace(12),
_renderCurrentStabilityAIKey(context),
const VSpace(12),
_renderLoginOrLogoutButton(context, state),
const VSpace(12),
],
),
),
),
);
}
Row _buildUserIconSetting(BuildContext context) {
return Row(
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _showIconPickerDialog(context),
child: FlowyHover(
style: const HoverStyle.transparent(),
builder: (context, onHover) {
Widget avatar = UserAvatar(
iconUrl: user.iconUrl,
name: user.name,
isLarge: true,
);
if (onHover) {
avatar = _avatarOverlay(
context: context,
hasIcon: user.iconUrl.isNotEmpty,
child: avatar,
);
}
return avatar;
},
),
),
const HSpace(12),
Flexible(child: _renderUserNameInput(context)),
],
);
}
Future<void> _showIconPickerDialog(BuildContext context) {
return showDialog(
context: context,
builder: (dialogContext) => SimpleDialog(
title: FlowyText.medium(
LocaleKeys.settings_user_selectAnIcon.tr(),
fontSize: FontSizes.s16,
),
children: [
Container(
height: 380,
width: 360,
margin: const EdgeInsets.symmetric(horizontal: 12),
child: FlowyEmojiPicker(
onEmojiSelected: (_, emoji) {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserIcon(iconUrl: emoji));
Navigator.of(dialogContext).pop();
},
),
),
],
),
);
}
/// Renders either a login or logout button based on the user's authentication status, or nothing if Supabase is not enabled.
///
/// This function checks the current user's authentication type and Supabase
/// configuration to determine whether to render a third-party login button
/// or a logout button.
Widget _renderLoginOrLogoutButton(
BuildContext context,
SettingsUserState state,
) {
if (!isAuthEnabled) {
return const SizedBox.shrink();
}
// If the user is logged in locally, render a third-party login button.
if (state.userProfile.authenticator == AuthenticatorPB.Local) {
return SettingThirdPartyLogin(didLogin: didLogin);
}
return SettingLogoutButton(user: user, didLogout: didLogout);
}
Widget _renderUserNameInput(BuildContext context) {
final String name =
context.read<SettingsUserViewBloc>().state.userProfile.name;
return UserNameInput(name);
}
Widget _renderCurrentOpenaiKey(BuildContext context) {
final String accessKey =
context.read<SettingsUserViewBloc>().state.userProfile.openaiKey;
return _AIAccessKeyInput(
accessKey: accessKey,
title: 'OpenAI Key',
hintText: LocaleKeys.settings_user_pleaseInputYourOpenAIKey.tr(),
callback: (key) => context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserOpenAIKey(key)),
);
}
Widget _renderCurrentStabilityAIKey(BuildContext context) {
final String accessKey =
context.read<SettingsUserViewBloc>().state.userProfile.stabilityAiKey;
return _AIAccessKeyInput(
accessKey: accessKey,
title: 'Stability AI Key',
hintText: LocaleKeys.settings_user_pleaseInputYourStabilityAIKey.tr(),
callback: (key) => context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserStabilityAIKey(key)),
);
}
Widget _avatarOverlay({
required BuildContext context,
required bool hasIcon,
required Widget child,
}) =>
FlowyTooltip(
message: LocaleKeys.settings_user_tooltipSelectIcon.tr(),
child: Stack(
children: [
Container(
width: 56,
height: 56,
foregroundDecoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(hasIcon ? 0.8 : 0.5),
shape: BoxShape.circle,
),
child: child,
),
const Positioned(
top: 0,
left: 0,
bottom: 0,
right: 0,
child: Center(
child: SizedBox(
width: 32,
height: 32,
child: FlowySvg(FlowySvgs.emoji_s),
),
),
),
],
),
);
}
@visibleForTesting
class UserNameInput extends StatefulWidget {
const UserNameInput(this.name, {super.key});
final String name;
@override
UserNameInputState createState() => UserNameInputState();
}
class UserNameInputState extends State<UserNameInput> {
late TextEditingController _controller;
Timer? _debounce;
final Duration _debounceDuration = const Duration(milliseconds: 500);
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.name);
}
@override
void dispose() {
_controller.dispose();
_debounce?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
labelText: LocaleKeys.settings_user_name.tr(),
labelStyle: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.w500),
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: Theme.of(context).colorScheme.onBackground),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
),
),
onChanged: (val) {
if (_debounce?.isActive ?? false) {
_debounce!.cancel();
}
_debounce = Timer(_debounceDuration, () {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserName(val));
});
},
);
}
}
@visibleForTesting
class UserEmailInput extends StatefulWidget {
const UserEmailInput(this.email, {super.key});
final String email;
@override
UserEmailInputState createState() => UserEmailInputState();
}
class UserEmailInputState extends State<UserEmailInput> {
late TextEditingController _controller;
Timer? _debounce;
final Duration _debounceDuration = const Duration(milliseconds: 500);
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.email);
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
labelText: LocaleKeys.settings_user_email.tr(),
labelStyle: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.w500),
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: Theme.of(context).colorScheme.onBackground),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
),
),
onChanged: (val) {
if (_debounce?.isActive ?? false) {
_debounce!.cancel();
}
_debounce = Timer(_debounceDuration, () {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserEmail(val));
});
},
);
}
@override
void dispose() {
_controller.dispose();
_debounce?.cancel();
super.dispose();
}
}
class _AIAccessKeyInput extends StatefulWidget {
const _AIAccessKeyInput({
required this.accessKey,
required this.title,
required this.hintText,
required this.callback,
});
final String accessKey;
final String title;
final String hintText;
final void Function(String key) callback;
@override
State<_AIAccessKeyInput> createState() => _AIAccessKeyInputState();
}
class _AIAccessKeyInputState extends State<_AIAccessKeyInput> {
bool visible = false;
final textEditingController = TextEditingController();
final debounce = Debounce();
@override
void initState() {
super.initState();
textEditingController.text = widget.accessKey;
}
@override
void dispose() {
textEditingController.dispose();
debounce.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: textEditingController,
obscureText: !visible,
decoration: InputDecoration(
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: Theme.of(context).colorScheme.onBackground),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
),
labelText: widget.title,
labelStyle: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.w500),
hintText: widget.hintText,
suffixIcon: FlowyIconButton(
width: 40,
height: 40,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
icon: Icon(
visible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
visible = !visible;
});
},
),
),
onChanged: (value) {
debounce.call(() {
widget.callback(value);
});
},
);
}
}
typedef SelectIconCallback = void Function(String iconUrl, bool isSelected);
final builtInSVGIcons = [
'1F9CC',
'1F9DB',
'1F9DD-200D-2642-FE0F',
'1F9DE-200D-2642-FE0F',
'1F9DF',
'1F42F',
'1F43A',
'1F431',
'1F435',
'1F600',
'1F984',
];
// REMOVE this widget in next version 0.3.10
class IconGallery extends StatelessWidget {
const IconGallery({
super.key,
required this.selectedIcon,
required this.onSelectIcon,
this.defaultOption,
});
final String selectedIcon;
final SelectIconCallback onSelectIcon;
final Widget? defaultOption;
@override
Widget build(BuildContext context) {
return GridView.count(
padding: const EdgeInsets.all(20),
crossAxisCount: 5,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
children: [
if (defaultOption != null) defaultOption!,
...builtInSVGIcons.mapIndexed(
(int index, String iconUrl) => IconOption(
emoji: FlowySvgData('emoji/$iconUrl'),
iconUrl: iconUrl,
onSelectIcon: onSelectIcon,
isSelected: iconUrl == selectedIcon,
),
),
],
);
}
}
class IconOption extends StatelessWidget {
IconOption({
required this.emoji,
required this.iconUrl,
required this.onSelectIcon,
required this.isSelected,
}) : super(key: ValueKey(emoji));
final FlowySvgData emoji;
final String iconUrl;
final SelectIconCallback onSelectIcon;
final bool isSelected;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: Corners.s8Border,
hoverColor: Theme.of(context).colorScheme.tertiaryContainer,
onTap: () => onSelectIcon(iconUrl, isSelected),
child: DecoratedBox(
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.transparent,
borderRadius: Corners.s8Border,
),
child: FlowySvg(
emoji,
size: _iconSize,
blendMode: null,
),
),
);
}
}
class SettingLogoutButton extends StatelessWidget {
const SettingLogoutButton({
super.key,
required this.user,
required this.didLogout,
});
final UserProfilePB user;
final VoidCallback didLogout;
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: 160,
child: FlowyButton(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 2.0),
text: FlowyText.medium(
LocaleKeys.settings_menu_logout.tr(),
fontSize: 13,
textAlign: TextAlign.center,
),
onTap: () {
NavigatorAlertDialog(
title: logoutPromptMessage(),
confirm: () async {
await getIt<AuthService>().signOut();
didLogout();
},
).show(context);
},
),
),
);
}
String logoutPromptMessage() {
switch (user.encryptionType) {
case EncryptionTypePB.Symmetric:
return LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr();
default:
return LocaleKeys.settings_menu_logoutPrompt.tr();
}
}
}

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
@ -11,7 +13,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MoreViewActions extends StatefulWidget {
@ -107,7 +108,7 @@ class _MoreViewActionsState extends State<MoreViewActions> {
FlowySvgs.three_dots_vertical_s,
size: const Size.square(16),
color: isHovering
? Theme.of(context).colorScheme.onPrimary
? Theme.of(context).colorScheme.onSecondary
: Theme.of(context).iconTheme.color,
),
),

Some files were not shown because too many files have changed in this diff Show More