mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 19:17:43 +03:00
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:
parent
f3544375c9
commit
4981baac13
@ -3,21 +3,28 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/startup.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/af_cloud_mock_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.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/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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.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:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
import '../desktop/board/board_hide_groups_test.dart';
|
||||||
import '../shared/dir.dart';
|
import '../shared/dir.dart';
|
||||||
import '../shared/mock/mock_file_picker.dart';
|
import '../shared/mock/mock_file_picker.dart';
|
||||||
import '../shared/util.dart';
|
import '../shared/util.dart';
|
||||||
@ -37,22 +44,35 @@ void main() {
|
|||||||
|
|
||||||
// reanme the name of the anon user
|
// reanme the name of the anon user
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
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.pumpAndSettle();
|
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
|
// sign up with Google
|
||||||
await tester.tapGoogleLoginInButton();
|
await tester.tapGoogleLoginInButton();
|
||||||
|
|
||||||
// sign out
|
// sign out
|
||||||
await tester.expectToSeeHomePage();
|
await tester.expectToSeeHomePage();
|
||||||
await tester.openSettings();
|
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.logout();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@ -63,8 +83,9 @@ void main() {
|
|||||||
|
|
||||||
// New anon user name
|
// New anon user name
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
final userNameInput = tester.widget(userNameFinder) as UserNameInput;
|
final userNameInput =
|
||||||
|
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
||||||
expect(userNameInput.name, 'Me');
|
expect(userNameInput.name, 'Me');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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/af_cloud_mock_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.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/setting_appflowy_cloud.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -37,11 +37,18 @@ void main() {
|
|||||||
|
|
||||||
// Open the setting page and sign out
|
// Open the setting page and sign out
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
await tester.tapButton(find.byType(SettingLogoutButton));
|
|
||||||
|
|
||||||
tester.expectToSeeText(LocaleKeys.button_ok.tr());
|
// Scroll to sign-out
|
||||||
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
|
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
|
// Go to the sign in page again
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
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
|
// should not see the sync setting page when sign in as anonymous
|
||||||
await tester.openSettings();
|
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();
|
tester.expectToSeeGoogleLoginButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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/af_cloud_mock_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.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/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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
import '../shared/dir.dart';
|
import '../shared/dir.dart';
|
||||||
import '../shared/mock/mock_file_picker.dart';
|
import '../shared/mock/mock_file_picker.dart';
|
||||||
import '../shared/util.dart';
|
import '../shared/util.dart';
|
||||||
@ -50,7 +51,7 @@ void main() {
|
|||||||
await tester.waitForSeconds(6);
|
await tester.waitForSeconds(6);
|
||||||
|
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
await tester.logout();
|
await tester.logout();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import 'package:appflowy/env/cloud_env.dart';
|
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/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/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:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
@ -25,11 +23,8 @@ void main() {
|
|||||||
|
|
||||||
// Open the setting page and sign out
|
// Open the setting page and sign out
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
await tester.tapButton(find.byType(SettingLogoutButton));
|
await tester.logout();
|
||||||
|
|
||||||
tester.expectToSeeText(LocaleKeys.button_ok.tr());
|
|
||||||
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
|
|
||||||
|
|
||||||
// Go to the sign in page again
|
// Go to the sign in page again
|
||||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
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
|
// should not see the sync setting page when sign in as anonymous
|
||||||
await tester.openSettings();
|
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();
|
tester.expectToSeeGoogleLoginButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,22 +2,27 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/startup.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/af_cloud_mock_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.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/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/workspace/presentation/widgets/user_avatar.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.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:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:integration_test/integration_test.dart';
|
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/database_test_op.dart';
|
||||||
import '../shared/dir.dart';
|
import '../shared/dir.dart';
|
||||||
import '../shared/emoji.dart';
|
import '../shared/emoji.dart';
|
||||||
@ -39,28 +44,9 @@ void main() {
|
|||||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||||
|
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
// final userAvatarFinder = find.descendant(
|
|
||||||
// of: find.byType(SettingsUserView),
|
|
||||||
// matching: find.byType(UserAvatar),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Open icon picker dialog and select emoji
|
await tester.enterUserName(name);
|
||||||
// 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.tapEscButton();
|
await tester.tapEscButton();
|
||||||
|
|
||||||
// wait 2 seconds for the sync to finish
|
// wait 2 seconds for the sync to finish
|
||||||
@ -78,23 +64,12 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.user);
|
await tester.openSettingsPage(SettingsPage.account);
|
||||||
|
|
||||||
// verify icon
|
// Verify name
|
||||||
// final userAvatarFinder = find.descendant(
|
final profileSetting =
|
||||||
// of: find.byType(SettingsUserView),
|
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
||||||
// matching: find.byType(UserAvatar),
|
|
||||||
// );
|
|
||||||
// final UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
|
|
||||||
// expect(userAvatar.iconUrl, '😁');
|
|
||||||
|
|
||||||
// verify name
|
expect(profileSetting.name, 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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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/application/settings/prelude.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.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/setting_appflowy_cloud.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.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_actions.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.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/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/workspace/presentation/widgets/user_avatar.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
@ -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/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.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/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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
@ -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/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.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/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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
|
@ -1,25 +1,33 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.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_appflowy_cloud.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'base.dart';
|
import 'util.dart';
|
||||||
import 'expectation.dart';
|
|
||||||
|
|
||||||
extension AppFlowyAuthTest on WidgetTester {
|
extension AppFlowyAuthTest on WidgetTester {
|
||||||
Future<void> tapGoogleLoginInButton() async {
|
Future<void> tapGoogleLoginInButton() async {
|
||||||
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
|
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Requires being on the SettingsPage.account of the SettingsDialog
|
||||||
Future<void> logout() async {
|
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 tapButton(find.byType(SignInOutButton));
|
||||||
await tapButtonWithName(LocaleKeys.button_ok.tr());
|
|
||||||
|
expectToSeeText(LocaleKeys.button_confirm.tr());
|
||||||
|
await tapButtonWithName(LocaleKeys.button_confirm.tr());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapSignInAsGuest() async {
|
Future<void> tapSignInAsGuest() async {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
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.dart';
|
||||||
import 'package:appflowy/env/cloud_env_test.dart';
|
import 'package:appflowy/env/cloud_env_test.dart';
|
||||||
import 'package:appflowy/startup/entry_point.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:appflowy/workspace/application/settings/prelude.dart';
|
||||||
import 'package:flowy_infra/uuid.dart';
|
import 'package:flowy_infra/uuid.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter_test/flutter_test.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
class FlowyTestContext {
|
class FlowyTestContext {
|
||||||
FlowyTestContext({
|
FlowyTestContext({required this.applicationDataDirectory});
|
||||||
required this.applicationDataDirectory,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String applicationDataDirectory;
|
final String applicationDataDirectory;
|
||||||
}
|
}
|
||||||
@ -75,7 +74,7 @@ extension AppFlowyTestBase on WidgetTester {
|
|||||||
if (cloudType != null) {
|
if (cloudType != null) {
|
||||||
switch (cloudType) {
|
switch (cloudType) {
|
||||||
case AuthenticatorType.local:
|
case AuthenticatorType.local:
|
||||||
await useLocal();
|
await useLocalServer();
|
||||||
break;
|
break;
|
||||||
case AuthenticatorType.supabase:
|
case AuthenticatorType.supabase:
|
||||||
await useTestSupabaseCloud();
|
await useTestSupabaseCloud();
|
||||||
@ -187,37 +186,14 @@ extension AppFlowyTestBase on WidgetTester {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapButtonWithName(
|
Future<void> tapButtonWithName(String tr, {int milliseconds = 500}) async {
|
||||||
String tr, {
|
Finder button = find.text(tr, findRichText: true, skipOffstage: false);
|
||||||
int milliseconds = 500,
|
|
||||||
}) async {
|
|
||||||
Finder button = find.text(
|
|
||||||
tr,
|
|
||||||
findRichText: true,
|
|
||||||
skipOffstage: false,
|
|
||||||
);
|
|
||||||
if (button.evaluate().isEmpty) {
|
if (button.evaluate().isEmpty) {
|
||||||
button = find.byWidgetPredicate(
|
button = find.byWidgetPredicate(
|
||||||
(widget) => widget is FlowyText && widget.text == tr,
|
(widget) => widget is FlowyText && widget.text == tr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await tapButton(
|
await tapButton(button, milliseconds: milliseconds);
|
||||||
button,
|
|
||||||
milliseconds: milliseconds,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapButtonWithTooltip(
|
|
||||||
String tr, {
|
|
||||||
int milliseconds = 500,
|
|
||||||
}) async {
|
|
||||||
final button = find.byTooltip(tr);
|
|
||||||
await tapButton(
|
|
||||||
button,
|
|
||||||
milliseconds: milliseconds,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> doubleTapAt(
|
Future<void> doubleTapAt(
|
||||||
@ -232,34 +208,8 @@ extension AppFlowyTestBase on WidgetTester {
|
|||||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
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 {
|
Future<void> wait(int milliseconds) async {
|
||||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,10 +221,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> useLocal() async {
|
|
||||||
await useLocalServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> useTestSupabaseCloud() async {
|
Future<void> useTestSupabaseCloud() async {
|
||||||
await useSupabaseCloud(
|
await useSupabaseCloud(
|
||||||
url: TestEnv.supabaseUrl,
|
url: TestEnv.supabaseUrl,
|
||||||
|
@ -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/flowy_tab.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.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/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/settings/widgets/settings_language_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -72,27 +73,6 @@ extension CommonOperations on WidgetTester {
|
|||||||
await tapButton(newPageButton);
|
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.
|
/// Tap the import button.
|
||||||
///
|
///
|
||||||
/// Must call [tapAddViewButton] first.
|
/// Must call [tapAddViewButton] first.
|
||||||
@ -181,15 +161,9 @@ extension CommonOperations on WidgetTester {
|
|||||||
}) async {
|
}) async {
|
||||||
final pageNames = findPageName(name, layout: layout);
|
final pageNames = findPageName(name, layout: layout);
|
||||||
if (useLast) {
|
if (useLast) {
|
||||||
await hoverOnWidget(
|
await hoverOnWidget(pageNames.last, onHover: onHover);
|
||||||
pageNames.last,
|
|
||||||
onHover: onHover,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await hoverOnWidget(
|
await hoverOnWidget(pageNames.first, onHover: onHover);
|
||||||
pageNames.first,
|
|
||||||
onHover: onHover,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,9 +471,7 @@ extension CommonOperations on WidgetTester {
|
|||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openNotificationHub({
|
Future<void> openNotificationHub({int tabIndex = 0}) async {
|
||||||
int tabIndex = 0,
|
|
||||||
}) async {
|
|
||||||
final finder = find.descendant(
|
final finder = find.descendant(
|
||||||
of: find.byType(NotificationButton),
|
of: find.byType(NotificationButton),
|
||||||
matching: find.byWidgetPredicate(
|
matching: find.byWidgetPredicate(
|
||||||
@ -542,15 +514,6 @@ extension CommonOperations on WidgetTester {
|
|||||||
await tapButton(workspace, milliseconds: 2000);
|
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 {
|
Future<void> createCollaborativeWorkspace(String name) async {
|
||||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||||
throw UnsupportedError('Collaborative workspace is not enabled');
|
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 {
|
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||||
String get menuName {
|
String get menuName {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy/core/config/kv_keys.dart';
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
import 'package:archive/archive_io.dart';
|
import 'package:archive/archive_io.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@ -51,11 +52,7 @@ class TestWorkspaceService {
|
|||||||
Future<void> setUpAll() async {
|
Future<void> setUpAll() async {
|
||||||
final root = await workspace.root;
|
final root = await workspace.root;
|
||||||
final path = root.path;
|
final path = root.path;
|
||||||
SharedPreferences.setMockInitialValues(
|
SharedPreferences.setMockInitialValues({KVKeys.pathLocation: path});
|
||||||
{
|
|
||||||
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.
|
/// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control.
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import 'dart:io';
|
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/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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/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/footer/grid_footer.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.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/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/create_sort_list.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.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_cell_editor.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.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/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/accessory/cell_accessory.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
|
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/row/row_banner.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/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/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/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/date_picker/widgets/reminder_selector.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
@ -343,16 +342,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
return w.isToday;
|
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 {
|
Future<void> tapChangeDateTimeFormatButton() async {
|
||||||
await tapButton(find.byType(DateTypeOptionButton));
|
await tapButton(find.byType(DateTypeOptionButton));
|
||||||
}
|
}
|
||||||
@ -403,9 +392,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The [SelectOptionCellEditor] must be opened first.
|
/// The [SelectOptionCellEditor] must be opened first.
|
||||||
Future<void> createOption({
|
Future<void> createOption({required String name}) async {
|
||||||
required String name,
|
|
||||||
}) async {
|
|
||||||
final findEditor = find.byType(SelectOptionCellEditor);
|
final findEditor = find.byType(SelectOptionCellEditor);
|
||||||
expect(findEditor, findsOneWidget);
|
expect(findEditor, findsOneWidget);
|
||||||
|
|
||||||
@ -419,9 +406,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> selectOption({
|
Future<void> selectOption({required String name}) async {
|
||||||
required String name,
|
|
||||||
}) async {
|
|
||||||
final option = find.byWidgetPredicate(
|
final option = find.byWidgetPredicate(
|
||||||
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
|
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
|
||||||
);
|
);
|
||||||
@ -440,11 +425,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
(widget.name == name || widget.option?.name == name),
|
(widget.name == name || widget.option?.name == name),
|
||||||
);
|
);
|
||||||
|
|
||||||
final cell = find.descendant(
|
final cell = find.descendant(of: findRow.at(rowIndex), matching: option);
|
||||||
of: findRow.at(rowIndex),
|
|
||||||
matching: option,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(cell, findsOneWidget);
|
expect(cell, findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,11 +439,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
(widget) => widget is SelectOptionTag,
|
(widget) => widget is SelectOptionTag,
|
||||||
);
|
);
|
||||||
|
|
||||||
final cell = find.descendant(
|
final cell = find.descendant(of: findRow.at(rowIndex), matching: options);
|
||||||
of: findRow.at(rowIndex),
|
|
||||||
matching: options,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(cell, matcher);
|
expect(cell, matcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -470,21 +447,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
final findRow = find.byType(GridRow);
|
final findRow = find.byType(GridRow);
|
||||||
final findCell = finderForFieldType(FieldType.Checklist);
|
final findCell = finderForFieldType(FieldType.Checklist);
|
||||||
|
|
||||||
final cell = find.descendant(
|
final cell = find.descendant(of: findRow.at(rowIndex), matching: findCell);
|
||||||
of: findRow.at(rowIndex),
|
|
||||||
matching: findCell,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tapButton(cell);
|
await tapButton(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
void assertChecklistEditorVisible({required bool visible}) {
|
void assertChecklistEditorVisible({required bool visible}) {
|
||||||
final editor = find.byType(ChecklistCellEditor);
|
final editor = find.byType(ChecklistCellEditor);
|
||||||
if (visible) {
|
if (visible) {
|
||||||
expect(editor, findsOneWidget);
|
return expect(editor, findsOneWidget);
|
||||||
} else {
|
|
||||||
expect(editor, findsNothing);
|
|
||||||
}
|
}
|
||||||
|
expect(editor, findsNothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createNewChecklistTask({
|
Future<void> createNewChecklistTask({
|
||||||
@ -519,7 +491,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
required bool isChecked,
|
required bool isChecked,
|
||||||
}) {
|
}) {
|
||||||
final task = find.byType(ChecklistItem).at(index);
|
final task = find.byType(ChecklistItem).at(index);
|
||||||
|
|
||||||
final widget = this.widget<ChecklistItem>(task);
|
final widget = this.widget<ChecklistItem>(task);
|
||||||
assert(
|
assert(
|
||||||
widget.task.data.name == name && widget.task.isSelected == isChecked,
|
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 {
|
Future<void> hoverRowBanner() async {
|
||||||
final banner = find.byType(RowBanner);
|
final banner = find.byType(RowBanner);
|
||||||
expect(banner, findsOneWidget);
|
expect(banner, findsOneWidget);
|
||||||
|
|
||||||
await startGesture(
|
await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse);
|
||||||
getCenter(banner),
|
|
||||||
kind: PointerDeviceKind.mouse,
|
|
||||||
);
|
|
||||||
|
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> openEmojiPicker() async {
|
Future<void> openEmojiPicker() async =>
|
||||||
await tapButton(find.byType(AddEmojiButton));
|
tapButton(find.byType(AddEmojiButton));
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapDateCellInRowDetailPage() async {
|
Future<void> tapDateCellInRowDetailPage() async {
|
||||||
final findDateCell = find.byType(EditableDateCell);
|
final findDateCell = find.byType(EditableDateCell);
|
||||||
@ -630,25 +590,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await pumpAndSettle();
|
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 {
|
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
|
||||||
final fieldButtons = find.byType(FieldCellButton);
|
final fieldButtons = find.byType(FieldCellButton);
|
||||||
final button = find
|
final button = find
|
||||||
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
|
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
|
||||||
.at(index);
|
.at(index);
|
||||||
return startGesture(
|
return startGesture(getCenter(button), kind: PointerDeviceKind.mouse);
|
||||||
getCenter(button),
|
|
||||||
kind: PointerDeviceKind.mouse,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reorderFieldInRowDetail({required double offset}) async {
|
Future<void> reorderFieldInRowDetail({required double offset}) async {
|
||||||
@ -657,11 +604,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
(widget) => widget is ReorderableDragStartListener && widget.enabled,
|
(widget) => widget is ReorderableDragStartListener && widget.enabled,
|
||||||
)
|
)
|
||||||
.first;
|
.first;
|
||||||
await drag(
|
await drag(thumb, Offset(0, offset), kind: PointerDeviceKind.mouse);
|
||||||
thumb,
|
|
||||||
Offset(0, offset),
|
|
||||||
kind: PointerDeviceKind.mouse,
|
|
||||||
);
|
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -681,8 +624,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
|
|
||||||
Future<void> tapDeletePropertyInFieldEditor() async {
|
Future<void> tapDeletePropertyInFieldEditor() async {
|
||||||
final deleteButton = find.byWidgetPredicate(
|
final deleteButton = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is FieldActionCell && w.action == FieldAction.delete,
|
||||||
widget is FieldActionCell && widget.action == FieldAction.delete,
|
|
||||||
);
|
);
|
||||||
await tapButton(deleteButton);
|
await tapButton(deleteButton);
|
||||||
|
|
||||||
@ -693,11 +635,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(confirmButton);
|
await tapButton(confirmButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> scrollGridByOffset(Offset offset) async {
|
|
||||||
await drag(find.byType(GridPage), offset);
|
|
||||||
await pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> scrollRowDetailByOffset(Offset offset) async {
|
Future<void> scrollRowDetailByOffset(Offset offset) async {
|
||||||
await drag(find.byType(RowDetailPage), offset);
|
await drag(find.byType(RowDetailPage), offset);
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
@ -756,8 +693,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
/// Should call [tapGridFieldWithName] first.
|
/// Should call [tapGridFieldWithName] first.
|
||||||
Future<void> tapDeletePropertyButton() async {
|
Future<void> tapDeletePropertyButton() async {
|
||||||
final field = find.byWidgetPredicate(
|
final field = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is FieldActionCell && w.action == FieldAction.delete,
|
||||||
widget is FieldActionCell && widget.action == FieldAction.delete,
|
|
||||||
);
|
);
|
||||||
await tapButton(field);
|
await tapButton(field);
|
||||||
}
|
}
|
||||||
@ -765,9 +701,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
/// A SimpleDialog must be shown first, e.g. when deleting a field.
|
/// A SimpleDialog must be shown first, e.g. when deleting a field.
|
||||||
Future<void> tapDialogOkButton() async {
|
Future<void> tapDialogOkButton() async {
|
||||||
final field = find.byWidgetPredicate(
|
final field = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is PrimaryTextButton && w.label == LocaleKeys.button_ok.tr(),
|
||||||
widget is PrimaryTextButton &&
|
|
||||||
widget.label == LocaleKeys.button_ok.tr(),
|
|
||||||
);
|
);
|
||||||
await tapButton(field);
|
await tapButton(field);
|
||||||
}
|
}
|
||||||
@ -775,8 +709,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
/// Should call [tapGridFieldWithName] first.
|
/// Should call [tapGridFieldWithName] first.
|
||||||
Future<void> tapDuplicatePropertyButton() async {
|
Future<void> tapDuplicatePropertyButton() async {
|
||||||
final field = find.byWidgetPredicate(
|
final field = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is FieldActionCell && w.action == FieldAction.duplicate,
|
||||||
widget is FieldActionCell && widget.action == FieldAction.duplicate,
|
|
||||||
);
|
);
|
||||||
await tapButton(field);
|
await tapButton(field);
|
||||||
}
|
}
|
||||||
@ -798,45 +731,34 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
/// Should call [tapGridFieldWithName] first.
|
/// Should call [tapGridFieldWithName] first.
|
||||||
Future<void> tapHidePropertyButton() async {
|
Future<void> tapHidePropertyButton() async {
|
||||||
final field = find.byWidgetPredicate(
|
final field = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
|
||||||
widget is FieldActionCell &&
|
|
||||||
widget.action == FieldAction.toggleVisibility,
|
|
||||||
);
|
);
|
||||||
await tapButton(field);
|
await tapButton(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapHidePropertyButtonInFieldEditor() async {
|
Future<void> tapHidePropertyButtonInFieldEditor() async {
|
||||||
final button = find.byWidgetPredicate(
|
final button = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
|
||||||
widget is FieldActionCell &&
|
|
||||||
widget.action == FieldAction.toggleVisibility,
|
|
||||||
);
|
);
|
||||||
await tapButton(button);
|
await tapButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapRowDetailPageRowActionButton() async {
|
Future<void> tapRowDetailPageRowActionButton() async =>
|
||||||
await tapButton(find.byType(RowActionButton));
|
tapButton(find.byType(RowActionButton));
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
Future<void> tapRowDetailPageCreatePropertyButton() async =>
|
||||||
await tapButton(find.byType(CreateRowFieldButton));
|
tapButton(find.byType(CreateRowFieldButton));
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapRowDetailPageDeleteRowButton() async {
|
Future<void> tapRowDetailPageDeleteRowButton() async =>
|
||||||
await tapButton(find.byType(RowDetailPageDeleteButton));
|
tapButton(find.byType(RowDetailPageDeleteButton));
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapRowDetailPageDuplicateRowButton() async {
|
Future<void> tapRowDetailPageDuplicateRowButton() async =>
|
||||||
await tapButton(find.byType(RowDetailPageDuplicateButton));
|
tapButton(find.byType(RowDetailPageDuplicateButton));
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapSwitchFieldTypeButton() async {
|
Future<void> tapSwitchFieldTypeButton() async =>
|
||||||
await tapButton(find.byType(SwitchFieldButton));
|
tapButton(find.byType(SwitchFieldButton));
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapEscButton() async {
|
Future<void> tapEscButton() async => sendKeyEvent(LogicalKeyboardKey.escape);
|
||||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Must call [tapSwitchFieldTypeButton] first.
|
/// Must call [tapSwitchFieldTypeButton] first.
|
||||||
Future<void> selectFieldType(FieldType fieldType) async {
|
Future<void> selectFieldType(FieldType fieldType) async {
|
||||||
@ -851,15 +773,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use in edit mode of FieldEditor
|
// Use in edit mode of FieldEditor
|
||||||
void expectEmptyTypeOptionEditor() {
|
void expectEmptyTypeOptionEditor() => expect(
|
||||||
expect(
|
find.descendant(
|
||||||
find.descendant(
|
of: find.byType(FieldTypeOptionEditor),
|
||||||
of: find.byType(FieldTypeOptionEditor),
|
matching: find.byType(TypeOptionSeparator),
|
||||||
matching: find.byType(TypeOptionSeparator),
|
),
|
||||||
),
|
findsNothing,
|
||||||
findsNothing,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Each field has its own cell, so we can find the corresponding cell by
|
/// Each field has its own cell, so we can find the corresponding cell by
|
||||||
/// the field type after create a new field.
|
/// the field type after create a new field.
|
||||||
@ -868,10 +788,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
expect(finder, findsWidgets);
|
expect(finder, findsWidgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> assertNumberOfFieldsInGridPage(int num) async {
|
|
||||||
expect(find.byType(GridFieldCell), findsNWidgets(num));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> assertNumberOfRowsInGridPage(int num) async {
|
Future<void> assertNumberOfRowsInGridPage(int num) async {
|
||||||
expect(
|
expect(
|
||||||
find.byType(GridRow, skipOffstage: false),
|
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.
|
/// Check the field type of the [FieldCellButton] is the same as the name.
|
||||||
Future<void> assertFieldTypeWithFieldName(
|
Future<void> assertFieldTypeWithFieldName(String name, FieldType type) async {
|
||||||
String name,
|
|
||||||
FieldType fieldType,
|
|
||||||
) async {
|
|
||||||
final field = find.byWidgetPredicate(
|
final field = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) =>
|
||||||
widget is FieldCellButton &&
|
widget is FieldCellButton &&
|
||||||
widget.field.fieldType == fieldType &&
|
widget.field.fieldType == type &&
|
||||||
widget.field.name == name,
|
widget.field.name == name,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -936,11 +849,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
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 {
|
Future<void> findDateEditor(dynamic matcher) async {
|
||||||
final finder = find.byType(DateCellEditor);
|
final finder = find.byType(DateCellEditor);
|
||||||
expect(finder, matcher);
|
expect(finder, matcher);
|
||||||
@ -994,41 +902,29 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(find.byType(SortButton));
|
await tapButton(find.byType(SortButton));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapCreateFilterByFieldType(
|
Future<void> tapCreateFilterByFieldType(FieldType type, String title) async {
|
||||||
FieldType fieldType,
|
|
||||||
String title,
|
|
||||||
) async {
|
|
||||||
final findFilter = find.byWidgetPredicate(
|
final findFilter = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) =>
|
||||||
widget is GridFilterPropertyCell &&
|
widget is GridFilterPropertyCell &&
|
||||||
widget.fieldInfo.fieldType == fieldType &&
|
widget.fieldInfo.fieldType == type &&
|
||||||
widget.fieldInfo.name == title,
|
widget.fieldInfo.name == title,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tapButton(findFilter);
|
await tapButton(findFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapFilterButtonInGrid(String filterName) async {
|
Future<void> tapFilterButtonInGrid(String name) async {
|
||||||
final findFilter = find.byType(FilterMenuItem);
|
final findFilter = find.byType(FilterMenuItem);
|
||||||
final button = find.descendant(
|
final button = find.descendant(of: findFilter, matching: find.text(name));
|
||||||
of: findFilter,
|
|
||||||
matching: find.text(filterName),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tapButton(button);
|
await tapButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapCreateSortByFieldType(
|
Future<void> tapCreateSortByFieldType(FieldType type, String title) async {
|
||||||
FieldType fieldType,
|
|
||||||
String title,
|
|
||||||
) async {
|
|
||||||
final findSort = find.byWidgetPredicate(
|
final findSort = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) =>
|
||||||
widget is GridSortPropertyCell &&
|
widget is GridSortPropertyCell &&
|
||||||
widget.fieldInfo.fieldType == fieldType &&
|
widget.fieldInfo.fieldType == type &&
|
||||||
widget.fieldInfo.name == title,
|
widget.fieldInfo.name == title,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tapButton(findSort);
|
await tapButton(findSort);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1085,10 +981,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
of: fromSortItem,
|
of: fromSortItem,
|
||||||
matching: find.byType(ReorderableDragStartListener),
|
matching: find.byType(ReorderableDragStartListener),
|
||||||
);
|
);
|
||||||
await drag(
|
await drag(dragElement, getCenter(toSortItem) - getCenter(fromSortItem));
|
||||||
dragElement,
|
|
||||||
getCenter(toSortItem) - getCenter(fromSortItem),
|
|
||||||
);
|
|
||||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1166,15 +1059,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(findCell);
|
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 {
|
Future<void> tapUnCheckedButtonOnCheckboxFilter() async {
|
||||||
final button = find.descendant(
|
final button = find.descendant(
|
||||||
of: find.byType(HoverButton),
|
of: find.byType(HoverButton),
|
||||||
@ -1193,15 +1077,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(button);
|
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.
|
/// Should call [tapDatabaseSettingButton] first.
|
||||||
Future<void> tapViewPropertiesButton() async {
|
Future<void> tapViewPropertiesButton() async {
|
||||||
final findSettingItem = find.byType(DatabaseSettingsList);
|
final findSettingItem = find.byType(DatabaseSettingsList);
|
||||||
@ -1252,16 +1127,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(button);
|
await tapButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> tapFirstDayOfWeek() async {
|
Future<void> tapFirstDayOfWeek() async =>
|
||||||
await tapButton(find.byType(FirstDayOfWeek));
|
tapButton(find.byType(FirstDayOfWeek));
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapFirstDayOfWeekStartFromSunday() async {
|
|
||||||
final finder = find.byWidgetPredicate(
|
|
||||||
(widget) => widget is StartFromButton && widget.dayIndex == 0,
|
|
||||||
);
|
|
||||||
await tapButton(finder);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapFirstDayOfWeekStartFromMonday() async {
|
Future<void> tapFirstDayOfWeekStartFromMonday() async {
|
||||||
final finder = find.byWidgetPredicate(
|
final finder = find.byWidgetPredicate(
|
||||||
@ -1277,20 +1144,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
|
|
||||||
void assertFirstDayOfWeekStartFromMonday() {
|
void assertFirstDayOfWeekStartFromMonday() {
|
||||||
final finder = find.byWidgetPredicate(
|
final finder = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is StartFromButton && w.dayIndex == 1 && w.isSelected == true,
|
||||||
widget is StartFromButton &&
|
|
||||||
widget.dayIndex == 1 &&
|
|
||||||
widget.isSelected == true,
|
|
||||||
);
|
);
|
||||||
expect(finder, findsOneWidget);
|
expect(finder, findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
void assertFirstDayOfWeekStartFromSunday() {
|
void assertFirstDayOfWeekStartFromSunday() {
|
||||||
final finder = find.byWidgetPredicate(
|
final finder = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is StartFromButton && w.dayIndex == 0 && w.isSelected == true,
|
||||||
widget is StartFromButton &&
|
|
||||||
widget.dayIndex == 0 &&
|
|
||||||
widget.isSelected == true,
|
|
||||||
);
|
);
|
||||||
expect(finder, findsOneWidget);
|
expect(finder, findsOneWidget);
|
||||||
}
|
}
|
||||||
@ -1307,11 +1168,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.first;
|
.first;
|
||||||
await scrollUntilVisible(
|
await scrollUntilVisible(todayCell, 300, scrollable: scrollable);
|
||||||
todayCell,
|
|
||||||
300,
|
|
||||||
scrollable: scrollable,
|
|
||||||
);
|
|
||||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1351,12 +1208,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
String? title,
|
String? title,
|
||||||
}) {
|
}) {
|
||||||
final findDayCell = find.byWidgetPredicate(
|
final findDayCell = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) => widget is CalendarDayCard && isSameDay(widget.date, date),
|
||||||
widget is CalendarDayCard &&
|
|
||||||
isSameDay(
|
|
||||||
widget.date,
|
|
||||||
date,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
Finder findEvents = find.descendant(
|
Finder findEvents = find.descendant(
|
||||||
of: findDayCell,
|
of: findDayCell,
|
||||||
@ -1390,13 +1242,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(cards.at(index));
|
await tapButton(cards.at(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
void assertEventEditorOpen() {
|
void assertEventEditorOpen() =>
|
||||||
expect(find.byType(CalendarEventEditor), findsOneWidget);
|
expect(find.byType(CalendarEventEditor), findsOneWidget);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> dismissEventEditor() async {
|
Future<void> dismissEventEditor() async =>
|
||||||
await simulateKeyEvent(LogicalKeyboardKey.escape);
|
simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> editEventTitle(String title) async {
|
Future<void> editEventTitle(String title) async {
|
||||||
final textField = find.descendant(
|
final textField = find.descendant(
|
||||||
@ -1507,10 +1357,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
matching: find.byType(TextField),
|
matching: find.byType(TextField),
|
||||||
);
|
);
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
expect(textField, findsOneWidget);
|
return expect(textField, findsOneWidget);
|
||||||
} else {
|
|
||||||
expect(textField, findsNothing);
|
|
||||||
}
|
}
|
||||||
|
expect(textField, findsNothing);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> enterNewGroupName(String name, {required bool submit}) async {
|
Future<void> enterNewGroupName(String name, {required bool submit}) async {
|
||||||
@ -1612,21 +1461,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
await tapButton(okButton);
|
await tapButton(okButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) {
|
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
|
||||||
switch (layout) {
|
DatabaseLayoutPB.Board =>
|
||||||
case DatabaseLayoutPB.Board:
|
expect(find.byType(BoardPage), findsOneWidget),
|
||||||
expect(find.byType(BoardPage), findsOneWidget);
|
DatabaseLayoutPB.Calendar =>
|
||||||
break;
|
expect(find.byType(CalendarPage), findsOneWidget),
|
||||||
case DatabaseLayoutPB.Calendar:
|
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
|
||||||
expect(find.byType(CalendarPage), findsOneWidget);
|
_ => throw Exception('Unknown database layout type: $layout'),
|
||||||
break;
|
};
|
||||||
case DatabaseLayoutPB.Grid:
|
|
||||||
expect(find.byType(GridPage), findsOneWidget);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw Exception('Unknown database layout type: $layout');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
||||||
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
|
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
|
||||||
@ -1634,11 +1476,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
(widget) => widget is FlowyText && widget.text == layout.layoutName,
|
(widget) => widget is FlowyText && widget.text == layout.layoutName,
|
||||||
);
|
);
|
||||||
|
|
||||||
final button = find.descendant(
|
final button = find.descendant(of: findLayoutCell, matching: findText);
|
||||||
of: findLayoutCell,
|
|
||||||
matching: findText,
|
|
||||||
);
|
|
||||||
|
|
||||||
await tapButton(button);
|
await tapButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1660,8 +1498,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
|
|
||||||
await tapButton(
|
await tapButton(
|
||||||
find.byWidgetPredicate(
|
find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is NumberFormatCell && w.format == NumberFormatPB.USD,
|
||||||
widget is NumberFormatCell && widget.format == NumberFormatPB.USD,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1675,8 +1512,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
String fieldName,
|
String fieldName,
|
||||||
) async {
|
) async {
|
||||||
final field = find.byWidgetPredicate(
|
final field = find.byWidgetPredicate(
|
||||||
(widget) =>
|
(w) => w is DatabasePropertyCell && w.fieldInfo.name == fieldName,
|
||||||
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
|
|
||||||
);
|
);
|
||||||
final toggleVisibilityButton =
|
final toggleVisibilityButton =
|
||||||
find.descendant(of: field, matching: find.byType(FlowyIconButton));
|
find.descendant(of: field, matching: find.byType(FlowyIconButton));
|
||||||
@ -1684,18 +1520,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
|
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
|
||||||
switch (layout) {
|
DatabaseLayoutPB.Board => find.byType(BoardPage),
|
||||||
case DatabaseLayoutPB.Board:
|
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
|
||||||
return find.byType(BoardPage);
|
DatabaseLayoutPB.Grid => find.byType(GridPage),
|
||||||
case DatabaseLayoutPB.Calendar:
|
_ => throw Exception('Unknown database layout type: $layout'),
|
||||||
return find.byType(CalendarPage);
|
};
|
||||||
case DatabaseLayoutPB.Grid:
|
|
||||||
return find.byType(GridPage);
|
|
||||||
default:
|
|
||||||
throw Exception('Unknown database layout type: $layout');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Finder finderForFieldType(FieldType fieldType) {
|
Finder finderForFieldType(FieldType fieldType) {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
Future<void> deleteDirectoriesWithSameBaseNameAsPrefix(
|
Future<void> deleteDirectoriesWithSameBaseNameAsPrefix(
|
||||||
String path,
|
String path,
|
||||||
|
@ -30,11 +30,8 @@ class EditorOperations {
|
|||||||
|
|
||||||
final WidgetTester tester;
|
final WidgetTester tester;
|
||||||
|
|
||||||
EditorState getCurrentEditorState() {
|
EditorState getCurrentEditorState() =>
|
||||||
return tester
|
tester.widget<AppFlowyEditor>(find.byType(AppFlowyEditor)).editorState;
|
||||||
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
|
|
||||||
.editorState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tap the line of editor at [index]
|
/// Tap the line of editor at [index]
|
||||||
Future<void> tapLineOfEditorAt(int index) async {
|
Future<void> tapLineOfEditorAt(int index) async {
|
||||||
@ -144,16 +141,8 @@ class EditorOperations {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> switchNetworkImageCover(String imageUrl) async {
|
Future<void> tapOnRemoveCover() async =>
|
||||||
final image = find.byWidgetPredicate(
|
tester.tapButton(find.byType(DeleteCoverButton));
|
||||||
(widget) => widget is ImageGridItem,
|
|
||||||
);
|
|
||||||
await tester.tapButton(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> tapOnRemoveCover() async {
|
|
||||||
await tester.tapButton(find.byType(DeleteCoverButton));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A cover must be present in the document to function properly since this
|
/// A cover must be present in the document to function properly since this
|
||||||
/// catches all cover types collectively
|
/// catches all cover types collectively
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/banner.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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'util.dart';
|
import 'util.dart';
|
||||||
@ -89,18 +90,6 @@ extension Expectation on WidgetTester {
|
|||||||
expect(exportSuccess, findsOneWidget);
|
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
|
/// Expect to see the document header toolbar empty
|
||||||
void expectToSeeEmptyDocumentHeaderToolbar() {
|
void expectToSeeEmptyDocumentHeaderToolbar() {
|
||||||
final addCover = find.textContaining(
|
final addCover = find.textContaining(
|
||||||
@ -153,14 +142,6 @@ extension Expectation on WidgetTester {
|
|||||||
expect(findRemoveIcon, findsOneWidget);
|
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
|
/// Expect to see a text
|
||||||
void expectToSeeText(String text) {
|
void expectToSeeText(String text) {
|
||||||
Finder textWidget = find.textContaining(text, findRichText: true);
|
Finder textWidget = find.textContaining(text, findRichText: true);
|
||||||
@ -178,26 +159,23 @@ extension Expectation on WidgetTester {
|
|||||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||||
String? parentName,
|
String? parentName,
|
||||||
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
|
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
|
||||||
}) {
|
}) =>
|
||||||
return find.byWidgetPredicate(
|
find.byWidgetPredicate(
|
||||||
(widget) =>
|
(widget) =>
|
||||||
widget is SingleInnerViewItem &&
|
widget is SingleInnerViewItem &&
|
||||||
widget.view.isFavorite &&
|
widget.view.isFavorite &&
|
||||||
widget.categoryType == FolderCategoryType.favorite &&
|
widget.categoryType == FolderCategoryType.favorite &&
|
||||||
widget.view.name == name &&
|
widget.view.name == name &&
|
||||||
widget.view.layout == layout,
|
widget.view.layout == layout,
|
||||||
skipOffstage: false,
|
skipOffstage: false,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Finder findAllFavoritePages() {
|
Finder findAllFavoritePages() => find.byWidgetPredicate(
|
||||||
return find.byWidgetPredicate(
|
(widget) =>
|
||||||
(widget) =>
|
widget is SingleInnerViewItem &&
|
||||||
widget is SingleInnerViewItem &&
|
widget.view.isFavorite &&
|
||||||
widget.view.isFavorite &&
|
widget.categoryType == FolderCategoryType.favorite,
|
||||||
widget.categoryType == FolderCategoryType.favorite,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Finder findPageName(
|
Finder findPageName(
|
||||||
String name, {
|
String name, {
|
||||||
|
@ -11,9 +11,7 @@ class MockFilePicker implements FilePickerService {
|
|||||||
final List<String> mockPaths;
|
final List<String> mockPaths;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> getDirectoryPath({String? title}) {
|
Future<String?> getDirectoryPath({String? title}) => Future.value(mockPath);
|
||||||
return Future.value(mockPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> saveFile({
|
Future<String?> saveFile({
|
||||||
@ -23,9 +21,8 @@ class MockFilePicker implements FilePickerService {
|
|||||||
FileType type = FileType.any,
|
FileType type = FileType.any,
|
||||||
List<String>? allowedExtensions,
|
List<String>? allowedExtensions,
|
||||||
bool lockParentWindow = false,
|
bool lockParentWindow = false,
|
||||||
}) {
|
}) =>
|
||||||
return Future.value(mockPath);
|
Future.value(mockPath);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FilePickerResult?> pickFiles({
|
Future<FilePickerResult?> pickFiles({
|
||||||
@ -42,34 +39,21 @@ class MockFilePicker implements FilePickerService {
|
|||||||
}) {
|
}) {
|
||||||
final platformFiles =
|
final platformFiles =
|
||||||
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
|
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
|
||||||
return Future.value(
|
return Future.value(FilePickerResult(platformFiles));
|
||||||
FilePickerResult(
|
|
||||||
platformFiles,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> mockGetDirectoryPath(
|
Future<void> mockGetDirectoryPath(String path) async {
|
||||||
String path,
|
|
||||||
) async {
|
|
||||||
getIt.unregister<FilePickerService>();
|
getIt.unregister<FilePickerService>();
|
||||||
getIt.registerFactory<FilePickerService>(
|
getIt.registerFactory<FilePickerService>(
|
||||||
() => MockFilePicker(
|
() => MockFilePicker(mockPath: path),
|
||||||
mockPath: path,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> mockSaveFilePath(
|
Future<String> mockSaveFilePath(String path) async {
|
||||||
String path,
|
|
||||||
) async {
|
|
||||||
getIt.unregister<FilePickerService>();
|
getIt.unregister<FilePickerService>();
|
||||||
getIt.registerFactory<FilePickerService>(
|
getIt.registerFactory<FilePickerService>(
|
||||||
() => MockFilePicker(
|
() => MockFilePicker(mockPath: path),
|
||||||
mockPath: path,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@ -77,9 +61,7 @@ Future<String> mockSaveFilePath(
|
|||||||
List<String> mockPickFilePaths({required List<String> paths}) {
|
List<String> mockPickFilePaths({required List<String> paths}) {
|
||||||
getIt.unregister<FilePickerService>();
|
getIt.unregister<FilePickerService>();
|
||||||
getIt.registerFactory<FilePickerService>(
|
getIt.registerFactory<FilePickerService>(
|
||||||
() => MockFilePicker(
|
() => MockFilePicker(mockPaths: paths),
|
||||||
mockPaths: paths,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
@ -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: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 {
|
class MyMockClient extends Mock implements http.Client {
|
||||||
@override
|
@override
|
||||||
@ -52,7 +53,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
|
|||||||
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
|
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
|
||||||
final response = await client.send(request);
|
final response = await client.send(request);
|
||||||
|
|
||||||
var previousSyntax = '';
|
String previousSyntax = '';
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
await for (final chunk in response.stream
|
await for (final chunk in response.stream
|
||||||
.transform(const Utf8Decoder())
|
.transform(const Utf8Decoder())
|
||||||
@ -76,6 +77,5 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,9 +27,7 @@ class MockUrlLauncher extends Fake
|
|||||||
bool launchCalled = false;
|
bool launchCalled = false;
|
||||||
|
|
||||||
// ignore: use_setters_to_change_properties
|
// ignore: use_setters_to_change_properties
|
||||||
void setCanLaunchExpectations(String url) {
|
void setCanLaunchExpectations(String url) => this.url = url;
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setLaunchExpectations({
|
void setLaunchExpectations({
|
||||||
required String url,
|
required String url,
|
||||||
@ -53,10 +51,7 @@ class MockUrlLauncher extends Fake
|
|||||||
this.webOnlyWindowName = webOnlyWindowName;
|
this.webOnlyWindowName = webOnlyWindowName;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore: use_setters_to_change_properties
|
void setResponse(bool response) => this.response = response;
|
||||||
void setResponse(bool response) {
|
|
||||||
this.response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
LinkDelegate? get linkDelegate => null;
|
LinkDelegate? get linkDelegate => null;
|
||||||
@ -104,7 +99,5 @@ class MockUrlLauncher extends Fake
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> closeWebView() async {
|
Future<void> closeWebView() async => closeWebViewCalled = true;
|
||||||
closeWebViewCalled = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.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/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/settings_dialog.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.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_menu_element.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.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 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../desktop/board/board_hide_groups_test.dart';
|
||||||
|
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
|
||||||
extension AppFlowySettings on WidgetTester {
|
extension AppFlowySettings on WidgetTester {
|
||||||
@ -31,14 +35,6 @@ extension AppFlowySettings on WidgetTester {
|
|||||||
return;
|
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
|
/// Restore the AppFlowy data storage location
|
||||||
Future<void> restoreLocation() async {
|
Future<void> restoreLocation() async {
|
||||||
final button =
|
final button =
|
||||||
@ -48,13 +44,6 @@ extension AppFlowySettings on WidgetTester {
|
|||||||
return;
|
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 {
|
Future<void> tapCustomLocationButton() async {
|
||||||
final button = find.byTooltip(
|
final button = find.byTooltip(
|
||||||
LocaleKeys.settings_files_changeLocationTooltips.tr(),
|
LocaleKeys.settings_files_changeLocationTooltips.tr(),
|
||||||
@ -66,12 +55,22 @@ extension AppFlowySettings on WidgetTester {
|
|||||||
|
|
||||||
/// Enter user name
|
/// Enter user name
|
||||||
Future<void> enterUserName(String name) async {
|
Future<void> enterUserName(String name) async {
|
||||||
final uni = find.byType(UserNameInput);
|
// Enable editing username
|
||||||
expect(uni, findsOneWidget);
|
final editUsernameFinder = find.descendant(
|
||||||
await tap(uni);
|
of: find.byType(UserProfileSetting),
|
||||||
await enterText(uni, name);
|
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||||
await wait(300); //
|
);
|
||||||
await testTextInput.receiveAction(TextInputAction.done);
|
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();
|
await pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,10 @@
|
|||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:appflowy/core/notification/notification_helper.dart';
|
import 'package:appflowy/core/notification/notification_helper.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.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
|
// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value
|
||||||
const String _source = 'Document';
|
const String _source = 'Document';
|
||||||
|
|
||||||
typedef DocumentNotificationCallback = void Function(
|
|
||||||
DocumentNotification,
|
|
||||||
FlowyResult<Uint8List, FlowyError>,
|
|
||||||
);
|
|
||||||
|
|
||||||
class DocumentNotificationParser
|
class DocumentNotificationParser
|
||||||
extends NotificationParser<DocumentNotification, FlowyError> {
|
extends NotificationParser<DocumentNotification, FlowyError> {
|
||||||
DocumentNotificationParser({
|
DocumentNotificationParser({
|
||||||
|
@ -12,12 +12,6 @@ import 'notification_helper.dart';
|
|||||||
// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value
|
// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value
|
||||||
const String _source = 'Workspace';
|
const String _source = 'Workspace';
|
||||||
|
|
||||||
// Folder
|
|
||||||
typedef FolderNotificationCallback = void Function(
|
|
||||||
FolderNotification,
|
|
||||||
FlowyResult<Uint8List, FlowyError>,
|
|
||||||
);
|
|
||||||
|
|
||||||
class FolderNotificationParser
|
class FolderNotificationParser
|
||||||
extends NotificationParser<FolderNotification, FlowyError> {
|
extends NotificationParser<FolderNotification, FlowyError> {
|
||||||
FolderNotificationParser({
|
FolderNotificationParser({
|
||||||
|
@ -12,12 +12,6 @@ import 'notification_helper.dart';
|
|||||||
// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value
|
// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value
|
||||||
const String _source = 'Database';
|
const String _source = 'Database';
|
||||||
|
|
||||||
// DatabasePB
|
|
||||||
typedef DatabaseNotificationCallback = void Function(
|
|
||||||
DatabaseNotification,
|
|
||||||
FlowyResult<Uint8List, FlowyError>,
|
|
||||||
);
|
|
||||||
|
|
||||||
class DatabaseNotificationParser
|
class DatabaseNotificationParser
|
||||||
extends NotificationParser<DatabaseNotification, FlowyError> {
|
extends NotificationParser<DatabaseNotification, FlowyError> {
|
||||||
DatabaseNotificationParser({
|
DatabaseNotificationParser({
|
||||||
|
@ -13,11 +13,6 @@ import 'notification_helper.dart';
|
|||||||
// This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE)
|
// This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE)
|
||||||
const _source = 'Search';
|
const _source = 'Search';
|
||||||
|
|
||||||
typedef SearchNotificationCallback = void Function(
|
|
||||||
SearchNotification,
|
|
||||||
FlowyResult<Uint8List, FlowyError>,
|
|
||||||
);
|
|
||||||
|
|
||||||
class SearchNotificationParser
|
class SearchNotificationParser
|
||||||
extends NotificationParser<SearchNotification, FlowyError> {
|
extends NotificationParser<SearchNotification, FlowyError> {
|
||||||
SearchNotificationParser({
|
SearchNotificationParser({
|
||||||
|
@ -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-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/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:appflowy_backend/rust_stream.dart';
|
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
|
||||||
|
|
||||||
import 'notification_helper.dart';
|
import 'notification_helper.dart';
|
||||||
|
|
||||||
// This value should be the same as the USER_OBSERVABLE_SOURCE value
|
// This value should be the same as the USER_OBSERVABLE_SOURCE value
|
||||||
const String _source = 'User';
|
const String _source = 'User';
|
||||||
|
|
||||||
// User
|
|
||||||
typedef UserNotificationCallback = void Function(
|
|
||||||
UserNotification,
|
|
||||||
FlowyResult<Uint8List, FlowyError>,
|
|
||||||
);
|
|
||||||
|
|
||||||
class UserNotificationParser
|
class UserNotificationParser
|
||||||
extends NotificationParser<UserNotification, FlowyError> {
|
extends NotificationParser<UserNotification, FlowyError> {
|
||||||
UserNotificationParser({
|
UserNotificationParser({
|
||||||
@ -29,26 +17,3 @@ class UserNotificationParser
|
|||||||
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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.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/emoji/emoji_picker_screen.dart';
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/startup/startup.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/settings_user_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_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/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:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
}
|
|
@ -6,11 +6,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
|||||||
|
|
||||||
import 'cell_controller.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> {
|
abstract class CellDataParser<T> {
|
||||||
T? parserData(List<int> data);
|
T? parserData(List<int> data);
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,6 @@ typedef OnNumOfRowsChanged = void Function(
|
|||||||
ChangedReason reason,
|
ChangedReason reason,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef OnError = void Function(FlowyError);
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class LoadingState with _$LoadingState {
|
class LoadingState with _$LoadingState {
|
||||||
const factory LoadingState.idle() = _Idle;
|
const factory LoadingState.idle() = _Idle;
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import 'dart:collection';
|
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/database_view_service.dart';
|
||||||
import 'package:appflowy/plugins/database/domain/field_listener.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_listener.dart';
|
||||||
import 'package:appflowy/plugins/database/domain/field_settings_service.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_listener.dart';
|
||||||
import 'package:appflowy/plugins/database/domain/filter_service.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_listener.dart';
|
||||||
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.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_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
|
|
||||||
import '../setting/setting_service.dart';
|
import '../setting/setting_service.dart';
|
||||||
|
|
||||||
import 'field_info.dart';
|
import 'field_info.dart';
|
||||||
|
|
||||||
class _GridFieldNotifier extends ChangeNotifier {
|
class _GridFieldNotifier extends ChangeNotifier {
|
||||||
@ -73,7 +75,7 @@ typedef OnReceiveField = void Function(FieldInfo);
|
|||||||
typedef OnReceiveFields = void Function(List<FieldInfo>);
|
typedef OnReceiveFields = void Function(List<FieldInfo>);
|
||||||
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
||||||
typedef OnReceiveSorts = void Function(List<SortInfo>);
|
typedef OnReceiveSorts = void Function(List<SortInfo>);
|
||||||
typedef OnReceiveFieldSettings = void Function(List<FieldInfo>);
|
|
||||||
|
|
||||||
class FieldController {
|
class FieldController {
|
||||||
FieldController({required this.viewId})
|
FieldController({required this.viewId})
|
||||||
|
@ -4,13 +4,7 @@ abstract class TypeOptionParser<T> {
|
|||||||
T fromBuffer(List<int> buffer);
|
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> {
|
class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
|
||||||
@override
|
@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> {
|
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||||
@override
|
@override
|
||||||
DateTypeOptionPB fromBuffer(List<int> buffer) {
|
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
|
class RelationTypeOptionDataParser
|
||||||
extends TypeOptionParser<RelationTypeOptionPB> {
|
extends TypeOptionParser<RelationTypeOptionPB> {
|
||||||
@override
|
@override
|
||||||
|
@ -9,8 +9,6 @@ import 'package:appflowy_result/appflowy_result.dart';
|
|||||||
import 'package:flowy_infra/notifier.dart';
|
import 'package:flowy_infra/notifier.dart';
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
typedef OnGroupError = void Function(FlowyError);
|
|
||||||
|
|
||||||
abstract class GroupControllerDelegate {
|
abstract class GroupControllerDelegate {
|
||||||
bool hasGroup(String groupId);
|
bool hasGroup(String groupId);
|
||||||
void removeRow(GroupPB group, RowId rowId);
|
void removeRow(GroupPB group, RowId rowId);
|
||||||
|
@ -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,
|
|
||||||
}
|
|
@ -456,16 +456,6 @@ class CalendarState with _$CalendarState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class CalendarEditingRow {
|
|
||||||
CalendarEditingRow({
|
|
||||||
required this.row,
|
|
||||||
required this.index,
|
|
||||||
});
|
|
||||||
|
|
||||||
RowPB row;
|
|
||||||
int? index;
|
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class CalendarDayEvent with _$CalendarDayEvent {
|
class CalendarDayEvent with _$CalendarDayEvent {
|
||||||
const factory CalendarDayEvent({
|
const factory CalendarDayEvent({
|
||||||
|
@ -8,8 +8,6 @@ import 'package:protobuf/protobuf.dart';
|
|||||||
|
|
||||||
part 'calendar_setting_bloc.freezed.dart';
|
part 'calendar_setting_bloc.freezed.dart';
|
||||||
|
|
||||||
typedef DayOfWeek = int;
|
|
||||||
|
|
||||||
class CalendarSettingBloc
|
class CalendarSettingBloc
|
||||||
extends Bloc<CalendarSettingEvent, CalendarSettingState> {
|
extends Bloc<CalendarSettingEvent, CalendarSettingState> {
|
||||||
CalendarSettingBloc({required DatabaseController databaseController})
|
CalendarSettingBloc({required DatabaseController databaseController})
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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.dart';
|
||||||
@ -19,12 +21,12 @@ import 'package:flowy_infra/size.dart';
|
|||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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/flowy_tooltip.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../application/row/row_controller.dart';
|
import '../../application/row/row_controller.dart';
|
||||||
import '../../widgets/row/row_detail.dart';
|
import '../../widgets/row/row_detail.dart';
|
||||||
|
|
||||||
import 'calendar_day.dart';
|
import 'calendar_day.dart';
|
||||||
import 'layout/sizes.dart';
|
import 'layout/sizes.dart';
|
||||||
import 'toolbar/calendar_setting_bar.dart';
|
import 'toolbar/calendar_setting_bar.dart';
|
||||||
@ -265,6 +267,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
fillColor: Colors.transparent,
|
fillColor: Colors.transparent,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
|
fontColor: AFThemeExtension.of(context).textColor,
|
||||||
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
|
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
|
@ -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),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,8 +8,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
|||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:flowy_infra/notifier.dart';
|
import 'package:flowy_infra/notifier.dart';
|
||||||
|
|
||||||
typedef GroupConfigurationUpdateValue
|
|
||||||
= FlowyResult<List<GroupSettingPB>, FlowyError>;
|
|
||||||
typedef GroupUpdateValue = FlowyResult<GroupChangesPB, FlowyError>;
|
typedef GroupUpdateValue = FlowyResult<GroupChangesPB, FlowyError>;
|
||||||
typedef GroupByNewFieldValue = FlowyResult<List<GroupPB>, FlowyError>;
|
typedef GroupByNewFieldValue = FlowyResult<List<GroupPB>, FlowyError>;
|
||||||
|
|
||||||
|
@ -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/cell/cell_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/field/field_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/grid/application/row/row_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.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: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 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../layout/sizes.dart';
|
import '../../layout/sizes.dart';
|
||||||
import "package:appflowy/generated/locale_keys.g.dart";
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
|
|
||||||
class MobileGridRow extends StatefulWidget {
|
class MobileGridRow extends StatefulWidget {
|
||||||
const MobileGridRow({
|
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 {
|
class RowContent extends StatelessWidget {
|
||||||
const RowContent({
|
const RowContent({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -23,27 +23,12 @@ Map<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) {
|
|||||||
return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};
|
return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Type, Action<Intent>> bindActions() {
|
|
||||||
return {
|
|
||||||
KeyboardKeyIdent: KeyboardBindingAction(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class KeyboardKeyIdent extends Intent {
|
class KeyboardKeyIdent extends Intent {
|
||||||
const KeyboardKeyIdent(this.key);
|
const KeyboardKeyIdent(this.key);
|
||||||
|
|
||||||
final KeyboardKey key;
|
final KeyboardKey key;
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyboardBindingAction extends Action<KeyboardKeyIdent> {
|
|
||||||
KeyboardBindingAction();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void invoke(covariant KeyboardKeyIdent intent) {
|
|
||||||
// print(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LoggingActionDispatcher extends ActionDispatcher {
|
class LoggingActionDispatcher extends ActionDispatcher {
|
||||||
@override
|
@override
|
||||||
Object? invokeAction(
|
Object? invokeAction(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
|
|
||||||
enum AccessoryType {
|
enum AccessoryType {
|
||||||
edit,
|
edit,
|
||||||
more,
|
more,
|
||||||
@ -11,10 +12,6 @@ abstract mixin class CardAccessory implements Widget {
|
|||||||
void onTap(BuildContext context) {}
|
void onTap(BuildContext context) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef CardAccessoryBuilder = List<CardAccessory> Function(
|
|
||||||
BuildContext buildContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
class CardAccessoryContainer extends StatelessWidget {
|
class CardAccessoryContainer extends StatelessWidget {
|
||||||
const CardAccessoryContainer({
|
const CardAccessoryContainer({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/cell/cell_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/row/row_banner_bloc.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_builder.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.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/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/widgets/row/row_action.dart';
|
||||||
import 'package:appflowy/plugins/database_document/database_document_plugin.dart';
|
import 'package:appflowy/plugins/database_document/database_document_plugin.dart';
|
||||||
import 'package:appflowy/startup/plugin/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:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
typedef OnSubmittedEmoji = void Function(String emoji);
|
|
||||||
const _kBannerActionHeight = 40.0;
|
const _kBannerActionHeight = 40.0;
|
||||||
|
|
||||||
class RowBanner extends StatefulWidget {
|
class RowBanner extends StatefulWidget {
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +1,8 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:ui';
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
const String kLocalImagesKey = 'local_images';
|
const String kLocalImagesKey = 'local_images';
|
||||||
|
|
||||||
@ -22,302 +11,6 @@ List<String> get builtInAssetImages => [
|
|||||||
"assets/images/app_flowy_abstract_cover_2.jpg",
|
"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 {
|
class ColorOption {
|
||||||
const ColorOption({
|
const ColorOption({
|
||||||
required this.colorHex,
|
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
|
@visibleForTesting
|
||||||
class ColorItem extends StatelessWidget {
|
class ColorItem extends StatelessWidget {
|
||||||
const ColorItem({
|
const ColorItem({
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,271 +1,9 @@
|
|||||||
import 'package:flutter/material.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/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/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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 {
|
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(
|
Future<void> insertMathEquation(
|
||||||
Selection selection,
|
Selection selection,
|
||||||
) async {
|
) async {
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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);
|
|
||||||
},
|
|
||||||
);
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
},
|
|
||||||
);
|
|
@ -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();
|
|
||||||
},
|
|
||||||
);
|
|
@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -79,15 +79,10 @@ void _resolveCommonService(
|
|||||||
IntegrationMode mode,
|
IntegrationMode mode,
|
||||||
) async {
|
) async {
|
||||||
getIt.registerFactory<FilePickerService>(() => FilePicker());
|
getIt.registerFactory<FilePickerService>(() => FilePicker());
|
||||||
if (mode.isTest) {
|
|
||||||
getIt.registerFactory<ApplicationDataStorage>(
|
getIt.registerFactory<ApplicationDataStorage>(
|
||||||
() => MockApplicationDataStorage(),
|
() => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
getIt.registerFactory<ApplicationDataStorage>(
|
|
||||||
() => ApplicationDataStorage(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIt.registerFactoryAsync<OpenAIRepository>(
|
getIt.registerFactoryAsync<OpenAIRepository>(
|
||||||
() async {
|
() async {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:appflowy/core/notification/folder_notification.dart';
|
import 'package:appflowy/core/notification/folder_notification.dart';
|
||||||
import 'package:appflowy/core/notification/user_notification.dart';
|
import 'package:appflowy/core/notification/user_notification.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
@ -18,7 +20,6 @@ typedef DidUserWorkspaceUpdateCallback = void Function(
|
|||||||
RepeatedUserWorkspacePB workspaces,
|
RepeatedUserWorkspacePB workspaces,
|
||||||
);
|
);
|
||||||
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
||||||
typedef AuthNotifyValue = FlowyResult<void, FlowyError>;
|
|
||||||
|
|
||||||
class UserListener {
|
class UserListener {
|
||||||
UserListener({
|
UserListener({
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
extension Base64Encode on String {
|
|
||||||
String get base64 => base64Encode(utf8.encode(this));
|
|
||||||
}
|
|
13
frontend/appflowy_flutter/lib/util/built_in_svgs.dart
Normal file
13
frontend/appflowy_flutter/lib/util/built_in_svgs.dart
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
final builtInSVGIcons = [
|
||||||
|
'1F9CC',
|
||||||
|
'1F9DB',
|
||||||
|
'1F9DD-200D-2642-FE0F',
|
||||||
|
'1F9DE-200D-2642-FE0F',
|
||||||
|
'1F9DF',
|
||||||
|
'1F42F',
|
||||||
|
'1F43A',
|
||||||
|
'1F431',
|
||||||
|
'1F435',
|
||||||
|
'1F600',
|
||||||
|
'1F984',
|
||||||
|
];
|
@ -4,7 +4,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
|
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
|
||||||
show WorkspaceSettingPB;
|
show WorkspaceSettingPB;
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flowy_infra/time/duration.dart';
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
part 'home_bloc.freezed.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
|
@freezed
|
||||||
class HomeEvent with _$HomeEvent {
|
class HomeEvent with _$HomeEvent {
|
||||||
const factory HomeEvent.initial() = _Initial;
|
const factory HomeEvent.initial() = _Initial;
|
||||||
|
@ -39,9 +39,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
|||||||
_userListener.start(onProfileUpdated: _profileUpdated);
|
_userListener.start(onProfileUpdated: _profileUpdated);
|
||||||
await _initUser();
|
await _initUser();
|
||||||
},
|
},
|
||||||
fetchWorkspaces: () async {
|
|
||||||
//
|
|
||||||
},
|
|
||||||
didReceiveUserProfile: (UserProfilePB newUserProfile) {
|
didReceiveUserProfile: (UserProfilePB newUserProfile) {
|
||||||
emit(state.copyWith(userProfile: newUserProfile));
|
emit(state.copyWith(userProfile: newUserProfile));
|
||||||
},
|
},
|
||||||
@ -70,9 +67,7 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
userProfileOrFailed.fold(
|
userProfileOrFailed.fold(
|
||||||
(newUserProfile) => add(
|
(profile) => add(MenuUserEvent.didReceiveUserProfile(profile)),
|
||||||
MenuUserEvent.didReceiveUserProfile(newUserProfile),
|
|
||||||
),
|
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -81,7 +76,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
|||||||
@freezed
|
@freezed
|
||||||
class MenuUserEvent with _$MenuUserEvent {
|
class MenuUserEvent with _$MenuUserEvent {
|
||||||
const factory MenuUserEvent.initial() = _Initial;
|
const factory MenuUserEvent.initial() = _Initial;
|
||||||
const factory MenuUserEvent.fetchWorkspaces() = _FetchWorkspaces;
|
|
||||||
const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName;
|
const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName;
|
||||||
const factory MenuUserEvent.didReceiveUserProfile(
|
const factory MenuUserEvent.didReceiveUserProfile(
|
||||||
UserProfilePB newUserProfile,
|
UserProfilePB newUserProfile,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme.dart';
|
import 'package:flowy_infra/theme.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class DesktopAppearance extends BaseAppearance {
|
class DesktopAppearance extends BaseAppearance {
|
||||||
@override
|
@override
|
||||||
@ -119,6 +120,7 @@ class DesktopAppearance extends BaseAppearance {
|
|||||||
tint8: theme.tint8,
|
tint8: theme.tint8,
|
||||||
tint9: theme.tint9,
|
tint9: theme.tint9,
|
||||||
textColor: theme.text,
|
textColor: theme.text,
|
||||||
|
secondaryTextColor: theme.secondaryText,
|
||||||
greyHover: theme.hoverBG1,
|
greyHover: theme.hoverBG1,
|
||||||
greySelect: theme.bg3,
|
greySelect: theme.bg3,
|
||||||
lightGreyHover: theme.hoverBG3,
|
lightGreyHover: theme.hoverBG3,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// ThemeData in mobile
|
// ThemeData in mobile
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
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:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme.dart';
|
import 'package:flowy_infra/theme.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class MobileAppearance extends BaseAppearance {
|
class MobileAppearance extends BaseAppearance {
|
||||||
static const _primaryColor = Color(0xFF00BCF0); //primary 100
|
static const _primaryColor = Color(0xFF00BCF0); //primary 100
|
||||||
@ -28,9 +29,7 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
);
|
);
|
||||||
|
|
||||||
final codeFontStyle = getFontStyle(
|
final codeFontStyle = getFontStyle(fontFamily: codeFontFamily);
|
||||||
fontFamily: codeFontFamily,
|
|
||||||
);
|
|
||||||
|
|
||||||
final theme = brightness == Brightness.light
|
final theme = brightness == Brightness.light
|
||||||
? appTheme.lightTheme
|
? appTheme.lightTheme
|
||||||
@ -81,9 +80,7 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
: _hintColorInDarkMode;
|
: _hintColorInDarkMode;
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
// color
|
|
||||||
useMaterial3: false,
|
useMaterial3: false,
|
||||||
|
|
||||||
primaryColor: colorTheme.primary, //primary 100
|
primaryColor: colorTheme.primary, //primary 100
|
||||||
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
||||||
dividerColor: colorTheme.outline, //caption
|
dividerColor: colorTheme.outline, //caption
|
||||||
@ -124,6 +121,7 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
shadowColor: MaterialStateProperty.all(null),
|
shadowColor: MaterialStateProperty.all(null),
|
||||||
|
foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||||
(Set<MaterialState> states) {
|
(Set<MaterialState> states) {
|
||||||
if (states.contains(MaterialState.disabled)) {
|
if (states.contains(MaterialState.disabled)) {
|
||||||
@ -132,7 +130,6 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
return colorTheme.primary;
|
return colorTheme.primary;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
foregroundColor: MaterialStateProperty.all(Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
@ -144,20 +141,13 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
foregroundColor: MaterialStateProperty.all(
|
foregroundColor: MaterialStateProperty.all(colorTheme.onBackground),
|
||||||
colorTheme.onBackground,
|
|
||||||
),
|
|
||||||
backgroundColor: MaterialStateProperty.all(colorTheme.background),
|
backgroundColor: MaterialStateProperty.all(colorTheme.background),
|
||||||
shape: MaterialStateProperty.all(
|
shape: MaterialStateProperty.all(
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
side: MaterialStateProperty.all(
|
side: MaterialStateProperty.all(
|
||||||
BorderSide(
|
BorderSide(color: colorTheme.outline, width: 0.5),
|
||||||
color: colorTheme.outline,
|
|
||||||
width: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
padding: MaterialStateProperty.all(
|
padding: MaterialStateProperty.all(
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
@ -166,9 +156,7 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
),
|
),
|
||||||
textButtonTheme: TextButtonThemeData(
|
textButtonTheme: TextButtonThemeData(
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
textStyle: MaterialStateProperty.all(
|
textStyle: MaterialStateProperty.all(fontStyle),
|
||||||
fontStyle,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// text
|
// text
|
||||||
@ -262,6 +250,7 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
tint8: theme.tint8,
|
tint8: theme.tint8,
|
||||||
tint9: theme.tint9,
|
tint9: theme.tint9,
|
||||||
textColor: theme.text,
|
textColor: theme.text,
|
||||||
|
secondaryTextColor: theme.secondaryText,
|
||||||
greyHover: theme.hoverBG1,
|
greyHover: theme.hoverBG1,
|
||||||
greySelect: theme.bg3,
|
greySelect: theme.bg3,
|
||||||
lightGreyHover: theme.hoverBG3,
|
lightGreyHover: theme.hoverBG3,
|
||||||
|
@ -9,10 +9,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
part 'settings_dialog_bloc.freezed.dart';
|
part 'settings_dialog_bloc.freezed.dart';
|
||||||
|
|
||||||
enum SettingsPage {
|
enum SettingsPage {
|
||||||
|
// NEW
|
||||||
|
account,
|
||||||
|
// OLD
|
||||||
appearance,
|
appearance,
|
||||||
language,
|
language,
|
||||||
files,
|
files,
|
||||||
user,
|
// user,
|
||||||
notifications,
|
notifications,
|
||||||
cloud,
|
cloud,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
@ -88,6 +91,6 @@ class SettingsDialogState with _$SettingsDialogState {
|
|||||||
SettingsDialogState(
|
SettingsDialogState(
|
||||||
userProfile: userProfile,
|
userProfile: userProfile,
|
||||||
successOrFailure: FlowyResult.success(null),
|
successOrFailure: FlowyResult.success(null),
|
||||||
page: SettingsPage.appearance,
|
page: SettingsPage.account,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -111,14 +111,12 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
|||||||
|
|
||||||
void _profileUpdated(
|
void _profileUpdated(
|
||||||
FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,
|
FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,
|
||||||
) {
|
) =>
|
||||||
userProfileOrFailed.fold(
|
userProfileOrFailed.fold(
|
||||||
(newUserProfile) {
|
(newUserProfile) =>
|
||||||
add(SettingsUserEvent.didReceiveUserProfile(newUserProfile));
|
add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)),
|
||||||
},
|
(err) => Log.error(err),
|
||||||
(err) => Log.error(err),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -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: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 {
|
class FolderHeader extends StatefulWidget {
|
||||||
const FolderHeader({
|
const FolderHeader({
|
||||||
super.key,
|
super.key,
|
||||||
@ -40,6 +42,7 @@ class _FolderHeaderState extends State<FolderHeader> {
|
|||||||
constraints: const BoxConstraints(
|
constraints: const BoxConstraints(
|
||||||
minHeight: iconSize + textPadding * 2,
|
minHeight: iconSize + textPadding * 2,
|
||||||
),
|
),
|
||||||
|
fontColor: AFThemeExtension.of(context).textColor,
|
||||||
padding: const EdgeInsets.all(textPadding),
|
padding: const EdgeInsets.all(textPadding),
|
||||||
fillColor: Colors.transparent,
|
fillColor: Colors.transparent,
|
||||||
onPressed: widget.onPressed,
|
onPressed: widget.onPressed,
|
||||||
|
@ -62,14 +62,10 @@ class UserSettingButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showSettingsDialog(
|
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) =>
|
||||||
BuildContext context,
|
showDialog(
|
||||||
UserProfilePB userProfile,
|
context: context,
|
||||||
) {
|
builder: (dialogContext) => BlocProvider<DocumentAppearanceCubit>.value(
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (dialogContext) {
|
|
||||||
return BlocProvider<DocumentAppearanceCubit>.value(
|
|
||||||
key: _settingsDialogKey,
|
key: _settingsDialogKey,
|
||||||
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
||||||
child: SettingsDialog(
|
child: SettingsDialog(
|
||||||
@ -81,10 +77,9 @@ void showSettingsDialog(
|
|||||||
},
|
},
|
||||||
dismissDialog: () {
|
dismissDialog: () {
|
||||||
if (Navigator.of(dialogContext).canPop()) {
|
if (Navigator.of(dialogContext).canPop()) {
|
||||||
Navigator.of(dialogContext).pop();
|
return Navigator.of(dialogContext).pop();
|
||||||
} else {
|
|
||||||
Log.warn("Can't pop dialog context");
|
|
||||||
}
|
}
|
||||||
|
Log.warn("Can't pop dialog context");
|
||||||
},
|
},
|
||||||
restartApp: () async {
|
restartApp: () async {
|
||||||
// Pop the dialog using the dialog context
|
// Pop the dialog using the dialog context
|
||||||
@ -92,7 +87,5 @@ void showSettingsDialog(
|
|||||||
await runAppFlowy();
|
await runAppFlowy();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
// keep this widget in case we need to roll back (lucas.xu)
|
// keep this widget in case we need to roll back (lucas.xu)
|
||||||
@ -23,10 +24,8 @@ class SidebarUser extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<MenuUserBloc>(
|
return BlocProvider<MenuUserBloc>(
|
||||||
create: (context) => MenuUserBloc(userProfile)
|
create: (_) =>
|
||||||
..add(
|
MenuUserBloc(userProfile)..add(const MenuUserEvent.initial()),
|
||||||
const MenuUserEvent.initial(),
|
|
||||||
),
|
|
||||||
child: BlocBuilder<MenuUserBloc, MenuUserState>(
|
child: BlocBuilder<MenuUserBloc, MenuUserState>(
|
||||||
builder: (context, state) => Row(
|
builder: (context, state) => Row(
|
||||||
children: [
|
children: [
|
||||||
@ -35,9 +34,7 @@ class SidebarUser extends StatelessWidget {
|
|||||||
name: state.userProfile.name,
|
name: state.userProfile.name,
|
||||||
),
|
),
|
||||||
const HSpace(8),
|
const HSpace(8),
|
||||||
Expanded(
|
Expanded(child: _buildUserName(context, state)),
|
||||||
child: _buildUserName(context, state),
|
|
||||||
),
|
|
||||||
UserSettingButton(userProfile: state.userProfile),
|
UserSettingButton(userProfile: state.userProfile),
|
||||||
const HSpace(4),
|
const HSpace(4),
|
||||||
const NotificationButton(),
|
const NotificationButton(),
|
||||||
|
@ -15,8 +15,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
typedef NaviAction = void Function();
|
|
||||||
|
|
||||||
class NavigationNotifier with ChangeNotifier {
|
class NavigationNotifier with ChangeNotifier {
|
||||||
NavigationNotifier({required this.navigationItems});
|
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 {
|
class EllipsisNaviItem extends NavigationItem {
|
||||||
EllipsisNaviItem({required this.items});
|
EllipsisNaviItem({required this.items});
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.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/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/members/workspace_member_page.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.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_language_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.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_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: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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'widgets/setting_cloud.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 {
|
class SettingsDialog extends StatelessWidget {
|
||||||
SettingsDialog(
|
SettingsDialog(
|
||||||
this.user, {
|
this.user, {
|
||||||
@ -41,49 +37,31 @@ class SettingsDialog extends StatelessWidget {
|
|||||||
..add(const SettingsDialogEvent.initial()),
|
..add(const SettingsDialogEvent.initial()),
|
||||||
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
|
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
|
||||||
builder: (context, state) => FlowyDialog(
|
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,
|
width: MediaQuery.of(context).size.width * 0.7,
|
||||||
child: ScaffoldMessenger(
|
child: ScaffoldMessenger(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
body: Padding(
|
body: Row(
|
||||||
padding: _dialogHorizontalPadding,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Row(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
SizedBox(
|
||||||
children: [
|
width: 200,
|
||||||
SizedBox(
|
child: SettingsMenu(
|
||||||
width: 200,
|
userProfile: user,
|
||||||
child: SettingsMenu(
|
changeSelectedPage: (index) => context
|
||||||
userProfile: user,
|
.read<SettingsDialogBloc>()
|
||||||
changeSelectedPage: (index) {
|
.add(SettingsDialogEvent.setSelectedPage(index)),
|
||||||
context
|
currentPage:
|
||||||
.read<SettingsDialogBloc>()
|
context.read<SettingsDialogBloc>().state.page,
|
||||||
.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) {
|
Widget getSettingsView(SettingsPage page, UserProfilePB user) {
|
||||||
switch (page) {
|
switch (page) {
|
||||||
|
case SettingsPage.account:
|
||||||
|
return SettingsAccountView(
|
||||||
|
userProfile: user,
|
||||||
|
didLogout: didLogout,
|
||||||
|
didLogin: dismissDialog,
|
||||||
|
);
|
||||||
case SettingsPage.appearance:
|
case SettingsPage.appearance:
|
||||||
return const SettingsAppearanceView();
|
return const SettingsAppearanceView();
|
||||||
case SettingsPage.language:
|
case SettingsPage.language:
|
||||||
return const SettingsLanguageView();
|
return const SettingsLanguageView();
|
||||||
case SettingsPage.files:
|
case SettingsPage.files:
|
||||||
return const SettingsFileSystemView();
|
return const SettingsFileSystemView();
|
||||||
case SettingsPage.user:
|
|
||||||
return SettingsUserView(
|
|
||||||
user,
|
|
||||||
didLogin: () => dismissDialog(),
|
|
||||||
didLogout: didLogout,
|
|
||||||
didOpenUser: restartApp,
|
|
||||||
);
|
|
||||||
case SettingsPage.notifications:
|
case SettingsPage.notifications:
|
||||||
return const SettingsNotificationsView();
|
return const SettingsNotificationsView();
|
||||||
case SettingsPage.cloud:
|
case SettingsPage.cloud:
|
||||||
return SettingCloud(
|
return SettingCloud(restartAppFlowy: () => restartApp());
|
||||||
restartAppFlowy: () => restartApp(),
|
|
||||||
);
|
|
||||||
case SettingsPage.shortcuts:
|
case SettingsPage.shortcuts:
|
||||||
return const SettingsCustomizeShortcutsWrapper();
|
return const SettingsShortcutsView();
|
||||||
case SettingsPage.member:
|
case SettingsPage.member:
|
||||||
return WorkspaceMembersPage(userProfile: user);
|
return WorkspaceMembersPage(userProfile: user);
|
||||||
case SettingsPage.featureFlags:
|
case SettingsPage.featureFlags:
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,11 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/startup/startup.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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class FeatureFlagsPage extends StatelessWidget {
|
class FeatureFlagsPage extends StatelessWidget {
|
||||||
const FeatureFlagsPage({
|
const FeatureFlagsPage({
|
||||||
@ -10,36 +14,30 @@ class FeatureFlagsPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return SettingsBody(
|
||||||
child: SeparatedColumn(
|
children: [
|
||||||
children: [
|
const SettingsHeader(title: 'Feature flags'),
|
||||||
...FeatureFlag.data.entries
|
SeparatedColumn(
|
||||||
|
children: FeatureFlag.data.entries
|
||||||
.where((e) => e.key != FeatureFlag.unknown)
|
.where((e) => e.key != FeatureFlag.unknown)
|
||||||
.map(
|
.map((e) => _FeatureFlagItem(featureFlag: e.key))
|
||||||
(e) => _FeatureFlagItem(featureFlag: e.key),
|
.toList(),
|
||||||
),
|
),
|
||||||
FlowyTextButton(
|
const SettingsCategorySpacer(),
|
||||||
'Restart the app to apply changes',
|
FlowyTextButton(
|
||||||
fontSize: 16.0,
|
'Restart the app to apply changes',
|
||||||
fontColor: Colors.red,
|
fontSize: 16.0,
|
||||||
padding: const EdgeInsets.symmetric(
|
fontColor: Colors.red,
|
||||||
horizontal: 16.0,
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
vertical: 12.0,
|
onPressed: () async => runAppFlowy(),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
],
|
||||||
await runAppFlowy();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FeatureFlagItem extends StatefulWidget {
|
class _FeatureFlagItem extends StatefulWidget {
|
||||||
const _FeatureFlagItem({
|
const _FeatureFlagItem({required this.featureFlag});
|
||||||
required this.featureFlag,
|
|
||||||
});
|
|
||||||
|
|
||||||
final FeatureFlag featureFlag;
|
final FeatureFlag featureFlag;
|
||||||
|
|
||||||
@ -51,21 +49,11 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: FlowyText(
|
title: FlowyText(widget.featureFlag.name, fontSize: 16.0),
|
||||||
widget.featureFlag.name,
|
subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3),
|
||||||
fontSize: 16.0,
|
|
||||||
),
|
|
||||||
subtitle: FlowyText.small(
|
|
||||||
widget.featureFlag.description,
|
|
||||||
maxLines: 3,
|
|
||||||
),
|
|
||||||
trailing: Switch.adaptive(
|
trailing: Switch.adaptive(
|
||||||
value: widget.featureFlag.isOn,
|
value: widget.featureFlag.isOn,
|
||||||
onChanged: (value) {
|
onChanged: (value) => setState(() => widget.featureFlag.update(value)),
|
||||||
setState(() {
|
|
||||||
widget.featureFlag.update(value);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_location_cubit.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/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.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:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
@ -19,9 +20,7 @@ import '../../../../../startup/startup.dart';
|
|||||||
import '../../../../../startup/tasks/prelude.dart';
|
import '../../../../../startup/tasks/prelude.dart';
|
||||||
|
|
||||||
class SettingsFileLocationCustomizer extends StatefulWidget {
|
class SettingsFileLocationCustomizer extends StatefulWidget {
|
||||||
const SettingsFileLocationCustomizer({
|
const SettingsFileLocationCustomizer({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsFileLocationCustomizer> createState() =>
|
State<SettingsFileLocationCustomizer> createState() =>
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.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/settings/widgets/members/workspace_member_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.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/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:string_validator/string_validator.dart';
|
import 'package:string_validator/string_validator.dart';
|
||||||
|
|
||||||
class WorkspaceMembersPage extends StatelessWidget {
|
class WorkspaceMembersPage extends StatelessWidget {
|
||||||
const WorkspaceMembersPage({
|
const WorkspaceMembersPage({super.key, required this.userProfile});
|
||||||
super.key,
|
|
||||||
required this.userProfile,
|
|
||||||
});
|
|
||||||
|
|
||||||
final UserProfilePB userProfile;
|
final UserProfilePB userProfile;
|
||||||
|
|
||||||
@ -33,25 +34,22 @@ class WorkspaceMembersPage extends StatelessWidget {
|
|||||||
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
|
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||||
listener: _showResultDialog,
|
listener: _showResultDialog,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SingleChildScrollView(
|
return SettingsBody(
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// title
|
||||||
children: [
|
SettingsHeader(
|
||||||
// title
|
title: LocaleKeys.settings_appearance_members_title.tr(),
|
||||||
FlowyText.semibold(
|
),
|
||||||
LocaleKeys.settings_appearance_members_title.tr(),
|
if (state.myRole.canInvite) const _InviteMember(),
|
||||||
fontSize: 20,
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const VSpace(12.0),
|
|
||||||
FlowyText.semibold(
|
FlowyText.semibold(
|
||||||
LocaleKeys.settings_appearance_members_inviteMembers.tr(),
|
LocaleKeys.settings_appearance_members_inviteMembers.tr(),
|
||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
@ -151,7 +148,6 @@ class _InviteMemberState extends State<_InviteMember> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const VSpace(16.0),
|
|
||||||
/* Enable this when the feature is ready
|
/* Enable this when the feature is ready
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
backgroundColor: const Color(0xFFE0E0E0),
|
backgroundColor: const Color(0xFFE0E0E0),
|
||||||
@ -183,10 +179,6 @@ class _InviteMemberState extends State<_InviteMember> {
|
|||||||
),
|
),
|
||||||
const VSpace(16.0),
|
const VSpace(16.0),
|
||||||
*/
|
*/
|
||||||
const Divider(
|
|
||||||
height: 1.0,
|
|
||||||
thickness: 1.0,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -194,11 +186,10 @@ class _InviteMemberState extends State<_InviteMember> {
|
|||||||
void _inviteMember() {
|
void _inviteMember() {
|
||||||
final email = _emailController.text;
|
final email = _emailController.text;
|
||||||
if (!isEmail(email)) {
|
if (!isEmail(email)) {
|
||||||
showSnackBarMessage(
|
return showSnackBarMessage(
|
||||||
context,
|
context,
|
||||||
LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
context
|
context
|
||||||
.read<WorkspaceMemberBloc>()
|
.read<WorkspaceMemberBloc>()
|
||||||
@ -219,22 +210,17 @@ class _MemberList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return SeparatedColumn(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
separatorBuilder: () => const Divider(),
|
||||||
children: [
|
children: [
|
||||||
const VSpace(16.0),
|
const _MemberListHeader(),
|
||||||
SeparatedColumn(
|
...members.map(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
(member) => _MemberItem(
|
||||||
separatorBuilder: () => const Divider(),
|
member: member,
|
||||||
children: [
|
myRole: myRole,
|
||||||
const _MemberListHeader(),
|
userProfile: userProfile,
|
||||||
...members.map(
|
),
|
||||||
(member) => _MemberItem(
|
|
||||||
member: member,
|
|
||||||
myRole: myRole,
|
|
||||||
userProfile: userProfile,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
import 'package:appflowy/env/env.dart';
|
import 'package:appflowy/env/env.dart';
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.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/mobile/presentation/widgets/widgets.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.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/settings/widgets/setting_local_cloud.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.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:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@ -37,8 +40,11 @@ class SettingCloud extends StatelessWidget {
|
|||||||
create: (context) => CloudSettingBloc(cloudType),
|
create: (context) => CloudSettingBloc(cloudType),
|
||||||
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return SettingsBody(
|
||||||
children: [
|
children: [
|
||||||
|
SettingsHeader(
|
||||||
|
title: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||||
|
),
|
||||||
if (Env.enableCustomCloud)
|
if (Env.enableCustomCloud)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/startup.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SettingThirdPartyLogin extends StatelessWidget {
|
class SettingThirdPartyLogin extends StatelessWidget {
|
||||||
@ -42,24 +43,12 @@ class SettingThirdPartyLogin extends StatelessWidget {
|
|||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
FlowyText.medium(
|
|
||||||
LocaleKeys.signIn_signInWith.tr(),
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
const HSpace(6),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const VSpace(6),
|
|
||||||
promptMessage,
|
promptMessage,
|
||||||
const VSpace(6),
|
const VSpace(6),
|
||||||
indicator,
|
indicator,
|
||||||
const VSpace(6),
|
const VSpace(6),
|
||||||
if (isAuthEnabled) const ThirdPartySignInButtons(),
|
if (isAuthEnabled) const ThirdPartySignInButtons(),
|
||||||
const VSpace(6),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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/appearance_defaults.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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/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/date_format_setting.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/time_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:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'settings_appearance/settings_appearance.dart';
|
import 'settings_appearance/settings_appearance.dart';
|
||||||
@ -14,59 +19,57 @@ class SettingsAppearanceView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return BlocProvider<DynamicPluginBloc>(
|
||||||
child: BlocProvider<DynamicPluginBloc>(
|
create: (_) => DynamicPluginBloc(),
|
||||||
create: (_) => DynamicPluginBloc(),
|
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
builder: (context, state) {
|
||||||
builder: (context, state) {
|
return SettingsBody(
|
||||||
return Column(
|
children: [
|
||||||
children: [
|
SettingsHeader(title: LocaleKeys.settings_menu_appearance.tr()),
|
||||||
ColorSchemeSetting(
|
ColorSchemeSetting(
|
||||||
currentTheme: state.appTheme.themeName,
|
currentTheme: state.appTheme.themeName,
|
||||||
bloc: context.read<DynamicPluginBloc>(),
|
bloc: context.read<DynamicPluginBloc>(),
|
||||||
),
|
),
|
||||||
BrightnessSetting(
|
BrightnessSetting(
|
||||||
currentThemeMode: state.themeMode,
|
currentThemeMode: state.themeMode,
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
ThemeFontFamilySetting(
|
ThemeFontFamilySetting(
|
||||||
currentFontFamily: state.font,
|
currentFontFamily: state.font,
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
DocumentCursorColorSetting(
|
DocumentCursorColorSetting(
|
||||||
currentCursorColor: state.documentCursorColor ??
|
currentCursorColor: state.documentCursorColor ??
|
||||||
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
|
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
DocumentSelectionColorSetting(
|
DocumentSelectionColorSetting(
|
||||||
currentSelectionColor: state.documentSelectionColor ??
|
currentSelectionColor: state.documentSelectionColor ??
|
||||||
DefaultAppearanceSettings
|
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(
|
||||||
.getDefaultDocumentSelectionColor(
|
context,
|
||||||
context,
|
),
|
||||||
),
|
),
|
||||||
),
|
const Divider(),
|
||||||
const Divider(),
|
LayoutDirectionSetting(
|
||||||
LayoutDirectionSetting(
|
currentLayoutDirection: state.layoutDirection,
|
||||||
currentLayoutDirection: state.layoutDirection,
|
),
|
||||||
),
|
TextDirectionSetting(
|
||||||
TextDirectionSetting(
|
currentTextDirection: state.textDirection,
|
||||||
currentTextDirection: state.textDirection,
|
),
|
||||||
),
|
const EnableRTLToolbarItemsSetting(),
|
||||||
const EnableRTLToolbarItemsSetting(),
|
const Divider(),
|
||||||
const Divider(),
|
DateFormatSetting(
|
||||||
DateFormatSetting(
|
currentFormat: state.dateFormat,
|
||||||
currentFormat: state.dateFormat,
|
),
|
||||||
),
|
TimeFormatSetting(
|
||||||
TimeFormatSetting(
|
currentFormat: state.timeFormat,
|
||||||
currentFormat: state.timeFormat,
|
),
|
||||||
),
|
const Divider(),
|
||||||
const Divider(),
|
CreateFileSettings(),
|
||||||
CreateFileSettings(),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.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_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.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:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SettingsCustomizeShortcutsWrapper extends StatelessWidget {
|
class SettingsShortcutsView extends StatelessWidget {
|
||||||
const SettingsCustomizeShortcutsWrapper({super.key});
|
const SettingsShortcutsView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<ShortcutsCubit>(
|
return BlocProvider<ShortcutsCubit>(
|
||||||
create: (_) =>
|
create: (_) =>
|
||||||
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
|
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
|
||||||
child: const SettingsCustomizeShortcutsView(),
|
child: SettingsBody(
|
||||||
);
|
children: [
|
||||||
}
|
SettingsHeader(
|
||||||
}
|
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||||
|
),
|
||||||
class SettingsCustomizeShortcutsView extends StatelessWidget {
|
BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
||||||
const SettingsCustomizeShortcutsView({super.key});
|
builder: (_, state) => switch (state.status) {
|
||||||
|
ShortcutsStatus.initial ||
|
||||||
@override
|
ShortcutsStatus.updating =>
|
||||||
Widget build(BuildContext context) {
|
const Center(child: CircularProgressIndicator()),
|
||||||
return BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
ShortcutsStatus.success =>
|
||||||
builder: (context, state) {
|
ShortcutsListView(shortcuts: state.commandShortcutEvents),
|
||||||
switch (state.status) {
|
ShortcutsStatus.failure =>
|
||||||
case ShortcutsStatus.initial:
|
ShortcutsErrorView(errorMessage: state.error),
|
||||||
case ShortcutsStatus.updating:
|
},
|
||||||
return const Center(child: CircularProgressIndicator());
|
),
|
||||||
case ShortcutsStatus.success:
|
],
|
||||||
return ShortcutsListView(shortcuts: state.commandShortcutEvents);
|
),
|
||||||
case ShortcutsStatus.failure:
|
|
||||||
return ShortcutsErrorView(
|
|
||||||
errorMessage: state.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShortcutsListView extends StatelessWidget {
|
class ShortcutsListView extends StatelessWidget {
|
||||||
const ShortcutsListView({
|
const ShortcutsListView({super.key, required this.shortcuts});
|
||||||
super.key,
|
|
||||||
required this.shortcuts,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<CommandShortcutEvent> shortcuts;
|
final List<CommandShortcutEvent> shortcuts;
|
||||||
|
|
||||||
@ -73,14 +67,7 @@ class ShortcutsListView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const VSpace(10),
|
const VSpace(10),
|
||||||
Expanded(
|
...shortcuts.map((e) => ShortcutsListTile(shortcutEvent: e)),
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: shortcuts.length,
|
|
||||||
itemBuilder: (context, index) => ShortcutsListTile(
|
|
||||||
shortcutEvent: shortcuts[index],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const VSpace(10),
|
const VSpace(10),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
@ -88,9 +75,7 @@ class ShortcutsListView extends StatelessWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
FlowyTextButton(
|
FlowyTextButton(
|
||||||
LocaleKeys.settings_shortcuts_resetToDefault.tr(),
|
LocaleKeys.settings_shortcuts_resetToDefault.tr(),
|
||||||
onPressed: () {
|
onPressed: () => context.read<ShortcutsCubit>().resetToDefault(),
|
||||||
context.read<ShortcutsCubit>().resetToDefault();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -248,9 +233,7 @@ class ShortcutsErrorView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
FlowyIconButton(
|
FlowyIconButton(
|
||||||
icon: const Icon(Icons.replay_outlined),
|
icon: const Icon(Icons.replay_outlined),
|
||||||
onPressed: () {
|
onPressed: () => context.read<ShortcutsCubit>().fetchShortcuts(),
|
||||||
BlocProvider.of<ShortcutsCubit>(context).fetchShortcuts();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -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/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_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_cache_widget.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SettingsFileSystemView extends StatefulWidget {
|
class SettingsFileSystemView extends StatelessWidget {
|
||||||
const SettingsFileSystemView({
|
const SettingsFileSystemView({super.key});
|
||||||
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(),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SeparatedColumn(
|
return SettingsBody(
|
||||||
separatorBuilder: () => const Divider(),
|
children: [
|
||||||
children: _items,
|
SettingsHeader(title: LocaleKeys.settings_menu_files.tr()),
|
||||||
|
const SettingsFileLocationCustomizer(),
|
||||||
|
const SettingsCategorySpacer(),
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
const SettingsExportFileWidget(),
|
||||||
|
],
|
||||||
|
const ImportAppFlowyData(),
|
||||||
|
const SettingsCategorySpacer(),
|
||||||
|
const SettingsFileCacheWidget(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.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/language.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SettingsLanguageView extends StatelessWidget {
|
class SettingsLanguageView extends StatelessWidget {
|
||||||
@ -13,18 +16,21 @@ class SettingsLanguageView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
builder: (context, state) => SettingsBody(
|
||||||
builder: (context, state) => Row(
|
children: [
|
||||||
children: [
|
SettingsHeader(title: LocaleKeys.settings_menu_language.tr()),
|
||||||
Expanded(
|
Row(
|
||||||
child: FlowyText.medium(
|
children: [
|
||||||
LocaleKeys.settings_menu_language.tr(),
|
Expanded(
|
||||||
|
child: FlowyText.medium(
|
||||||
|
LocaleKeys.settings_menu_language.tr(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
LanguageSelector(currentLocale: state.locale),
|
||||||
LanguageSelector(currentLocale: state.locale),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.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/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.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/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SettingsMenu extends StatelessWidget {
|
class SettingsMenu extends StatelessWidget {
|
||||||
const SettingsMenu({
|
const SettingsMenu({
|
||||||
@ -22,79 +25,127 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
// Column > Expanded for full size no matter the content
|
||||||
child: SeparatedColumn(
|
return Column(
|
||||||
separatorBuilder: () => const SizedBox(height: 10),
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
SettingsMenuElement(
|
child: Container(
|
||||||
page: SettingsPage.appearance,
|
padding: const EdgeInsets.symmetric(vertical: 8) +
|
||||||
selectedPage: currentPage,
|
const EdgeInsets.only(left: 8, right: 4),
|
||||||
label: LocaleKeys.settings_menu_appearance.tr(),
|
decoration: BoxDecoration(
|
||||||
icon: Icons.brightness_4,
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
changeSelectedPage: changeSelectedPage,
|
borderRadius: const BorderRadius.only(
|
||||||
),
|
topLeft: Radius.circular(8),
|
||||||
SettingsMenuElement(
|
bottomLeft: Radius.circular(8),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
if (kDebugMode)
|
child: SingleChildScrollView(
|
||||||
SettingsMenuElement(
|
// Right padding is added to make the scrollbar centered
|
||||||
// no need to translate this page
|
// in the space between the menu and the content
|
||||||
page: SettingsPage.featureFlags,
|
padding: const EdgeInsets.only(right: 4) +
|
||||||
selectedPage: currentPage,
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
label: 'Feature Flags',
|
physics: const ClampingScrollPhysics(),
|
||||||
icon: Icons.flag,
|
child: SeparatedColumn(
|
||||||
changeSelectedPage: changeSelectedPage,
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||||
import 'package:flowy_infra/size.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/hover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class SettingsMenuElement extends StatelessWidget {
|
class SettingsMenuElement extends StatelessWidget {
|
||||||
const SettingsMenuElement({
|
const SettingsMenuElement({
|
||||||
@ -17,27 +19,22 @@ class SettingsMenuElement extends StatelessWidget {
|
|||||||
final SettingsPage page;
|
final SettingsPage page;
|
||||||
final SettingsPage selectedPage;
|
final SettingsPage selectedPage;
|
||||||
final String label;
|
final String label;
|
||||||
final IconData icon;
|
final Widget icon;
|
||||||
final Function changeSelectedPage;
|
final Function changeSelectedPage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyHover(
|
return FlowyHover(
|
||||||
|
isSelected: () => page == selectedPage,
|
||||||
resetHoverOnRebuild: false,
|
resetHoverOnRebuild: false,
|
||||||
style: HoverStyle(
|
style: HoverStyle(
|
||||||
hoverColor: Theme.of(context).colorScheme.primary,
|
hoverColor: AFThemeExtension.of(context).greySelect,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
dense: true,
|
||||||
icon,
|
leading: icon,
|
||||||
size: 16,
|
onTap: () => changeSelectedPage(page),
|
||||||
color: page == selectedPage
|
|
||||||
? Theme.of(context).colorScheme.onSurface
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
changeSelectedPage(page);
|
|
||||||
},
|
|
||||||
selected: page == selectedPage,
|
selected: page == selectedPage,
|
||||||
selectedColor: Theme.of(context).colorScheme.onSurface,
|
selectedColor: Theme.of(context).colorScheme.onSurface,
|
||||||
selectedTileColor: Theme.of(context).colorScheme.primary,
|
selectedTileColor: Theme.of(context).colorScheme.primary,
|
||||||
@ -45,7 +42,7 @@ class SettingsMenuElement extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
minLeadingWidth: 0,
|
minLeadingWidth: 0,
|
||||||
title: FlowyText.semibold(
|
title: FlowyText.medium(
|
||||||
label,
|
label,
|
||||||
fontSize: FontSizes.s14,
|
fontSize: FontSizes.s14,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.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:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SettingsNotificationsView extends StatelessWidget {
|
class SettingsNotificationsView extends StatelessWidget {
|
||||||
@ -12,32 +15,26 @@ class SettingsNotificationsView extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
|
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SingleChildScrollView(
|
return SettingsBody(
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
SettingsHeader(title: LocaleKeys.settings_menu_notifications.tr()),
|
||||||
mainAxisSize: MainAxisSize.min,
|
FlowySettingListTile(
|
||||||
children: [
|
label: LocaleKeys.settings_notifications_enableNotifications_label
|
||||||
FlowySettingListTile(
|
.tr(),
|
||||||
label: LocaleKeys
|
hint: LocaleKeys.settings_notifications_enableNotifications_hint
|
||||||
.settings_notifications_enableNotifications_label
|
.tr(),
|
||||||
.tr(),
|
trailing: [
|
||||||
hint: LocaleKeys.settings_notifications_enableNotifications_hint
|
Switch(
|
||||||
.tr(),
|
value: state.isNotificationsEnabled,
|
||||||
trailing: [
|
splashRadius: 0,
|
||||||
Switch(
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
value: state.isNotificationsEnabled,
|
onChanged: (value) => context
|
||||||
splashRadius: 0,
|
.read<NotificationSettingsCubit>()
|
||||||
activeColor: Theme.of(context).colorScheme.primary,
|
.toggleNotificationsEnabled(),
|
||||||
onChanged: (value) {
|
),
|
||||||
context
|
],
|
||||||
.read<NotificationSettingsCubit>()
|
),
|
||||||
.toggleNotificationsEnabled();
|
],
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class MoreViewActions extends StatefulWidget {
|
class MoreViewActions extends StatefulWidget {
|
||||||
@ -107,7 +108,7 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
|||||||
FlowySvgs.three_dots_vertical_s,
|
FlowySvgs.three_dots_vertical_s,
|
||||||
size: const Size.square(16),
|
size: const Size.square(16),
|
||||||
color: isHovering
|
color: isHovering
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
? Theme.of(context).colorScheme.onSecondary
|
||||||
: Theme.of(context).iconTheme.color,
|
: Theme.of(context).iconTheme.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user