mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00: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:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../desktop/board/board_hide_groups_test.dart';
|
||||
import '../shared/dir.dart';
|
||||
import '../shared/mock/mock_file_picker.dart';
|
||||
import '../shared/util.dart';
|
||||
@ -37,22 +44,35 @@ void main() {
|
||||
|
||||
// reanme the name of the anon user
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
final userNameFinder = find.descendant(
|
||||
of: find.byType(SettingsUserView),
|
||||
matching: find.byType(UserNameInput),
|
||||
);
|
||||
await tester.enterText(userNameFinder, 'local_user');
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterUserName('local_user');
|
||||
|
||||
// Scroll to sign-in
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(SignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
|
||||
await tester.tapButton(find.byType(SignInOutButton));
|
||||
|
||||
// sign up with Google
|
||||
await tester.tapGoogleLoginInButton();
|
||||
|
||||
// sign out
|
||||
await tester.expectToSeeHomePage();
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Scroll to sign-out
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(SignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
|
||||
await tester.logout();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@ -63,8 +83,9 @@ void main() {
|
||||
|
||||
// New anon user name
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
final userNameInput = tester.widget(userNameFinder) as UserNameInput;
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
final userNameInput =
|
||||
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
||||
expect(userNameInput.name, 'Me');
|
||||
});
|
||||
});
|
||||
|
@ -6,8 +6,8 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -37,11 +37,18 @@ void main() {
|
||||
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.tapButton(find.byType(SettingLogoutButton));
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
tester.expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
// Scroll to sign-out
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(SignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
await tester.tapButton(find.byType(SignInOutButton));
|
||||
|
||||
tester.expectToSeeText(LocaleKeys.button_confirm.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());
|
||||
|
||||
// Go to the sign in page again
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
@ -56,7 +63,16 @@ void main() {
|
||||
|
||||
// should not see the sync setting page when sign in as anonymous
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Scroll to sign-in
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(SignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
await tester.tapButton(find.byType(SignInOutButton));
|
||||
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
|
||||
|
@ -8,14 +8,15 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../shared/dir.dart';
|
||||
import '../shared/mock/mock_file_picker.dart';
|
||||
import '../shared/util.dart';
|
||||
@ -50,7 +51,7 @@ void main() {
|
||||
await tester.waitForSeconds(6);
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
await tester.logout();
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@ -25,11 +23,8 @@ void main() {
|
||||
|
||||
// Open the setting page and sign out
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.tapButton(find.byType(SettingLogoutButton));
|
||||
|
||||
tester.expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
await tester.logout();
|
||||
|
||||
// Go to the sign in page again
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
@ -42,7 +37,16 @@ void main() {
|
||||
|
||||
// should not see the sync setting page when sign in as anonymous
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Scroll to sign-out
|
||||
await tester.scrollUntilVisible(
|
||||
find.byType(SignInOutButton),
|
||||
100,
|
||||
scrollable: find.findSettingsScrollable(),
|
||||
);
|
||||
await tester.tapButton(find.byType(SignInOutButton));
|
||||
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
|
||||
|
@ -2,22 +2,27 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import '../desktop/board/board_hide_groups_test.dart';
|
||||
import '../shared/database_test_op.dart';
|
||||
import '../shared/dir.dart';
|
||||
import '../shared/emoji.dart';
|
||||
@ -39,28 +44,9 @@ void main() {
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
// final userAvatarFinder = find.descendant(
|
||||
// of: find.byType(SettingsUserView),
|
||||
// matching: find.byType(UserAvatar),
|
||||
// );
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Open icon picker dialog and select emoji
|
||||
// await tester.tap(userAvatarFinder);
|
||||
// await tester.pumpAndSettle();
|
||||
// await tester.tapEmoji('😁');
|
||||
// await tester.pumpAndSettle();
|
||||
// final UserAvatar userAvatar =
|
||||
// tester.widget(userAvatarFinder) as UserAvatar;
|
||||
// expect(userAvatar.iconUrl, '😁');
|
||||
|
||||
// enter user name
|
||||
final userNameFinder = find.descendant(
|
||||
of: find.byType(SettingsUserView),
|
||||
matching: find.byType(UserNameInput),
|
||||
);
|
||||
await tester.enterText(userNameFinder, name);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterUserName(name);
|
||||
await tester.tapEscButton();
|
||||
|
||||
// wait 2 seconds for the sync to finish
|
||||
@ -78,23 +64,12 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// verify icon
|
||||
// final userAvatarFinder = find.descendant(
|
||||
// of: find.byType(SettingsUserView),
|
||||
// matching: find.byType(UserAvatar),
|
||||
// );
|
||||
// final UserAvatar userAvatar = tester.widget(userAvatarFinder) as UserAvatar;
|
||||
// expect(userAvatar.iconUrl, '😁');
|
||||
// Verify name
|
||||
final profileSetting =
|
||||
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
||||
|
||||
// verify name
|
||||
final userNameFinder = find.descendant(
|
||||
of: find.byType(SettingsUserView),
|
||||
matching: find.byType(UserNameInput),
|
||||
);
|
||||
final UserNameInput userNameInput =
|
||||
tester.widget(userNameFinder) as UserNameInput;
|
||||
expect(userNameInput.name, name);
|
||||
expect(profileSetting.name, name);
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
@ -14,12 +16,10 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_worksp
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
@ -14,7 +14,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
|
@ -11,7 +11,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
|
@ -1,25 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'expectation.dart';
|
||||
import 'util.dart';
|
||||
|
||||
extension AppFlowyAuthTest on WidgetTester {
|
||||
Future<void> tapGoogleLoginInButton() async {
|
||||
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
|
||||
}
|
||||
|
||||
/// Requires being on the SettingsPage.account of the SettingsDialog
|
||||
Future<void> logout() async {
|
||||
await tapButton(find.byType(SettingLogoutButton));
|
||||
final scrollable = find.findSettingsScrollable();
|
||||
await scrollUntilVisible(
|
||||
find.byType(SignInOutButton),
|
||||
100,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
|
||||
expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
await tapButton(find.byType(SignInOutButton));
|
||||
|
||||
expectToSeeText(LocaleKeys.button_confirm.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_confirm.tr());
|
||||
}
|
||||
|
||||
Future<void> tapSignInAsGuest() async {
|
||||
|
@ -1,6 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/env/cloud_env_test.dart';
|
||||
import 'package:appflowy/startup/entry_point.dart';
|
||||
@ -13,16 +16,12 @@ import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widget
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class FlowyTestContext {
|
||||
FlowyTestContext({
|
||||
required this.applicationDataDirectory,
|
||||
});
|
||||
FlowyTestContext({required this.applicationDataDirectory});
|
||||
|
||||
final String applicationDataDirectory;
|
||||
}
|
||||
@ -75,7 +74,7 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
if (cloudType != null) {
|
||||
switch (cloudType) {
|
||||
case AuthenticatorType.local:
|
||||
await useLocal();
|
||||
await useLocalServer();
|
||||
break;
|
||||
case AuthenticatorType.supabase:
|
||||
await useTestSupabaseCloud();
|
||||
@ -187,37 +186,14 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithName(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
Finder button = find.text(
|
||||
tr,
|
||||
findRichText: true,
|
||||
skipOffstage: false,
|
||||
);
|
||||
Future<void> tapButtonWithName(String tr, {int milliseconds = 500}) async {
|
||||
Finder button = find.text(tr, findRichText: true, skipOffstage: false);
|
||||
if (button.evaluate().isEmpty) {
|
||||
button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == tr,
|
||||
);
|
||||
}
|
||||
await tapButton(
|
||||
button,
|
||||
milliseconds: milliseconds,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithTooltip(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
final button = find.byTooltip(tr);
|
||||
await tapButton(
|
||||
button,
|
||||
milliseconds: milliseconds,
|
||||
);
|
||||
return;
|
||||
await tapButton(button, milliseconds: milliseconds);
|
||||
}
|
||||
|
||||
Future<void> doubleTapAt(
|
||||
@ -232,34 +208,8 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> doubleTapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(
|
||||
finder,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
|
||||
await pump(kDoubleTapMinTime);
|
||||
|
||||
await tap(
|
||||
finder,
|
||||
buttons: buttons,
|
||||
pointer: pointer,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> wait(int milliseconds) async {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,10 +221,6 @@ extension AppFlowyFinderTestBase on CommonFinders {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> useLocal() async {
|
||||
await useLocalServer();
|
||||
}
|
||||
|
||||
Future<void> useTestSupabaseCloud() async {
|
||||
await useSupabaseCloud(
|
||||
url: TestEnv.supabaseUrl,
|
||||
|
@ -24,6 +24,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -72,27 +73,6 @@ extension CommonOperations on WidgetTester {
|
||||
await tapButton(newPageButton);
|
||||
}
|
||||
|
||||
/// Tap the create document button.
|
||||
///
|
||||
/// Must call [tapAddViewButton] first.
|
||||
Future<void> tapCreateDocumentButton() async {
|
||||
await tapButtonWithName(LocaleKeys.document_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the create grid button.
|
||||
///
|
||||
/// Must call [tapAddViewButton] first.
|
||||
Future<void> tapCreateGridButton() async {
|
||||
await tapButtonWithName(LocaleKeys.grid_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the create grid button.
|
||||
///
|
||||
/// Must call [tapAddViewButton] first.
|
||||
Future<void> tapCreateCalendarButton() async {
|
||||
await tapButtonWithName(LocaleKeys.calendar_menuName.tr());
|
||||
}
|
||||
|
||||
/// Tap the import button.
|
||||
///
|
||||
/// Must call [tapAddViewButton] first.
|
||||
@ -181,15 +161,9 @@ extension CommonOperations on WidgetTester {
|
||||
}) async {
|
||||
final pageNames = findPageName(name, layout: layout);
|
||||
if (useLast) {
|
||||
await hoverOnWidget(
|
||||
pageNames.last,
|
||||
onHover: onHover,
|
||||
);
|
||||
await hoverOnWidget(pageNames.last, onHover: onHover);
|
||||
} else {
|
||||
await hoverOnWidget(
|
||||
pageNames.first,
|
||||
onHover: onHover,
|
||||
);
|
||||
await hoverOnWidget(pageNames.first, onHover: onHover);
|
||||
}
|
||||
}
|
||||
|
||||
@ -497,9 +471,7 @@ extension CommonOperations on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> openNotificationHub({
|
||||
int tabIndex = 0,
|
||||
}) async {
|
||||
Future<void> openNotificationHub({int tabIndex = 0}) async {
|
||||
final finder = find.descendant(
|
||||
of: find.byType(NotificationButton),
|
||||
matching: find.byWidgetPredicate(
|
||||
@ -542,15 +514,6 @@ extension CommonOperations on WidgetTester {
|
||||
await tapButton(workspace, milliseconds: 2000);
|
||||
}
|
||||
|
||||
Future<void> closeCollaborativeWorkspaceMenu() async {
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
throw UnsupportedError('Collaborative workspace is not enabled');
|
||||
}
|
||||
|
||||
await tapAt(Offset.zero);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> createCollaborativeWorkspace(String name) async {
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
throw UnsupportedError('Collaborative workspace is not enabled');
|
||||
@ -576,6 +539,20 @@ extension CommonOperations on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsFinder on CommonFinders {
|
||||
Finder findSettingsScrollable() => find
|
||||
.descendant(
|
||||
of: find
|
||||
.descendant(
|
||||
of: find.byType(SettingsBody),
|
||||
matching: find.byType(SingleChildScrollView),
|
||||
)
|
||||
.first,
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first;
|
||||
}
|
||||
|
||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||
String get menuName {
|
||||
switch (this) {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:archive/archive_io.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@ -51,11 +52,7 @@ class TestWorkspaceService {
|
||||
Future<void> setUpAll() async {
|
||||
final root = await workspace.root;
|
||||
final path = root.path;
|
||||
SharedPreferences.setMockInitialValues(
|
||||
{
|
||||
KVKeys.pathLocation: path,
|
||||
},
|
||||
);
|
||||
SharedPreferences.setMockInitialValues({KVKeys.pathLocation: path});
|
||||
}
|
||||
|
||||
/// Workspaces that are checked into source are compressed. [TestWorkspaceService.setUp()] decompresses the file into an ephemeral directory that will be ignored by source control.
|
||||
|
@ -1,7 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -31,8 +29,6 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/discl
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';
|
||||
@ -57,6 +53,10 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
|
||||
@ -71,7 +71,6 @@ import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
@ -343,16 +342,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
return w.isToday;
|
||||
}
|
||||
|
||||
Future<void> toggleDateRange() async {
|
||||
final findDateEditor = find.byType(EndTimeButton);
|
||||
final findToggle = find.byType(Toggle);
|
||||
final finder = find.descendant(
|
||||
of: findDateEditor,
|
||||
matching: findToggle,
|
||||
);
|
||||
await tapButton(finder);
|
||||
}
|
||||
|
||||
Future<void> tapChangeDateTimeFormatButton() async {
|
||||
await tapButton(find.byType(DateTypeOptionButton));
|
||||
}
|
||||
@ -403,9 +392,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
/// The [SelectOptionCellEditor] must be opened first.
|
||||
Future<void> createOption({
|
||||
required String name,
|
||||
}) async {
|
||||
Future<void> createOption({required String name}) async {
|
||||
final findEditor = find.byType(SelectOptionCellEditor);
|
||||
expect(findEditor, findsOneWidget);
|
||||
|
||||
@ -419,9 +406,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> selectOption({
|
||||
required String name,
|
||||
}) async {
|
||||
Future<void> selectOption({required String name}) async {
|
||||
final option = find.byWidgetPredicate(
|
||||
(widget) => widget is SelectOptionTagCell && widget.option.name == name,
|
||||
);
|
||||
@ -440,11 +425,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
(widget.name == name || widget.option?.name == name),
|
||||
);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: option,
|
||||
);
|
||||
|
||||
final cell = find.descendant(of: findRow.at(rowIndex), matching: option);
|
||||
expect(cell, findsOneWidget);
|
||||
}
|
||||
|
||||
@ -458,11 +439,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
(widget) => widget is SelectOptionTag,
|
||||
);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: options,
|
||||
);
|
||||
|
||||
final cell = find.descendant(of: findRow.at(rowIndex), matching: options);
|
||||
expect(cell, matcher);
|
||||
}
|
||||
|
||||
@ -470,21 +447,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(FieldType.Checklist);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
final cell = find.descendant(of: findRow.at(rowIndex), matching: findCell);
|
||||
await tapButton(cell);
|
||||
}
|
||||
|
||||
void assertChecklistEditorVisible({required bool visible}) {
|
||||
final editor = find.byType(ChecklistCellEditor);
|
||||
if (visible) {
|
||||
expect(editor, findsOneWidget);
|
||||
} else {
|
||||
expect(editor, findsNothing);
|
||||
return expect(editor, findsOneWidget);
|
||||
}
|
||||
expect(editor, findsNothing);
|
||||
}
|
||||
|
||||
Future<void> createNewChecklistTask({
|
||||
@ -519,7 +491,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
required bool isChecked,
|
||||
}) {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
final widget = this.widget<ChecklistItem>(task);
|
||||
assert(
|
||||
widget.task.data.name == name && widget.task.isSelected == isChecked,
|
||||
@ -591,27 +562,16 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> editTitleInRowDetailPage(String title) async {
|
||||
final titleField = find.byType(EditableTextCell);
|
||||
await enterText(titleField, title);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> hoverRowBanner() async {
|
||||
final banner = find.byType(RowBanner);
|
||||
expect(banner, findsOneWidget);
|
||||
|
||||
await startGesture(
|
||||
getCenter(banner),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
|
||||
await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> openEmojiPicker() async {
|
||||
await tapButton(find.byType(AddEmojiButton));
|
||||
}
|
||||
Future<void> openEmojiPicker() async =>
|
||||
tapButton(find.byType(AddEmojiButton));
|
||||
|
||||
Future<void> tapDateCellInRowDetailPage() async {
|
||||
final findDateCell = find.byType(EditableDateCell);
|
||||
@ -630,25 +590,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> duplicateRowInRowDetailPage() async {
|
||||
final duplicateButton = find.byType(RowDetailPageDuplicateButton);
|
||||
await tapButton(duplicateButton);
|
||||
}
|
||||
|
||||
Future<void> deleteRowInRowDetailPage() async {
|
||||
final deleteButton = find.byType(RowDetailPageDeleteButton);
|
||||
await tapButton(deleteButton);
|
||||
}
|
||||
|
||||
Future<TestGesture> hoverOnFieldInRowDetail({required int index}) async {
|
||||
final fieldButtons = find.byType(FieldCellButton);
|
||||
final button = find
|
||||
.descendant(of: find.byType(RowDetailPage), matching: fieldButtons)
|
||||
.at(index);
|
||||
return startGesture(
|
||||
getCenter(button),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
return startGesture(getCenter(button), kind: PointerDeviceKind.mouse);
|
||||
}
|
||||
|
||||
Future<void> reorderFieldInRowDetail({required double offset}) async {
|
||||
@ -657,11 +604,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
(widget) => widget is ReorderableDragStartListener && widget.enabled,
|
||||
)
|
||||
.first;
|
||||
await drag(
|
||||
thumb,
|
||||
Offset(0, offset),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
);
|
||||
await drag(thumb, Offset(0, offset), kind: PointerDeviceKind.mouse);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
@ -681,8 +624,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
Future<void> tapDeletePropertyInFieldEditor() async {
|
||||
final deleteButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell && widget.action == FieldAction.delete,
|
||||
(w) => w is FieldActionCell && w.action == FieldAction.delete,
|
||||
);
|
||||
await tapButton(deleteButton);
|
||||
|
||||
@ -693,11 +635,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(confirmButton);
|
||||
}
|
||||
|
||||
Future<void> scrollGridByOffset(Offset offset) async {
|
||||
await drag(find.byType(GridPage), offset);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> scrollRowDetailByOffset(Offset offset) async {
|
||||
await drag(find.byType(RowDetailPage), offset);
|
||||
await pumpAndSettle();
|
||||
@ -756,8 +693,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
/// Should call [tapGridFieldWithName] first.
|
||||
Future<void> tapDeletePropertyButton() async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell && widget.action == FieldAction.delete,
|
||||
(w) => w is FieldActionCell && w.action == FieldAction.delete,
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
@ -765,9 +701,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
/// A SimpleDialog must be shown first, e.g. when deleting a field.
|
||||
Future<void> tapDialogOkButton() async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is PrimaryTextButton &&
|
||||
widget.label == LocaleKeys.button_ok.tr(),
|
||||
(w) => w is PrimaryTextButton && w.label == LocaleKeys.button_ok.tr(),
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
@ -775,8 +709,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
/// Should call [tapGridFieldWithName] first.
|
||||
Future<void> tapDuplicatePropertyButton() async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell && widget.action == FieldAction.duplicate,
|
||||
(w) => w is FieldActionCell && w.action == FieldAction.duplicate,
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
@ -798,45 +731,34 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
/// Should call [tapGridFieldWithName] first.
|
||||
Future<void> tapHidePropertyButton() async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell &&
|
||||
widget.action == FieldAction.toggleVisibility,
|
||||
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
|
||||
);
|
||||
await tapButton(field);
|
||||
}
|
||||
|
||||
Future<void> tapHidePropertyButtonInFieldEditor() async {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldActionCell &&
|
||||
widget.action == FieldAction.toggleVisibility,
|
||||
(w) => w is FieldActionCell && w.action == FieldAction.toggleVisibility,
|
||||
);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapRowDetailPageRowActionButton() async {
|
||||
await tapButton(find.byType(RowActionButton));
|
||||
}
|
||||
Future<void> tapRowDetailPageRowActionButton() async =>
|
||||
tapButton(find.byType(RowActionButton));
|
||||
|
||||
Future<void> tapRowDetailPageCreatePropertyButton() async {
|
||||
await tapButton(find.byType(CreateRowFieldButton));
|
||||
}
|
||||
Future<void> tapRowDetailPageCreatePropertyButton() async =>
|
||||
tapButton(find.byType(CreateRowFieldButton));
|
||||
|
||||
Future<void> tapRowDetailPageDeleteRowButton() async {
|
||||
await tapButton(find.byType(RowDetailPageDeleteButton));
|
||||
}
|
||||
Future<void> tapRowDetailPageDeleteRowButton() async =>
|
||||
tapButton(find.byType(RowDetailPageDeleteButton));
|
||||
|
||||
Future<void> tapRowDetailPageDuplicateRowButton() async {
|
||||
await tapButton(find.byType(RowDetailPageDuplicateButton));
|
||||
}
|
||||
Future<void> tapRowDetailPageDuplicateRowButton() async =>
|
||||
tapButton(find.byType(RowDetailPageDuplicateButton));
|
||||
|
||||
Future<void> tapSwitchFieldTypeButton() async {
|
||||
await tapButton(find.byType(SwitchFieldButton));
|
||||
}
|
||||
Future<void> tapSwitchFieldTypeButton() async =>
|
||||
tapButton(find.byType(SwitchFieldButton));
|
||||
|
||||
Future<void> tapEscButton() async {
|
||||
await sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
}
|
||||
Future<void> tapEscButton() async => sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
/// Must call [tapSwitchFieldTypeButton] first.
|
||||
Future<void> selectFieldType(FieldType fieldType) async {
|
||||
@ -851,15 +773,13 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
// Use in edit mode of FieldEditor
|
||||
void expectEmptyTypeOptionEditor() {
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(FieldTypeOptionEditor),
|
||||
matching: find.byType(TypeOptionSeparator),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
}
|
||||
void expectEmptyTypeOptionEditor() => expect(
|
||||
find.descendant(
|
||||
of: find.byType(FieldTypeOptionEditor),
|
||||
matching: find.byType(TypeOptionSeparator),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
/// Each field has its own cell, so we can find the corresponding cell by
|
||||
/// the field type after create a new field.
|
||||
@ -868,10 +788,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(finder, findsWidgets);
|
||||
}
|
||||
|
||||
Future<void> assertNumberOfFieldsInGridPage(int num) async {
|
||||
expect(find.byType(GridFieldCell), findsNWidgets(num));
|
||||
}
|
||||
|
||||
Future<void> assertNumberOfRowsInGridPage(int num) async {
|
||||
expect(
|
||||
find.byType(GridRow, skipOffstage: false),
|
||||
@ -884,14 +800,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
|
||||
/// Check the field type of the [FieldCellButton] is the same as the name.
|
||||
Future<void> assertFieldTypeWithFieldName(
|
||||
String name,
|
||||
FieldType fieldType,
|
||||
) async {
|
||||
Future<void> assertFieldTypeWithFieldName(String name, FieldType type) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is FieldCellButton &&
|
||||
widget.field.fieldType == fieldType &&
|
||||
widget.field.fieldType == type &&
|
||||
widget.field.name == name,
|
||||
);
|
||||
|
||||
@ -936,11 +849,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Future<void> findFieldEditor(dynamic matcher) async {
|
||||
final finder = find.byType(FieldEditor);
|
||||
expect(finder, matcher);
|
||||
}
|
||||
|
||||
Future<void> findDateEditor(dynamic matcher) async {
|
||||
final finder = find.byType(DateCellEditor);
|
||||
expect(finder, matcher);
|
||||
@ -994,41 +902,29 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(find.byType(SortButton));
|
||||
}
|
||||
|
||||
Future<void> tapCreateFilterByFieldType(
|
||||
FieldType fieldType,
|
||||
String title,
|
||||
) async {
|
||||
Future<void> tapCreateFilterByFieldType(FieldType type, String title) async {
|
||||
final findFilter = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is GridFilterPropertyCell &&
|
||||
widget.fieldInfo.fieldType == fieldType &&
|
||||
widget.fieldInfo.fieldType == type &&
|
||||
widget.fieldInfo.name == title,
|
||||
);
|
||||
|
||||
await tapButton(findFilter);
|
||||
}
|
||||
|
||||
Future<void> tapFilterButtonInGrid(String filterName) async {
|
||||
Future<void> tapFilterButtonInGrid(String name) async {
|
||||
final findFilter = find.byType(FilterMenuItem);
|
||||
final button = find.descendant(
|
||||
of: findFilter,
|
||||
matching: find.text(filterName),
|
||||
);
|
||||
|
||||
final button = find.descendant(of: findFilter, matching: find.text(name));
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapCreateSortByFieldType(
|
||||
FieldType fieldType,
|
||||
String title,
|
||||
) async {
|
||||
Future<void> tapCreateSortByFieldType(FieldType type, String title) async {
|
||||
final findSort = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is GridSortPropertyCell &&
|
||||
widget.fieldInfo.fieldType == fieldType &&
|
||||
widget.fieldInfo.fieldType == type &&
|
||||
widget.fieldInfo.name == title,
|
||||
);
|
||||
|
||||
await tapButton(findSort);
|
||||
}
|
||||
|
||||
@ -1085,10 +981,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
of: fromSortItem,
|
||||
matching: find.byType(ReorderableDragStartListener),
|
||||
);
|
||||
await drag(
|
||||
dragElement,
|
||||
getCenter(toSortItem) - getCenter(fromSortItem),
|
||||
);
|
||||
await drag(dragElement, getCenter(toSortItem) - getCenter(fromSortItem));
|
||||
await pumpAndSettle(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
@ -1166,15 +1059,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(findCell);
|
||||
}
|
||||
|
||||
Future<void> tapCheckedButtonOnCheckboxFilter() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(LocaleKeys.grid_checkboxFilter_isChecked.tr()),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapUnCheckedButtonOnCheckboxFilter() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
@ -1193,15 +1077,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapUnCompletedButtonOnChecklistFilter() async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(HoverButton),
|
||||
matching: find.text(LocaleKeys.grid_checklistFilter_isIncomplted.tr()),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Should call [tapDatabaseSettingButton] first.
|
||||
Future<void> tapViewPropertiesButton() async {
|
||||
final findSettingItem = find.byType(DatabaseSettingsList);
|
||||
@ -1252,16 +1127,8 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> tapFirstDayOfWeek() async {
|
||||
await tapButton(find.byType(FirstDayOfWeek));
|
||||
}
|
||||
|
||||
Future<void> tapFirstDayOfWeekStartFromSunday() async {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) => widget is StartFromButton && widget.dayIndex == 0,
|
||||
);
|
||||
await tapButton(finder);
|
||||
}
|
||||
Future<void> tapFirstDayOfWeek() async =>
|
||||
tapButton(find.byType(FirstDayOfWeek));
|
||||
|
||||
Future<void> tapFirstDayOfWeekStartFromMonday() async {
|
||||
final finder = find.byWidgetPredicate(
|
||||
@ -1277,20 +1144,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
void assertFirstDayOfWeekStartFromMonday() {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is StartFromButton &&
|
||||
widget.dayIndex == 1 &&
|
||||
widget.isSelected == true,
|
||||
(w) => w is StartFromButton && w.dayIndex == 1 && w.isSelected == true,
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
|
||||
void assertFirstDayOfWeekStartFromSunday() {
|
||||
final finder = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is StartFromButton &&
|
||||
widget.dayIndex == 0 &&
|
||||
widget.isSelected == true,
|
||||
(w) => w is StartFromButton && w.dayIndex == 0 && w.isSelected == true,
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
@ -1307,11 +1168,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
),
|
||||
)
|
||||
.first;
|
||||
await scrollUntilVisible(
|
||||
todayCell,
|
||||
300,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await scrollUntilVisible(todayCell, 300, scrollable: scrollable);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
@ -1351,12 +1208,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
String? title,
|
||||
}) {
|
||||
final findDayCell = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is CalendarDayCard &&
|
||||
isSameDay(
|
||||
widget.date,
|
||||
date,
|
||||
),
|
||||
(widget) => widget is CalendarDayCard && isSameDay(widget.date, date),
|
||||
);
|
||||
Finder findEvents = find.descendant(
|
||||
of: findDayCell,
|
||||
@ -1390,13 +1242,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(cards.at(index));
|
||||
}
|
||||
|
||||
void assertEventEditorOpen() {
|
||||
expect(find.byType(CalendarEventEditor), findsOneWidget);
|
||||
}
|
||||
void assertEventEditorOpen() =>
|
||||
expect(find.byType(CalendarEventEditor), findsOneWidget);
|
||||
|
||||
Future<void> dismissEventEditor() async {
|
||||
await simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
}
|
||||
Future<void> dismissEventEditor() async =>
|
||||
simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
|
||||
Future<void> editEventTitle(String title) async {
|
||||
final textField = find.descendant(
|
||||
@ -1507,10 +1357,9 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
if (isVisible) {
|
||||
expect(textField, findsOneWidget);
|
||||
} else {
|
||||
expect(textField, findsNothing);
|
||||
return expect(textField, findsOneWidget);
|
||||
}
|
||||
expect(textField, findsNothing);
|
||||
}
|
||||
|
||||
Future<void> enterNewGroupName(String name, {required bool submit}) async {
|
||||
@ -1612,21 +1461,14 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
await tapButton(okButton);
|
||||
}
|
||||
|
||||
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) {
|
||||
switch (layout) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
expect(find.byType(BoardPage), findsOneWidget);
|
||||
break;
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
expect(find.byType(CalendarPage), findsOneWidget);
|
||||
break;
|
||||
case DatabaseLayoutPB.Grid:
|
||||
expect(find.byType(GridPage), findsOneWidget);
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unknown database layout type: $layout');
|
||||
}
|
||||
}
|
||||
void assertCurrentDatabaseTagIs(DatabaseLayoutPB layout) => switch (layout) {
|
||||
DatabaseLayoutPB.Board =>
|
||||
expect(find.byType(BoardPage), findsOneWidget),
|
||||
DatabaseLayoutPB.Calendar =>
|
||||
expect(find.byType(CalendarPage), findsOneWidget),
|
||||
DatabaseLayoutPB.Grid => expect(find.byType(GridPage), findsOneWidget),
|
||||
_ => throw Exception('Unknown database layout type: $layout'),
|
||||
};
|
||||
|
||||
Future<void> selectDatabaseLayoutType(DatabaseLayoutPB layout) async {
|
||||
final findLayoutCell = find.byType(DatabaseViewLayoutCell);
|
||||
@ -1634,11 +1476,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
(widget) => widget is FlowyText && widget.text == layout.layoutName,
|
||||
);
|
||||
|
||||
final button = find.descendant(
|
||||
of: findLayoutCell,
|
||||
matching: findText,
|
||||
);
|
||||
|
||||
final button = find.descendant(of: findLayoutCell, matching: findText);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
@ -1660,8 +1498,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
|
||||
await tapButton(
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is NumberFormatCell && widget.format == NumberFormatPB.USD,
|
||||
(w) => w is NumberFormatCell && w.format == NumberFormatPB.USD,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -1675,8 +1512,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
String fieldName,
|
||||
) async {
|
||||
final field = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is DatabasePropertyCell && widget.fieldInfo.name == fieldName,
|
||||
(w) => w is DatabasePropertyCell && w.fieldInfo.name == fieldName,
|
||||
);
|
||||
final toggleVisibilityButton =
|
||||
find.descendant(of: field, matching: find.byType(FlowyIconButton));
|
||||
@ -1684,18 +1520,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) {
|
||||
switch (layout) {
|
||||
case DatabaseLayoutPB.Board:
|
||||
return find.byType(BoardPage);
|
||||
case DatabaseLayoutPB.Calendar:
|
||||
return find.byType(CalendarPage);
|
||||
case DatabaseLayoutPB.Grid:
|
||||
return find.byType(GridPage);
|
||||
default:
|
||||
throw Exception('Unknown database layout type: $layout');
|
||||
}
|
||||
}
|
||||
Finder finderForDatabaseLayoutType(DatabaseLayoutPB layout) => switch (layout) {
|
||||
DatabaseLayoutPB.Board => find.byType(BoardPage),
|
||||
DatabaseLayoutPB.Calendar => find.byType(CalendarPage),
|
||||
DatabaseLayoutPB.Grid => find.byType(GridPage),
|
||||
_ => throw Exception('Unknown database layout type: $layout'),
|
||||
};
|
||||
|
||||
Finder finderForFieldType(FieldType fieldType) {
|
||||
switch (fieldType) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
Future<void> deleteDirectoriesWithSameBaseNameAsPrefix(
|
||||
String path,
|
||||
|
@ -30,11 +30,8 @@ class EditorOperations {
|
||||
|
||||
final WidgetTester tester;
|
||||
|
||||
EditorState getCurrentEditorState() {
|
||||
return tester
|
||||
.widget<AppFlowyEditor>(find.byType(AppFlowyEditor))
|
||||
.editorState;
|
||||
}
|
||||
EditorState getCurrentEditorState() =>
|
||||
tester.widget<AppFlowyEditor>(find.byType(AppFlowyEditor)).editorState;
|
||||
|
||||
/// Tap the line of editor at [index]
|
||||
Future<void> tapLineOfEditorAt(int index) async {
|
||||
@ -144,16 +141,8 @@ class EditorOperations {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> switchNetworkImageCover(String imageUrl) async {
|
||||
final image = find.byWidgetPredicate(
|
||||
(widget) => widget is ImageGridItem,
|
||||
);
|
||||
await tester.tapButton(image);
|
||||
}
|
||||
|
||||
Future<void> tapOnRemoveCover() async {
|
||||
await tester.tapButton(find.byType(DeleteCoverButton));
|
||||
}
|
||||
Future<void> tapOnRemoveCover() async =>
|
||||
tester.tapButton(find.byType(DeleteCoverButton));
|
||||
|
||||
/// A cover must be present in the document to function properly since this
|
||||
/// catches all cover types collectively
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
@ -12,7 +14,6 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@ -89,18 +90,6 @@ extension Expectation on WidgetTester {
|
||||
expect(exportSuccess, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see the add button and icon button in the cover toolbar
|
||||
void expectToSeePluginAddCoverAndIconButton() {
|
||||
final addCover = find.textContaining(
|
||||
LocaleKeys.document_plugins_cover_addCover.tr(),
|
||||
);
|
||||
final addIcon = find.textContaining(
|
||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||
);
|
||||
expect(addCover, findsOneWidget);
|
||||
expect(addIcon, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see the document header toolbar empty
|
||||
void expectToSeeEmptyDocumentHeaderToolbar() {
|
||||
final addCover = find.textContaining(
|
||||
@ -153,14 +142,6 @@ extension Expectation on WidgetTester {
|
||||
expect(findRemoveIcon, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see the user name on the home page
|
||||
void expectToSeeUserName(String name) {
|
||||
final userName = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowyText && widget.text == name,
|
||||
);
|
||||
expect(userName, findsOneWidget);
|
||||
}
|
||||
|
||||
/// Expect to see a text
|
||||
void expectToSeeText(String text) {
|
||||
Finder textWidget = find.textContaining(text, findRichText: true);
|
||||
@ -178,26 +159,23 @@ extension Expectation on WidgetTester {
|
||||
ViewLayoutPB layout = ViewLayoutPB.Document,
|
||||
String? parentName,
|
||||
ViewLayoutPB parentLayout = ViewLayoutPB.Document,
|
||||
}) {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SingleInnerViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite &&
|
||||
widget.view.name == name &&
|
||||
widget.view.layout == layout,
|
||||
skipOffstage: false,
|
||||
);
|
||||
}
|
||||
}) =>
|
||||
find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SingleInnerViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite &&
|
||||
widget.view.name == name &&
|
||||
widget.view.layout == layout,
|
||||
skipOffstage: false,
|
||||
);
|
||||
|
||||
Finder findAllFavoritePages() {
|
||||
return find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SingleInnerViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
);
|
||||
}
|
||||
Finder findAllFavoritePages() => find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is SingleInnerViewItem &&
|
||||
widget.view.isFavorite &&
|
||||
widget.categoryType == FolderCategoryType.favorite,
|
||||
);
|
||||
|
||||
Finder findPageName(
|
||||
String name, {
|
||||
|
@ -11,9 +11,7 @@ class MockFilePicker implements FilePickerService {
|
||||
final List<String> mockPaths;
|
||||
|
||||
@override
|
||||
Future<String?> getDirectoryPath({String? title}) {
|
||||
return Future.value(mockPath);
|
||||
}
|
||||
Future<String?> getDirectoryPath({String? title}) => Future.value(mockPath);
|
||||
|
||||
@override
|
||||
Future<String?> saveFile({
|
||||
@ -23,9 +21,8 @@ class MockFilePicker implements FilePickerService {
|
||||
FileType type = FileType.any,
|
||||
List<String>? allowedExtensions,
|
||||
bool lockParentWindow = false,
|
||||
}) {
|
||||
return Future.value(mockPath);
|
||||
}
|
||||
}) =>
|
||||
Future.value(mockPath);
|
||||
|
||||
@override
|
||||
Future<FilePickerResult?> pickFiles({
|
||||
@ -42,34 +39,21 @@ class MockFilePicker implements FilePickerService {
|
||||
}) {
|
||||
final platformFiles =
|
||||
mockPaths.map((e) => PlatformFile(path: e, name: '', size: 0)).toList();
|
||||
return Future.value(
|
||||
FilePickerResult(
|
||||
platformFiles,
|
||||
),
|
||||
);
|
||||
return Future.value(FilePickerResult(platformFiles));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mockGetDirectoryPath(
|
||||
String path,
|
||||
) async {
|
||||
Future<void> mockGetDirectoryPath(String path) async {
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPath: path,
|
||||
),
|
||||
() => MockFilePicker(mockPath: path),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<String> mockSaveFilePath(
|
||||
String path,
|
||||
) async {
|
||||
Future<String> mockSaveFilePath(String path) async {
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPath: path,
|
||||
),
|
||||
() => MockFilePicker(mockPath: path),
|
||||
);
|
||||
return path;
|
||||
}
|
||||
@ -77,9 +61,7 @@ Future<String> mockSaveFilePath(
|
||||
List<String> mockPickFilePaths({required List<String> paths}) {
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPaths: paths,
|
||||
),
|
||||
() => MockFilePicker(mockPaths: paths),
|
||||
);
|
||||
return paths;
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MyMockClient extends Mock implements http.Client {
|
||||
@override
|
||||
@ -52,7 +53,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
|
||||
final request = http.Request('POST', OpenAIRequestType.textCompletion.uri);
|
||||
final response = await client.send(request);
|
||||
|
||||
var previousSyntax = '';
|
||||
String previousSyntax = '';
|
||||
if (response.statusCode == 200) {
|
||||
await for (final chunk in response.stream
|
||||
.transform(const Utf8Decoder())
|
||||
@ -76,6 +77,5 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,7 @@ class MockUrlLauncher extends Fake
|
||||
bool launchCalled = false;
|
||||
|
||||
// ignore: use_setters_to_change_properties
|
||||
void setCanLaunchExpectations(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
void setCanLaunchExpectations(String url) => this.url = url;
|
||||
|
||||
void setLaunchExpectations({
|
||||
required String url,
|
||||
@ -53,10 +51,7 @@ class MockUrlLauncher extends Fake
|
||||
this.webOnlyWindowName = webOnlyWindowName;
|
||||
}
|
||||
|
||||
// ignore: use_setters_to_change_properties
|
||||
void setResponse(bool response) {
|
||||
this.response = response;
|
||||
}
|
||||
void setResponse(bool response) => this.response = response;
|
||||
|
||||
@override
|
||||
LinkDelegate? get linkDelegate => null;
|
||||
@ -104,7 +99,5 @@ class MockUrlLauncher extends Fake
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> closeWebView() async {
|
||||
closeWebViewCalled = true;
|
||||
}
|
||||
Future<void> closeWebView() async => closeWebViewCalled = true;
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../desktop/board/board_hide_groups_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
extension AppFlowySettings on WidgetTester {
|
||||
@ -31,14 +35,6 @@ extension AppFlowySettings on WidgetTester {
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> expectNoSettingsPage(SettingsPage page) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is SettingsMenuElement && widget.page == page,
|
||||
);
|
||||
expect(button, findsNothing);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Restore the AppFlowy data storage location
|
||||
Future<void> restoreLocation() async {
|
||||
final button =
|
||||
@ -48,13 +44,6 @@ extension AppFlowySettings on WidgetTester {
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapOpenFolderButton() async {
|
||||
final button = find.text(LocaleKeys.settings_files_open.tr());
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapCustomLocationButton() async {
|
||||
final button = find.byTooltip(
|
||||
LocaleKeys.settings_files_changeLocationTooltips.tr(),
|
||||
@ -66,12 +55,22 @@ extension AppFlowySettings on WidgetTester {
|
||||
|
||||
/// Enter user name
|
||||
Future<void> enterUserName(String name) async {
|
||||
final uni = find.byType(UserNameInput);
|
||||
expect(uni, findsOneWidget);
|
||||
await tap(uni);
|
||||
await enterText(uni, name);
|
||||
await wait(300); //
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
// Enable editing username
|
||||
final editUsernameFinder = find.descendant(
|
||||
of: find.byType(UserProfileSetting),
|
||||
matching: find.byFlowySvg(FlowySvgs.edit_s),
|
||||
);
|
||||
await tap(editUsernameFinder);
|
||||
await pumpAndSettle();
|
||||
|
||||
final userNameFinder = find.descendant(
|
||||
of: find.byType(UserProfileSetting),
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
await enterText(userNameFinder, name);
|
||||
await pumpAndSettle();
|
||||
|
||||
await tap(find.text(LocaleKeys.button_save.tr()));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,10 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/core/notification/notification_helper.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value
|
||||
const String _source = 'Document';
|
||||
|
||||
typedef DocumentNotificationCallback = void Function(
|
||||
DocumentNotification,
|
||||
FlowyResult<Uint8List, FlowyError>,
|
||||
);
|
||||
|
||||
class DocumentNotificationParser
|
||||
extends NotificationParser<DocumentNotification, FlowyError> {
|
||||
DocumentNotificationParser({
|
||||
|
@ -12,12 +12,6 @@ import 'notification_helper.dart';
|
||||
// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value
|
||||
const String _source = 'Workspace';
|
||||
|
||||
// Folder
|
||||
typedef FolderNotificationCallback = void Function(
|
||||
FolderNotification,
|
||||
FlowyResult<Uint8List, FlowyError>,
|
||||
);
|
||||
|
||||
class FolderNotificationParser
|
||||
extends NotificationParser<FolderNotification, FlowyError> {
|
||||
FolderNotificationParser({
|
||||
|
@ -12,12 +12,6 @@ import 'notification_helper.dart';
|
||||
// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value
|
||||
const String _source = 'Database';
|
||||
|
||||
// DatabasePB
|
||||
typedef DatabaseNotificationCallback = void Function(
|
||||
DatabaseNotification,
|
||||
FlowyResult<Uint8List, FlowyError>,
|
||||
);
|
||||
|
||||
class DatabaseNotificationParser
|
||||
extends NotificationParser<DatabaseNotification, FlowyError> {
|
||||
DatabaseNotificationParser({
|
||||
|
@ -13,11 +13,6 @@ import 'notification_helper.dart';
|
||||
// This value must be identical to the value in the backend (SEARCH_OBSERVABLE_SOURCE)
|
||||
const _source = 'Search';
|
||||
|
||||
typedef SearchNotificationCallback = void Function(
|
||||
SearchNotification,
|
||||
FlowyResult<Uint8List, FlowyError>,
|
||||
);
|
||||
|
||||
class SearchNotificationParser
|
||||
extends NotificationParser<SearchNotification, FlowyError> {
|
||||
SearchNotificationParser({
|
||||
|
@ -1,23 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_backend/rust_stream.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import 'notification_helper.dart';
|
||||
|
||||
// This value should be the same as the USER_OBSERVABLE_SOURCE value
|
||||
const String _source = 'User';
|
||||
|
||||
// User
|
||||
typedef UserNotificationCallback = void Function(
|
||||
UserNotification,
|
||||
FlowyResult<Uint8List, FlowyError>,
|
||||
);
|
||||
|
||||
class UserNotificationParser
|
||||
extends NotificationParser<UserNotification, FlowyError> {
|
||||
UserNotificationParser({
|
||||
@ -29,26 +17,3 @@ class UserNotificationParser
|
||||
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
|
||||
);
|
||||
}
|
||||
|
||||
typedef UserNotificationHandler = Function(
|
||||
UserNotification ty,
|
||||
FlowyResult<Uint8List, FlowyError> result,
|
||||
);
|
||||
|
||||
class UserNotificationListener {
|
||||
UserNotificationListener({
|
||||
required String objectId,
|
||||
required UserNotificationHandler handler,
|
||||
}) : _parser = UserNotificationParser(id: objectId, callback: handler) {
|
||||
_subscription =
|
||||
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
|
||||
}
|
||||
|
||||
UserNotificationParser? _parser;
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
|
||||
Future<void> stop() async {
|
||||
_parser = null;
|
||||
await _subscription?.cancel();
|
||||
}
|
||||
}
|
||||
|
@ -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/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@ -6,14 +8,13 @@ import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bott
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/built_in_svgs.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
|
@ -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';
|
||||
|
||||
abstract class IGridCellDataConfig {
|
||||
// The cell data will reload if it receives the field's change notification.
|
||||
bool get reloadOnFieldChanged;
|
||||
}
|
||||
|
||||
abstract class CellDataParser<T> {
|
||||
T? parserData(List<int> data);
|
||||
}
|
||||
|
@ -31,8 +31,6 @@ typedef OnNumOfRowsChanged = void Function(
|
||||
ChangedReason reason,
|
||||
);
|
||||
|
||||
typedef OnError = void Function(FlowyError);
|
||||
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.idle() = _Idle;
|
||||
|
@ -1,13 +1,15 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database/application/setting/setting_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_settings_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/field_settings_service.dart';
|
||||
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/filter_service.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database/application/setting/setting_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/sort_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/sort_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
|
||||
@ -17,9 +19,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../setting/setting_service.dart';
|
||||
|
||||
import 'field_info.dart';
|
||||
|
||||
class _GridFieldNotifier extends ChangeNotifier {
|
||||
@ -73,7 +75,7 @@ typedef OnReceiveField = void Function(FieldInfo);
|
||||
typedef OnReceiveFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
||||
typedef OnReceiveSorts = void Function(List<SortInfo>);
|
||||
typedef OnReceiveFieldSettings = void Function(List<FieldInfo>);
|
||||
|
||||
|
||||
class FieldController {
|
||||
FieldController({required this.viewId})
|
||||
|
@ -4,13 +4,7 @@ abstract class TypeOptionParser<T> {
|
||||
T fromBuffer(List<int> buffer);
|
||||
}
|
||||
|
||||
class RichTextTypeOptionDataParser
|
||||
extends TypeOptionParser<RichTextTypeOptionPB> {
|
||||
@override
|
||||
RichTextTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return RichTextTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
|
||||
@override
|
||||
@ -19,21 +13,6 @@ class NumberTypeOptionDataParser extends TypeOptionParser<NumberTypeOptionPB> {
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxTypeOptionDataParser
|
||||
extends TypeOptionParser<CheckboxTypeOptionPB> {
|
||||
@override
|
||||
CheckboxTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return CheckboxTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class URLTypeOptionDataParser extends TypeOptionParser<URLTypeOptionPB> {
|
||||
@override
|
||||
URLTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return URLTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
|
||||
@override
|
||||
DateTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
@ -65,14 +44,6 @@ class MultiSelectTypeOptionDataParser
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistTypeOptionDataParser
|
||||
extends TypeOptionParser<ChecklistTypeOptionPB> {
|
||||
@override
|
||||
ChecklistTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return ChecklistTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class RelationTypeOptionDataParser
|
||||
extends TypeOptionParser<RelationTypeOptionPB> {
|
||||
@override
|
||||
|
@ -9,8 +9,6 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
typedef OnGroupError = void Function(FlowyError);
|
||||
|
||||
abstract class GroupControllerDelegate {
|
||||
bool hasGroup(String groupId);
|
||||
void removeRow(GroupPB group, RowId rowId);
|
||||
|
@ -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
|
||||
class CalendarDayEvent with _$CalendarDayEvent {
|
||||
const factory CalendarDayEvent({
|
||||
|
@ -8,8 +8,6 @@ import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'calendar_setting_bloc.freezed.dart';
|
||||
|
||||
typedef DayOfWeek = int;
|
||||
|
||||
class CalendarSettingBloc
|
||||
extends Bloc<CalendarSettingEvent, CalendarSettingState> {
|
||||
CalendarSettingBloc({required DatabaseController databaseController})
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@ -19,12 +21,12 @@ import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../application/row/row_controller.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
|
||||
import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
@ -265,6 +267,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
fillColor: Colors.transparent,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
fontColor: AFThemeExtension.of(context).textColor,
|
||||
tooltip: LocaleKeys.calendar_navigation_jumpToday.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
|
@ -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:flowy_infra/notifier.dart';
|
||||
|
||||
typedef GroupConfigurationUpdateValue
|
||||
= FlowyResult<List<GroupSettingPB>, FlowyError>;
|
||||
typedef GroupUpdateValue = FlowyResult<GroupChangesPB, 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/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
@ -8,14 +9,9 @@ import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/mobile_cell_container.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../layout/sizes.dart';
|
||||
import "package:appflowy/generated/locale_keys.g.dart";
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class MobileGridRow extends StatefulWidget {
|
||||
const MobileGridRow({
|
||||
@ -90,26 +86,6 @@ class _MobileGridRowState extends State<MobileGridRow> {
|
||||
}
|
||||
}
|
||||
|
||||
class InsertRowButton extends StatelessWidget {
|
||||
const InsertRowButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
tooltipText: LocaleKeys.tooltip_addNewRow.tr(),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 20,
|
||||
height: 30,
|
||||
onPressed: () => context.read<RowBloc>().add(const RowEvent.createRow()),
|
||||
iconPadding: const EdgeInsets.all(3),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RowContent extends StatelessWidget {
|
||||
const RowContent({
|
||||
super.key,
|
||||
|
@ -23,27 +23,12 @@ Map<ShortcutActivator, Intent> bindKeys(List<LogicalKeyboardKey> keys) {
|
||||
return {for (final key in keys) LogicalKeySet(key): KeyboardKeyIdent(key)};
|
||||
}
|
||||
|
||||
Map<Type, Action<Intent>> bindActions() {
|
||||
return {
|
||||
KeyboardKeyIdent: KeyboardBindingAction(),
|
||||
};
|
||||
}
|
||||
|
||||
class KeyboardKeyIdent extends Intent {
|
||||
const KeyboardKeyIdent(this.key);
|
||||
|
||||
final KeyboardKey key;
|
||||
}
|
||||
|
||||
class KeyboardBindingAction extends Action<KeyboardKeyIdent> {
|
||||
KeyboardBindingAction();
|
||||
|
||||
@override
|
||||
void invoke(covariant KeyboardKeyIdent intent) {
|
||||
// print(intent);
|
||||
}
|
||||
}
|
||||
|
||||
class LoggingActionDispatcher extends ActionDispatcher {
|
||||
@override
|
||||
Object? invokeAction(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
|
||||
enum AccessoryType {
|
||||
edit,
|
||||
more,
|
||||
@ -11,10 +12,6 @@ abstract mixin class CardAccessory implements Widget {
|
||||
void onTap(BuildContext context) {}
|
||||
}
|
||||
|
||||
typedef CardAccessoryBuilder = List<CardAccessory> Function(
|
||||
BuildContext buildContext,
|
||||
);
|
||||
|
||||
class CardAccessoryContainer extends StatelessWidget {
|
||||
const CardAccessoryContainer({
|
||||
super.key,
|
||||
|
@ -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/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart';
|
||||
@ -8,7 +11,6 @@ import 'package:appflowy/plugins/database/domain/database_view_service.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
|
||||
import 'package:appflowy/plugins/database_document/database_document_plugin.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
@ -19,10 +21,8 @@ import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/em
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
typedef OnSubmittedEmoji = void Function(String emoji);
|
||||
const _kBannerActionHeight = 40.0;
|
||||
|
||||
class RowBanner extends StatefulWidget {
|
||||
|
@ -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 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
const String kLocalImagesKey = 'local_images';
|
||||
|
||||
@ -22,302 +11,6 @@ List<String> get builtInAssetImages => [
|
||||
"assets/images/app_flowy_abstract_cover_2.jpg",
|
||||
];
|
||||
|
||||
class ChangeCoverPopover extends StatefulWidget {
|
||||
const ChangeCoverPopover({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
required this.node,
|
||||
required this.onCoverChanged,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final Node node;
|
||||
final Function(
|
||||
CoverType selectionType,
|
||||
String selection,
|
||||
) onCoverChanged;
|
||||
|
||||
@override
|
||||
State<ChangeCoverPopover> createState() => _ChangeCoverPopoverState();
|
||||
}
|
||||
|
||||
class _ChangeCoverPopoverState extends State<ChangeCoverPopover> {
|
||||
bool isAddingImage = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ChangeCoverPopoverBloc(
|
||||
editorState: widget.editorState,
|
||||
node: widget.node,
|
||||
)..add(const ChangeCoverPopoverEvent.fetchPickedImagePaths()),
|
||||
child: BlocConsumer<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
|
||||
listener: (context, state) {
|
||||
if (state is Loaded && state.selectLatestImage) {
|
||||
widget.onCoverChanged(
|
||||
CoverType.file,
|
||||
state.imageNames.last,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: SingleChildScrollView(
|
||||
child: isAddingImage
|
||||
? CoverImagePicker(
|
||||
onBackPressed: () => setState(() {
|
||||
isAddingImage = false;
|
||||
}),
|
||||
onFileSubmit: (_) {
|
||||
context.read<ChangeCoverPopoverBloc>().add(
|
||||
const ChangeCoverPopoverEvent
|
||||
.fetchPickedImagePaths(
|
||||
selectLatestImage: true,
|
||||
),
|
||||
);
|
||||
|
||||
setState(() => isAddingImage = false);
|
||||
},
|
||||
)
|
||||
: _buildCoverSelection(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCoverSelection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.document_plugins_cover_colors.tr(),
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
const VSpace(10),
|
||||
_buildColorPickerList(),
|
||||
const VSpace(10),
|
||||
_buildImageHeader(),
|
||||
const VSpace(10),
|
||||
_buildFileImagePicker(),
|
||||
const VSpace(10),
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.document_plugins_cover_abstract.tr(),
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
const VSpace(10),
|
||||
_buildAbstractImagePicker(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageHeader() {
|
||||
return BlocBuilder<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
|
||||
builder: (context, state) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.document_plugins_cover_images.tr(),
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
FlowyTextButton(
|
||||
fillColor: Theme.of(context).cardColor,
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
LocaleKeys.document_plugins_cover_clearAll.tr(),
|
||||
fontColor: Theme.of(context).colorScheme.tertiary,
|
||||
onPressed: () async {
|
||||
final hasFileImageCover = CoverType.fromString(
|
||||
widget.node.attributes[DocumentHeaderBlockKeys.coverType],
|
||||
) ==
|
||||
CoverType.file;
|
||||
final changeCoverBloc = context.read<ChangeCoverPopoverBloc>();
|
||||
if (hasFileImageCover) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DeleteImageAlertDialog(
|
||||
onSubmit: () {
|
||||
changeCoverBloc.add(
|
||||
const ChangeCoverPopoverEvent.clearAllImages(),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
context
|
||||
.read<ChangeCoverPopoverBloc>()
|
||||
.add(const ChangeCoverPopoverEvent.clearAllImages());
|
||||
}
|
||||
},
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAbstractImagePicker() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1 / 0.65,
|
||||
crossAxisSpacing: 7,
|
||||
mainAxisSpacing: 7,
|
||||
),
|
||||
itemCount: builtInAssetImages.length,
|
||||
itemBuilder: (BuildContext ctx, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
widget.onCoverChanged(
|
||||
CoverType.asset,
|
||||
builtInAssetImages[index],
|
||||
);
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(builtInAssetImages[index]),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildColorPickerList() {
|
||||
final theme = Theme.of(context);
|
||||
return CoverColorPicker(
|
||||
pickerBackgroundColor: theme.cardColor,
|
||||
pickerItemHoverColor: theme.hoverColor,
|
||||
selectedBackgroundColorHex:
|
||||
widget.node.attributes[DocumentHeaderBlockKeys.coverType] ==
|
||||
CoverType.color.toString()
|
||||
? widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]
|
||||
: 'ffffff',
|
||||
backgroundColorOptions:
|
||||
_generateBackgroundColorOptions(widget.editorState),
|
||||
onSubmittedBackgroundColorHex: (color) {
|
||||
widget.onCoverChanged(CoverType.color, color);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFileImagePicker() {
|
||||
return BlocBuilder<ChangeCoverPopoverBloc, ChangeCoverPopoverState>(
|
||||
builder: (context, state) {
|
||||
if (state is Loaded) {
|
||||
final List<String> images = state.imageNames;
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1 / 0.65,
|
||||
crossAxisSpacing: 7,
|
||||
mainAxisSpacing: 7,
|
||||
),
|
||||
itemCount: images.length + 1,
|
||||
itemBuilder: (BuildContext ctx, index) {
|
||||
if (index == 0) {
|
||||
return NewCustomCoverButton(
|
||||
onPressed: () => setState(
|
||||
() => isAddingImage = true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ImageGridItem(
|
||||
onImageSelect: () {
|
||||
widget.onCoverChanged(
|
||||
CoverType.file,
|
||||
images[index - 1],
|
||||
);
|
||||
},
|
||||
onImageDelete: () async {
|
||||
final changeCoverBloc =
|
||||
context.read<ChangeCoverPopoverBloc>();
|
||||
final deletingCurrentCover = widget.node
|
||||
.attributes[DocumentHeaderBlockKeys.coverDetails] ==
|
||||
images[index - 1];
|
||||
if (deletingCurrentCover) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DeleteImageAlertDialog(
|
||||
onSubmit: () {
|
||||
changeCoverBloc.add(
|
||||
ChangeCoverPopoverEvent.deleteImage(
|
||||
images[index - 1],
|
||||
),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
changeCoverBloc.add(DeleteImage(images[index - 1]));
|
||||
}
|
||||
},
|
||||
imagePath: images[index - 1],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<ColorOption> _generateBackgroundColorOptions(EditorState editorState) {
|
||||
return FlowyTint.values
|
||||
.map(
|
||||
(t) => ColorOption(
|
||||
colorHex: t.color(context).toHex(),
|
||||
name: t.tintName(AppFlowyEditorL10n.current),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class NewCustomCoverButton extends StatelessWidget {
|
||||
const NewCustomCoverButton({super.key, required this.onPressed});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
child: FlowyIconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.15),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorOption {
|
||||
const ColorOption({
|
||||
required this.colorHex,
|
||||
@ -398,122 +91,6 @@ class _CoverColorPickerState extends State<CoverColorPicker> {
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteImageAlertDialog extends StatelessWidget {
|
||||
const DeleteImageAlertDialog({
|
||||
super.key,
|
||||
required this.onSubmit,
|
||||
});
|
||||
|
||||
final Function() onSubmit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: FlowyText.semibold(
|
||||
"Image is used in cover",
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(minHeight: 100),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 20,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(LocaleKeys.document_plugins_cover_coverRemoveAlert).tr(),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
const Text(
|
||||
LocaleKeys.document_plugins_cover_alertDialogConfirmation,
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0,
|
||||
horizontal: 20.0,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text(LocaleKeys.button_cancel).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: onSubmit,
|
||||
child: const Text(LocaleKeys.button_ok).tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImageGridItem extends StatefulWidget {
|
||||
const ImageGridItem({
|
||||
super.key,
|
||||
required this.onImageSelect,
|
||||
required this.onImageDelete,
|
||||
required this.imagePath,
|
||||
});
|
||||
|
||||
final Function() onImageSelect;
|
||||
final Function() onImageDelete;
|
||||
final String imagePath;
|
||||
|
||||
@override
|
||||
State<ImageGridItem> createState() => _ImageGridItemState();
|
||||
}
|
||||
|
||||
class _ImageGridItemState extends State<ImageGridItem> {
|
||||
bool showDeleteButton = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) {
|
||||
setState(() {
|
||||
showDeleteButton = true;
|
||||
});
|
||||
},
|
||||
onExit: (_) {
|
||||
setState(() {
|
||||
showDeleteButton = false;
|
||||
});
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: widget.onImageSelect,
|
||||
child: ClipRRect(
|
||||
borderRadius: Corners.s8Border,
|
||||
child: Image.file(File(widget.imagePath), fit: BoxFit.cover),
|
||||
),
|
||||
),
|
||||
if (showDeleteButton)
|
||||
Positioned(
|
||||
right: 2,
|
||||
top: 2,
|
||||
child: FlowyIconButton(
|
||||
fillColor:
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0.8),
|
||||
hoverColor:
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0.8),
|
||||
iconPadding: const EdgeInsets.all(5),
|
||||
width: 28,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.delete_s,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
onPressed: widget.onImageDelete,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class ColorItem extends StatelessWidget {
|
||||
const ColorItem({
|
||||
|
@ -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:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_blocks_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
// convert the current block to other block types
|
||||
// only show in single selection and text type
|
||||
final mobileAddBlockToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, editorState, ___) {
|
||||
if (!onlyShowInSingleSelectionAndTextType(editorState)) {
|
||||
return null;
|
||||
}
|
||||
return const FlowySvg(
|
||||
FlowySvgs.add_m,
|
||||
size: Size.square(48),
|
||||
);
|
||||
},
|
||||
itemMenuBuilder: (_, editorState, service) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return null;
|
||||
}
|
||||
return BlocksMenu(
|
||||
items: _addBlockMenuItems,
|
||||
editorState: editorState,
|
||||
service: service,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final _addBlockMenuItems = [
|
||||
// paragraph
|
||||
BlockMenuItem(
|
||||
blockType: ParagraphBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_text_decoration_m),
|
||||
label: LocaleKeys.editor_text.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
paragraphNode(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// to-do list
|
||||
BlockMenuItem(
|
||||
blockType: TodoListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_checkbox_m),
|
||||
label: LocaleKeys.editor_checkbox.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
todoListNode(checked: false),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// heading 1 - 3
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h1_m),
|
||||
label: LocaleKeys.editor_heading1.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
headingNode(level: 1),
|
||||
);
|
||||
},
|
||||
),
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h2_m),
|
||||
label: LocaleKeys.editor_heading2.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
headingNode(level: 2),
|
||||
);
|
||||
},
|
||||
),
|
||||
BlockMenuItem(
|
||||
blockType: HeadingBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_h3_m),
|
||||
label: LocaleKeys.editor_heading3.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
headingNode(level: 3),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// bulleted list
|
||||
BlockMenuItem(
|
||||
blockType: BulletedListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_bulleted_list_m),
|
||||
label: LocaleKeys.editor_bulletedList.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
bulletedListNode(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// numbered list
|
||||
BlockMenuItem(
|
||||
blockType: NumberedListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_numbered_list_m),
|
||||
label: LocaleKeys.editor_numberedList.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
numberedListNode(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// toggle list
|
||||
BlockMenuItem(
|
||||
blockType: ToggleListBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_toggle_list_m),
|
||||
label: LocaleKeys.document_plugins_toggleList.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
toggleListBlockNode(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// quote
|
||||
BlockMenuItem(
|
||||
blockType: QuoteBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_quote_m),
|
||||
label: LocaleKeys.editor_quote.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
quoteNode(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// callout
|
||||
BlockMenuItem(
|
||||
blockType: CalloutBlockKeys.type,
|
||||
icon: const Icon(Icons.note_rounded),
|
||||
label: LocaleKeys.document_plugins_callout.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
calloutNode(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// code
|
||||
BlockMenuItem(
|
||||
blockType: CodeBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_code_m),
|
||||
label: LocaleKeys.document_selectionMenu_codeBlock.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertBlockOrReplaceCurrentBlock(
|
||||
selection,
|
||||
codeBlockNode(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// divider
|
||||
BlockMenuItem(
|
||||
blockType: DividerBlockKeys.type,
|
||||
icon: const FlowySvg(FlowySvgs.m_divider_m),
|
||||
label: LocaleKeys.editor_divider.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertDivider(selection);
|
||||
},
|
||||
),
|
||||
|
||||
// math equation
|
||||
BlockMenuItem(
|
||||
blockType: MathEquationBlockKeys.type,
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.math_lg,
|
||||
size: Size.square(22),
|
||||
),
|
||||
label: LocaleKeys.document_plugins_mathEquation_name.tr(),
|
||||
isSelected: _unSelectable,
|
||||
onTap: (editorState, selection, service) async {
|
||||
service.closeItemMenu();
|
||||
await editorState.insertMathEquation(selection);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
bool _unSelectable(
|
||||
EditorState editorState,
|
||||
Selection selection,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
extension EditorStateAddBlock on EditorState {
|
||||
Future<void> insertBlockOrReplaceCurrentBlock(
|
||||
Selection selection,
|
||||
Node insertedNode,
|
||||
) async {
|
||||
// If the current block is not an empty paragraph block,
|
||||
// then insert a new block below the current block.
|
||||
final node = getNodeAtPath(selection.start.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
final transaction = this.transaction;
|
||||
if (node.type != ParagraphBlockKeys.type ||
|
||||
(node.delta?.isNotEmpty ?? true)) {
|
||||
final path = node.path.next;
|
||||
// insert the block below the current empty paragraph block
|
||||
transaction
|
||||
..insertNode(path, insertedNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: path),
|
||||
);
|
||||
} else {
|
||||
final path = node.path;
|
||||
// replace the current empty paragraph block with the inserted block
|
||||
transaction
|
||||
..insertNode(path, insertedNode)
|
||||
..deleteNode(node)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: path),
|
||||
)
|
||||
..selectionExtraInfo = null;
|
||||
}
|
||||
await apply(transaction);
|
||||
service.keyboardService?.enableKeyBoard(selection);
|
||||
}
|
||||
|
||||
Future<void> insertMathEquation(
|
||||
Selection selection,
|
||||
) async {
|
||||
|
@ -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,
|
||||
) async {
|
||||
getIt.registerFactory<FilePickerService>(() => FilePicker());
|
||||
if (mode.isTest) {
|
||||
getIt.registerFactory<ApplicationDataStorage>(
|
||||
() => MockApplicationDataStorage(),
|
||||
);
|
||||
} else {
|
||||
getIt.registerFactory<ApplicationDataStorage>(
|
||||
() => ApplicationDataStorage(),
|
||||
);
|
||||
}
|
||||
|
||||
getIt.registerFactory<ApplicationDataStorage>(
|
||||
() => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(),
|
||||
);
|
||||
|
||||
getIt.registerFactoryAsync<OpenAIRepository>(
|
||||
() async {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/core/notification/folder_notification.dart';
|
||||
import 'package:appflowy/core/notification/user_notification.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@ -18,7 +20,6 @@ typedef DidUserWorkspaceUpdateCallback = void Function(
|
||||
RepeatedUserWorkspacePB workspaces,
|
||||
);
|
||||
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
||||
typedef AuthNotifyValue = FlowyResult<void, FlowyError>;
|
||||
|
||||
class UserListener {
|
||||
UserListener({
|
||||
|
@ -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'
|
||||
show WorkspaceSettingPB;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'home_bloc.freezed.dart';
|
||||
|
||||
@ -65,22 +64,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
}
|
||||
}
|
||||
|
||||
enum MenuResizeType {
|
||||
slide,
|
||||
drag,
|
||||
}
|
||||
|
||||
extension MenuResizeTypeExtension on MenuResizeType {
|
||||
Duration duration() {
|
||||
switch (this) {
|
||||
case MenuResizeType.drag:
|
||||
return 30.milliseconds;
|
||||
case MenuResizeType.slide:
|
||||
return 350.milliseconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class HomeEvent with _$HomeEvent {
|
||||
const factory HomeEvent.initial() = _Initial;
|
||||
|
@ -39,9 +39,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
||||
_userListener.start(onProfileUpdated: _profileUpdated);
|
||||
await _initUser();
|
||||
},
|
||||
fetchWorkspaces: () async {
|
||||
//
|
||||
},
|
||||
didReceiveUserProfile: (UserProfilePB newUserProfile) {
|
||||
emit(state.copyWith(userProfile: newUserProfile));
|
||||
},
|
||||
@ -70,9 +67,7 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
||||
return;
|
||||
}
|
||||
userProfileOrFailed.fold(
|
||||
(newUserProfile) => add(
|
||||
MenuUserEvent.didReceiveUserProfile(newUserProfile),
|
||||
),
|
||||
(profile) => add(MenuUserEvent.didReceiveUserProfile(profile)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
@ -81,7 +76,6 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
||||
@freezed
|
||||
class MenuUserEvent with _$MenuUserEvent {
|
||||
const factory MenuUserEvent.initial() = _Initial;
|
||||
const factory MenuUserEvent.fetchWorkspaces() = _FetchWorkspaces;
|
||||
const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName;
|
||||
const factory MenuUserEvent.didReceiveUserProfile(
|
||||
UserProfilePB newUserProfile,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DesktopAppearance extends BaseAppearance {
|
||||
@override
|
||||
@ -119,6 +120,7 @@ class DesktopAppearance extends BaseAppearance {
|
||||
tint8: theme.tint8,
|
||||
tint9: theme.tint9,
|
||||
textColor: theme.text,
|
||||
secondaryTextColor: theme.secondaryText,
|
||||
greyHover: theme.hoverBG1,
|
||||
greySelect: theme.bg3,
|
||||
lightGreyHover: theme.hoverBG3,
|
||||
|
@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// ThemeData in mobile
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileAppearance extends BaseAppearance {
|
||||
static const _primaryColor = Color(0xFF00BCF0); //primary 100
|
||||
@ -28,9 +29,7 @@ class MobileAppearance extends BaseAppearance {
|
||||
fontWeight: FontWeight.w400,
|
||||
);
|
||||
|
||||
final codeFontStyle = getFontStyle(
|
||||
fontFamily: codeFontFamily,
|
||||
);
|
||||
final codeFontStyle = getFontStyle(fontFamily: codeFontFamily);
|
||||
|
||||
final theme = brightness == Brightness.light
|
||||
? appTheme.lightTheme
|
||||
@ -81,9 +80,7 @@ class MobileAppearance extends BaseAppearance {
|
||||
: _hintColorInDarkMode;
|
||||
|
||||
return ThemeData(
|
||||
// color
|
||||
useMaterial3: false,
|
||||
|
||||
primaryColor: colorTheme.primary, //primary 100
|
||||
primaryColorLight: const Color(0xFF57B5F8), //primary 80
|
||||
dividerColor: colorTheme.outline, //caption
|
||||
@ -124,6 +121,7 @@ class MobileAppearance extends BaseAppearance {
|
||||
),
|
||||
),
|
||||
shadowColor: MaterialStateProperty.all(null),
|
||||
foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||
backgroundColor: MaterialStateProperty.resolveWith<Color>(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.disabled)) {
|
||||
@ -132,7 +130,6 @@ class MobileAppearance extends BaseAppearance {
|
||||
return colorTheme.primary;
|
||||
},
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(Colors.white),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
@ -144,20 +141,13 @@ class MobileAppearance extends BaseAppearance {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
colorTheme.onBackground,
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(colorTheme.onBackground),
|
||||
backgroundColor: MaterialStateProperty.all(colorTheme.background),
|
||||
shape: MaterialStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
),
|
||||
side: MaterialStateProperty.all(
|
||||
BorderSide(
|
||||
color: colorTheme.outline,
|
||||
width: 0.5,
|
||||
),
|
||||
BorderSide(color: colorTheme.outline, width: 0.5),
|
||||
),
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
@ -166,9 +156,7 @@ class MobileAppearance extends BaseAppearance {
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
textStyle: MaterialStateProperty.all(
|
||||
fontStyle,
|
||||
),
|
||||
textStyle: MaterialStateProperty.all(fontStyle),
|
||||
),
|
||||
),
|
||||
// text
|
||||
@ -262,6 +250,7 @@ class MobileAppearance extends BaseAppearance {
|
||||
tint8: theme.tint8,
|
||||
tint9: theme.tint9,
|
||||
textColor: theme.text,
|
||||
secondaryTextColor: theme.secondaryText,
|
||||
greyHover: theme.hoverBG1,
|
||||
greySelect: theme.bg3,
|
||||
lightGreyHover: theme.hoverBG3,
|
||||
|
@ -9,10 +9,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'settings_dialog_bloc.freezed.dart';
|
||||
|
||||
enum SettingsPage {
|
||||
// NEW
|
||||
account,
|
||||
// OLD
|
||||
appearance,
|
||||
language,
|
||||
files,
|
||||
user,
|
||||
// user,
|
||||
notifications,
|
||||
cloud,
|
||||
shortcuts,
|
||||
@ -88,6 +91,6 @@ class SettingsDialogState with _$SettingsDialogState {
|
||||
SettingsDialogState(
|
||||
userProfile: userProfile,
|
||||
successOrFailure: FlowyResult.success(null),
|
||||
page: SettingsPage.appearance,
|
||||
page: SettingsPage.account,
|
||||
);
|
||||
}
|
||||
|
@ -111,14 +111,12 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
||||
|
||||
void _profileUpdated(
|
||||
FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,
|
||||
) {
|
||||
userProfileOrFailed.fold(
|
||||
(newUserProfile) {
|
||||
add(SettingsUserEvent.didReceiveUserProfile(newUserProfile));
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
) =>
|
||||
userProfileOrFailed.fold(
|
||||
(newUserProfile) =>
|
||||
add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
class FolderHeader extends StatefulWidget {
|
||||
const FolderHeader({
|
||||
super.key,
|
||||
@ -40,6 +42,7 @@ class _FolderHeaderState extends State<FolderHeader> {
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: iconSize + textPadding * 2,
|
||||
),
|
||||
fontColor: AFThemeExtension.of(context).textColor,
|
||||
padding: const EdgeInsets.all(textPadding),
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: widget.onPressed,
|
||||
|
@ -62,14 +62,10 @@ class UserSettingButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void showSettingsDialog(
|
||||
BuildContext context,
|
||||
UserProfilePB userProfile,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return BlocProvider<DocumentAppearanceCubit>.value(
|
||||
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) =>
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => BlocProvider<DocumentAppearanceCubit>.value(
|
||||
key: _settingsDialogKey,
|
||||
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
||||
child: SettingsDialog(
|
||||
@ -81,10 +77,9 @@ void showSettingsDialog(
|
||||
},
|
||||
dismissDialog: () {
|
||||
if (Navigator.of(dialogContext).canPop()) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
} else {
|
||||
Log.warn("Can't pop dialog context");
|
||||
return Navigator.of(dialogContext).pop();
|
||||
}
|
||||
Log.warn("Can't pop dialog context");
|
||||
},
|
||||
restartApp: () async {
|
||||
// Pop the dialog using the dialog context
|
||||
@ -92,7 +87,5 @@ void showSettingsDialog(
|
||||
await runAppFlowy();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
||||
@ -8,7 +10,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
// keep this widget in case we need to roll back (lucas.xu)
|
||||
@ -23,10 +24,8 @@ class SidebarUser extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<MenuUserBloc>(
|
||||
create: (context) => MenuUserBloc(userProfile)
|
||||
..add(
|
||||
const MenuUserEvent.initial(),
|
||||
),
|
||||
create: (_) =>
|
||||
MenuUserBloc(userProfile)..add(const MenuUserEvent.initial()),
|
||||
child: BlocBuilder<MenuUserBloc, MenuUserState>(
|
||||
builder: (context, state) => Row(
|
||||
children: [
|
||||
@ -35,9 +34,7 @@ class SidebarUser extends StatelessWidget {
|
||||
name: state.userProfile.name,
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: _buildUserName(context, state),
|
||||
),
|
||||
Expanded(child: _buildUserName(context, state)),
|
||||
UserSettingButton(userProfile: state.userProfile),
|
||||
const HSpace(4),
|
||||
const NotificationButton(),
|
||||
|
@ -15,8 +15,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
typedef NaviAction = void Function();
|
||||
|
||||
class NavigationNotifier with ChangeNotifier {
|
||||
NavigationNotifier({required this.navigationItems});
|
||||
|
||||
@ -145,19 +143,6 @@ class NaviItemWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class NaviItemDivider extends StatelessWidget {
|
||||
const NaviItemDivider({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [child, const Text('/')],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EllipsisNaviItem extends NavigationItem {
|
||||
EllipsisNaviItem({required this.items});
|
||||
|
||||
|
@ -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/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
|
||||
@ -9,18 +11,12 @@ import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_s
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'widgets/setting_cloud.dart';
|
||||
|
||||
const _dialogHorizontalPadding = EdgeInsets.symmetric(horizontal: 12);
|
||||
const _contentInsetPadding = EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 16.0);
|
||||
|
||||
class SettingsDialog extends StatelessWidget {
|
||||
SettingsDialog(
|
||||
this.user, {
|
||||
@ -41,49 +37,31 @@ class SettingsDialog extends StatelessWidget {
|
||||
..add(const SettingsDialogEvent.initial()),
|
||||
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
|
||||
builder: (context, state) => FlowyDialog(
|
||||
title: Padding(
|
||||
padding: _dialogHorizontalPadding + _contentInsetPadding,
|
||||
child: FlowyText(
|
||||
LocaleKeys.settings_title.tr(),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Padding(
|
||||
padding: _dialogHorizontalPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: SettingsMenu(
|
||||
userProfile: user,
|
||||
changeSelectedPage: (index) {
|
||||
context
|
||||
.read<SettingsDialogBloc>()
|
||||
.add(SettingsDialogEvent.setSelectedPage(index));
|
||||
},
|
||||
currentPage:
|
||||
context.read<SettingsDialogBloc>().state.page,
|
||||
),
|
||||
body: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: SettingsMenu(
|
||||
userProfile: user,
|
||||
changeSelectedPage: (index) => context
|
||||
.read<SettingsDialogBloc>()
|
||||
.add(SettingsDialogEvent.setSelectedPage(index)),
|
||||
currentPage:
|
||||
context.read<SettingsDialogBloc>().state.page,
|
||||
),
|
||||
VerticalDivider(
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
Expanded(
|
||||
child: getSettingsView(
|
||||
context.read<SettingsDialogBloc>().state.page,
|
||||
context.read<SettingsDialogBloc>().state.userProfile,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: getSettingsView(
|
||||
context.read<SettingsDialogBloc>().state.page,
|
||||
context.read<SettingsDialogBloc>().state.userProfile,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -94,27 +72,24 @@ class SettingsDialog extends StatelessWidget {
|
||||
|
||||
Widget getSettingsView(SettingsPage page, UserProfilePB user) {
|
||||
switch (page) {
|
||||
case SettingsPage.account:
|
||||
return SettingsAccountView(
|
||||
userProfile: user,
|
||||
didLogout: didLogout,
|
||||
didLogin: dismissDialog,
|
||||
);
|
||||
case SettingsPage.appearance:
|
||||
return const SettingsAppearanceView();
|
||||
case SettingsPage.language:
|
||||
return const SettingsLanguageView();
|
||||
case SettingsPage.files:
|
||||
return const SettingsFileSystemView();
|
||||
case SettingsPage.user:
|
||||
return SettingsUserView(
|
||||
user,
|
||||
didLogin: () => dismissDialog(),
|
||||
didLogout: didLogout,
|
||||
didOpenUser: restartApp,
|
||||
);
|
||||
case SettingsPage.notifications:
|
||||
return const SettingsNotificationsView();
|
||||
case SettingsPage.cloud:
|
||||
return SettingCloud(
|
||||
restartAppFlowy: () => restartApp(),
|
||||
);
|
||||
return SettingCloud(restartAppFlowy: () => restartApp());
|
||||
case SettingsPage.shortcuts:
|
||||
return const SettingsCustomizeShortcutsWrapper();
|
||||
return const SettingsShortcutsView();
|
||||
case SettingsPage.member:
|
||||
return WorkspaceMembersPage(userProfile: user);
|
||||
case SettingsPage.featureFlags:
|
||||
|
@ -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/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FeatureFlagsPage extends StatelessWidget {
|
||||
const FeatureFlagsPage({
|
||||
@ -10,36 +14,30 @@ class FeatureFlagsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: SeparatedColumn(
|
||||
children: [
|
||||
...FeatureFlag.data.entries
|
||||
return SettingsBody(
|
||||
children: [
|
||||
const SettingsHeader(title: 'Feature flags'),
|
||||
SeparatedColumn(
|
||||
children: FeatureFlag.data.entries
|
||||
.where((e) => e.key != FeatureFlag.unknown)
|
||||
.map(
|
||||
(e) => _FeatureFlagItem(featureFlag: e.key),
|
||||
),
|
||||
FlowyTextButton(
|
||||
'Restart the app to apply changes',
|
||||
fontSize: 16.0,
|
||||
fontColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 12.0,
|
||||
),
|
||||
onPressed: () async {
|
||||
await runAppFlowy();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
.map((e) => _FeatureFlagItem(featureFlag: e.key))
|
||||
.toList(),
|
||||
),
|
||||
const SettingsCategorySpacer(),
|
||||
FlowyTextButton(
|
||||
'Restart the app to apply changes',
|
||||
fontSize: 16.0,
|
||||
fontColor: Colors.red,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||
onPressed: () async => runAppFlowy(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FeatureFlagItem extends StatefulWidget {
|
||||
const _FeatureFlagItem({
|
||||
required this.featureFlag,
|
||||
});
|
||||
const _FeatureFlagItem({required this.featureFlag});
|
||||
|
||||
final FeatureFlag featureFlag;
|
||||
|
||||
@ -51,21 +49,11 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: FlowyText(
|
||||
widget.featureFlag.name,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
subtitle: FlowyText.small(
|
||||
widget.featureFlag.description,
|
||||
maxLines: 3,
|
||||
),
|
||||
title: FlowyText(widget.featureFlag.name, fontSize: 16.0),
|
||||
subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3),
|
||||
trailing: Switch.adaptive(
|
||||
value: widget.featureFlag.isOn,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
widget.featureFlag.update(value);
|
||||
});
|
||||
},
|
||||
onChanged: (value) => setState(() => widget.featureFlag.update(value)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart';
|
||||
@ -9,8 +12,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@ -19,9 +20,7 @@ import '../../../../../startup/startup.dart';
|
||||
import '../../../../../startup/tasks/prelude.dart';
|
||||
|
||||
class SettingsFileLocationCustomizer extends StatefulWidget {
|
||||
const SettingsFileLocationCustomizer({
|
||||
super.key,
|
||||
});
|
||||
const SettingsFileLocationCustomizer({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsFileLocationCustomizer> createState() =>
|
||||
|
@ -1,7 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
@ -13,15 +18,11 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
class WorkspaceMembersPage extends StatelessWidget {
|
||||
const WorkspaceMembersPage({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
const WorkspaceMembersPage({super.key, required this.userProfile});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@ -33,25 +34,22 @@ class WorkspaceMembersPage extends StatelessWidget {
|
||||
child: BlocConsumer<WorkspaceMemberBloc, WorkspaceMemberState>(
|
||||
listener: _showResultDialog,
|
||||
builder: (context, state) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// title
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.settings_appearance_members_title.tr(),
|
||||
fontSize: 20,
|
||||
return SettingsBody(
|
||||
children: [
|
||||
// title
|
||||
SettingsHeader(
|
||||
title: LocaleKeys.settings_appearance_members_title.tr(),
|
||||
),
|
||||
if (state.myRole.canInvite) const _InviteMember(),
|
||||
if (state.myRole.canInvite && state.members.isNotEmpty)
|
||||
const SettingsCategorySpacer(),
|
||||
if (state.members.isNotEmpty)
|
||||
_MemberList(
|
||||
members: state.members,
|
||||
userProfile: userProfile,
|
||||
myRole: state.myRole,
|
||||
),
|
||||
if (state.myRole.canInvite) const _InviteMember(),
|
||||
if (state.members.isNotEmpty)
|
||||
_MemberList(
|
||||
members: state.members,
|
||||
userProfile: userProfile,
|
||||
myRole: state.myRole,
|
||||
),
|
||||
const VSpace(48.0),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -117,7 +115,6 @@ class _InviteMemberState extends State<_InviteMember> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(12.0),
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.settings_appearance_members_inviteMembers.tr(),
|
||||
fontSize: 16.0,
|
||||
@ -151,7 +148,6 @@ class _InviteMemberState extends State<_InviteMember> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(16.0),
|
||||
/* Enable this when the feature is ready
|
||||
PrimaryButton(
|
||||
backgroundColor: const Color(0xFFE0E0E0),
|
||||
@ -183,10 +179,6 @@ class _InviteMemberState extends State<_InviteMember> {
|
||||
),
|
||||
const VSpace(16.0),
|
||||
*/
|
||||
const Divider(
|
||||
height: 1.0,
|
||||
thickness: 1.0,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -194,11 +186,10 @@ class _InviteMemberState extends State<_InviteMember> {
|
||||
void _inviteMember() {
|
||||
final email = _emailController.text;
|
||||
if (!isEmail(email)) {
|
||||
showSnackBarMessage(
|
||||
return showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_appearance_members_emailInvalidError.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context
|
||||
.read<WorkspaceMemberBloc>()
|
||||
@ -219,22 +210,17 @@ class _MemberList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
return SeparatedColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
separatorBuilder: () => const Divider(),
|
||||
children: [
|
||||
const VSpace(16.0),
|
||||
SeparatedColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
separatorBuilder: () => const Divider(),
|
||||
children: [
|
||||
const _MemberListHeader(),
|
||||
...members.map(
|
||||
(member) => _MemberItem(
|
||||
member: member,
|
||||
myRole: myRole,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
],
|
||||
const _MemberListHeader(),
|
||||
...members.map(
|
||||
(member) => _MemberItem(
|
||||
member: member,
|
||||
myRole: myRole,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
@ -6,6 +8,8 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@ -13,7 +17,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@ -37,8 +40,11 @@ class SettingCloud extends StatelessWidget {
|
||||
create: (context) => CloudSettingBloc(cloudType),
|
||||
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
return SettingsBody(
|
||||
children: [
|
||||
SettingsHeader(
|
||||
title: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||
),
|
||||
if (Env.enableCustomCloud)
|
||||
Row(
|
||||
children: [
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
@ -10,7 +12,6 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingThirdPartyLogin extends StatelessWidget {
|
||||
@ -42,24 +43,12 @@ class SettingThirdPartyLogin extends StatelessWidget {
|
||||
: const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.signIn_signInWith.tr(),
|
||||
fontSize: 16,
|
||||
),
|
||||
const HSpace(6),
|
||||
],
|
||||
),
|
||||
const VSpace(6),
|
||||
promptMessage,
|
||||
const VSpace(6),
|
||||
indicator,
|
||||
const VSpace(6),
|
||||
if (isAuthEnabled) const ThirdPartySignInButtons(),
|
||||
const VSpace(6),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -1,10 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/appearance_defaults.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/create_file_setting.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/date_format_setting.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/time_format_setting.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'settings_appearance/settings_appearance.dart';
|
||||
@ -14,59 +19,57 @@ class SettingsAppearanceView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: BlocProvider<DynamicPluginBloc>(
|
||||
create: (_) => DynamicPluginBloc(),
|
||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
ColorSchemeSetting(
|
||||
currentTheme: state.appTheme.themeName,
|
||||
bloc: context.read<DynamicPluginBloc>(),
|
||||
),
|
||||
BrightnessSetting(
|
||||
currentThemeMode: state.themeMode,
|
||||
),
|
||||
const Divider(),
|
||||
ThemeFontFamilySetting(
|
||||
currentFontFamily: state.font,
|
||||
),
|
||||
const Divider(),
|
||||
DocumentCursorColorSetting(
|
||||
currentCursorColor: state.documentCursorColor ??
|
||||
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
|
||||
context,
|
||||
),
|
||||
),
|
||||
DocumentSelectionColorSetting(
|
||||
currentSelectionColor: state.documentSelectionColor ??
|
||||
DefaultAppearanceSettings
|
||||
.getDefaultDocumentSelectionColor(
|
||||
context,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
LayoutDirectionSetting(
|
||||
currentLayoutDirection: state.layoutDirection,
|
||||
),
|
||||
TextDirectionSetting(
|
||||
currentTextDirection: state.textDirection,
|
||||
),
|
||||
const EnableRTLToolbarItemsSetting(),
|
||||
const Divider(),
|
||||
DateFormatSetting(
|
||||
currentFormat: state.dateFormat,
|
||||
),
|
||||
TimeFormatSetting(
|
||||
currentFormat: state.timeFormat,
|
||||
),
|
||||
const Divider(),
|
||||
CreateFileSettings(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
return BlocProvider<DynamicPluginBloc>(
|
||||
create: (_) => DynamicPluginBloc(),
|
||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
children: [
|
||||
SettingsHeader(title: LocaleKeys.settings_menu_appearance.tr()),
|
||||
ColorSchemeSetting(
|
||||
currentTheme: state.appTheme.themeName,
|
||||
bloc: context.read<DynamicPluginBloc>(),
|
||||
),
|
||||
BrightnessSetting(
|
||||
currentThemeMode: state.themeMode,
|
||||
),
|
||||
const Divider(),
|
||||
ThemeFontFamilySetting(
|
||||
currentFontFamily: state.font,
|
||||
),
|
||||
const Divider(),
|
||||
DocumentCursorColorSetting(
|
||||
currentCursorColor: state.documentCursorColor ??
|
||||
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
|
||||
context,
|
||||
),
|
||||
),
|
||||
DocumentSelectionColorSetting(
|
||||
currentSelectionColor: state.documentSelectionColor ??
|
||||
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(
|
||||
context,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
LayoutDirectionSetting(
|
||||
currentLayoutDirection: state.layoutDirection,
|
||||
),
|
||||
TextDirectionSetting(
|
||||
currentTextDirection: state.textDirection,
|
||||
),
|
||||
const EnableRTLToolbarItemsSetting(),
|
||||
const Divider(),
|
||||
DateFormatSetting(
|
||||
currentFormat: state.dateFormat,
|
||||
),
|
||||
TimeFormatSetting(
|
||||
currentFormat: state.timeFormat,
|
||||
),
|
||||
const Divider(),
|
||||
CreateFileSettings(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,55 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsCustomizeShortcutsWrapper extends StatelessWidget {
|
||||
const SettingsCustomizeShortcutsWrapper({super.key});
|
||||
class SettingsShortcutsView extends StatelessWidget {
|
||||
const SettingsShortcutsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<ShortcutsCubit>(
|
||||
create: (_) =>
|
||||
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
|
||||
child: const SettingsCustomizeShortcutsView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsCustomizeShortcutsView extends StatelessWidget {
|
||||
const SettingsCustomizeShortcutsView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
||||
builder: (context, state) {
|
||||
switch (state.status) {
|
||||
case ShortcutsStatus.initial:
|
||||
case ShortcutsStatus.updating:
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
case ShortcutsStatus.success:
|
||||
return ShortcutsListView(shortcuts: state.commandShortcutEvents);
|
||||
case ShortcutsStatus.failure:
|
||||
return ShortcutsErrorView(
|
||||
errorMessage: state.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SettingsBody(
|
||||
children: [
|
||||
SettingsHeader(
|
||||
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||
),
|
||||
BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
||||
builder: (_, state) => switch (state.status) {
|
||||
ShortcutsStatus.initial ||
|
||||
ShortcutsStatus.updating =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
ShortcutsStatus.success =>
|
||||
ShortcutsListView(shortcuts: state.commandShortcutEvents),
|
||||
ShortcutsStatus.failure =>
|
||||
ShortcutsErrorView(errorMessage: state.error),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShortcutsListView extends StatelessWidget {
|
||||
const ShortcutsListView({
|
||||
super.key,
|
||||
required this.shortcuts,
|
||||
});
|
||||
const ShortcutsListView({super.key, required this.shortcuts});
|
||||
|
||||
final List<CommandShortcutEvent> shortcuts;
|
||||
|
||||
@ -73,14 +67,7 @@ class ShortcutsListView extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const VSpace(10),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: shortcuts.length,
|
||||
itemBuilder: (context, index) => ShortcutsListTile(
|
||||
shortcutEvent: shortcuts[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
...shortcuts.map((e) => ShortcutsListTile(shortcutEvent: e)),
|
||||
const VSpace(10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
@ -88,9 +75,7 @@ class ShortcutsListView extends StatelessWidget {
|
||||
const Spacer(),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.settings_shortcuts_resetToDefault.tr(),
|
||||
onPressed: () {
|
||||
context.read<ShortcutsCubit>().resetToDefault();
|
||||
},
|
||||
onPressed: () => context.read<ShortcutsCubit>().resetToDefault(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -248,9 +233,7 @@ class ShortcutsErrorView extends StatelessWidget {
|
||||
),
|
||||
FlowyIconButton(
|
||||
icon: const Icon(Icons.replay_outlined),
|
||||
onPressed: () {
|
||||
BlocProvider.of<ShortcutsCubit>(context).fetchShortcuts();
|
||||
},
|
||||
onPressed: () => context.read<ShortcutsCubit>().fetchShortcuts(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -1,35 +1,33 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_customize_location_view.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class SettingsFileSystemView extends StatefulWidget {
|
||||
const SettingsFileSystemView({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SettingsFileSystemView> createState() => _SettingsFileSystemViewState();
|
||||
}
|
||||
|
||||
class _SettingsFileSystemViewState extends State<SettingsFileSystemView> {
|
||||
late final _items = [
|
||||
const SettingsFileLocationCustomizer(),
|
||||
// disable export data for v0.2.0 in release mode.
|
||||
if (kDebugMode) const SettingsExportFileWidget(),
|
||||
const ImportAppFlowyData(),
|
||||
// clear the cache
|
||||
const SettingsFileCacheWidget(),
|
||||
];
|
||||
class SettingsFileSystemView extends StatelessWidget {
|
||||
const SettingsFileSystemView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SeparatedColumn(
|
||||
separatorBuilder: () => const Divider(),
|
||||
children: _items,
|
||||
return SettingsBody(
|
||||
children: [
|
||||
SettingsHeader(title: LocaleKeys.settings_menu_files.tr()),
|
||||
const SettingsFileLocationCustomizer(),
|
||||
const SettingsCategorySpacer(),
|
||||
if (kDebugMode) ...[
|
||||
const SettingsExportFileWidget(),
|
||||
],
|
||||
const ImportAppFlowyData(),
|
||||
const SettingsCategorySpacer(),
|
||||
const SettingsFileCacheWidget(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra/language.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsLanguageView extends StatelessWidget {
|
||||
@ -13,18 +16,21 @@ class SettingsLanguageView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.settings_menu_language.tr(),
|
||||
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) => SettingsBody(
|
||||
children: [
|
||||
SettingsHeader(title: LocaleKeys.settings_menu_language.tr()),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.settings_menu_language.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
LanguageSelector(currentLocale: state.locale),
|
||||
],
|
||||
),
|
||||
LanguageSelector(currentLocale: state.locale),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsMenu extends StatelessWidget {
|
||||
const SettingsMenu({
|
||||
@ -22,79 +25,127 @@ class SettingsMenu extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: SeparatedColumn(
|
||||
separatorBuilder: () => const SizedBox(height: 10),
|
||||
children: [
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.appearance,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_appearance.tr(),
|
||||
icon: Icons.brightness_4,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.language,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_language.tr(),
|
||||
icon: Icons.translate,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.files,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_files.tr(),
|
||||
icon: Icons.file_present_outlined,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.user,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_user.tr(),
|
||||
icon: Icons.account_box_outlined,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.notifications,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_notifications.tr(),
|
||||
icon: Icons.notifications_outlined,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.cloud,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||
icon: Icons.sync,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.shortcuts,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||
icon: Icons.cut,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
if (FeatureFlag.membersSettings.isOn &&
|
||||
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud)
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.member,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
icon: Icons.people,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
// Column > Expanded for full size no matter the content
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8) +
|
||||
const EdgeInsets.only(left: 8, right: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
bottomLeft: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
if (kDebugMode)
|
||||
SettingsMenuElement(
|
||||
// no need to translate this page
|
||||
page: SettingsPage.featureFlags,
|
||||
selectedPage: currentPage,
|
||||
label: 'Feature Flags',
|
||||
icon: Icons.flag,
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
child: SingleChildScrollView(
|
||||
// Right padding is added to make the scrollbar centered
|
||||
// in the space between the menu and the content
|
||||
padding: const EdgeInsets.only(right: 4) +
|
||||
const EdgeInsets.symmetric(vertical: 16),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: SeparatedColumn(
|
||||
separatorBuilder: () => const VSpace(16),
|
||||
children: [
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.account,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_accountPage_menuLabel.tr(),
|
||||
icon: const FlowySvg(FlowySvgs.settings_account_m),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.appearance,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_appearance.tr(),
|
||||
icon: Icon(
|
||||
Icons.brightness_4,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.language,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_language.tr(),
|
||||
icon: Icon(
|
||||
Icons.translate,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.files,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_files.tr(),
|
||||
icon: Icon(
|
||||
Icons.file_present_outlined,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.notifications,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_notifications.tr(),
|
||||
icon: Icon(
|
||||
Icons.notifications_outlined,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.cloud,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||
icon: Icon(
|
||||
Icons.sync,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.shortcuts,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||
icon: Icon(
|
||||
Icons.cut,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
if (FeatureFlag.membersSettings.isOn &&
|
||||
userProfile.authenticator ==
|
||||
AuthenticatorPB.AppFlowyCloud)
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.member,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
icon: Icon(
|
||||
Icons.people,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
if (kDebugMode)
|
||||
SettingsMenuElement(
|
||||
// no need to translate this page
|
||||
page: SettingsPage.featureFlags,
|
||||
selectedPage: currentPage,
|
||||
label: 'Feature Flags',
|
||||
icon: Icon(
|
||||
Icons.flag,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsMenuElement extends StatelessWidget {
|
||||
const SettingsMenuElement({
|
||||
@ -17,27 +19,22 @@ class SettingsMenuElement extends StatelessWidget {
|
||||
final SettingsPage page;
|
||||
final SettingsPage selectedPage;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Widget icon;
|
||||
final Function changeSelectedPage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyHover(
|
||||
isSelected: () => page == selectedPage,
|
||||
resetHoverOnRebuild: false,
|
||||
style: HoverStyle(
|
||||
hoverColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: AFThemeExtension.of(context).greySelect,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: page == selectedPage
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: null,
|
||||
),
|
||||
onTap: () {
|
||||
changeSelectedPage(page);
|
||||
},
|
||||
dense: true,
|
||||
leading: icon,
|
||||
onTap: () => changeSelectedPage(page),
|
||||
selected: page == selectedPage,
|
||||
selectedColor: Theme.of(context).colorScheme.onSurface,
|
||||
selectedTileColor: Theme.of(context).colorScheme.primary,
|
||||
@ -45,7 +42,7 @@ class SettingsMenuElement extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
minLeadingWidth: 0,
|
||||
title: FlowyText.semibold(
|
||||
title: FlowyText.medium(
|
||||
label,
|
||||
fontSize: FontSizes.s14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsNotificationsView extends StatelessWidget {
|
||||
@ -12,32 +15,26 @@ class SettingsNotificationsView extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
|
||||
builder: (context, state) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowySettingListTile(
|
||||
label: LocaleKeys
|
||||
.settings_notifications_enableNotifications_label
|
||||
.tr(),
|
||||
hint: LocaleKeys.settings_notifications_enableNotifications_hint
|
||||
.tr(),
|
||||
trailing: [
|
||||
Switch(
|
||||
value: state.isNotificationsEnabled,
|
||||
splashRadius: 0,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (value) {
|
||||
context
|
||||
.read<NotificationSettingsCubit>()
|
||||
.toggleNotificationsEnabled();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
return SettingsBody(
|
||||
children: [
|
||||
SettingsHeader(title: LocaleKeys.settings_menu_notifications.tr()),
|
||||
FlowySettingListTile(
|
||||
label: LocaleKeys.settings_notifications_enableNotifications_label
|
||||
.tr(),
|
||||
hint: LocaleKeys.settings_notifications_enableNotifications_hint
|
||||
.tr(),
|
||||
trailing: [
|
||||
Switch(
|
||||
value: state.isNotificationsEnabled,
|
||||
splashRadius: 0,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (value) => context
|
||||
.read<NotificationSettingsCubit>()
|
||||
.toggleNotificationsEnabled(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -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/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
@ -11,7 +13,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MoreViewActions extends StatefulWidget {
|
||||
@ -107,7 +108,7 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
||||
FlowySvgs.three_dots_vertical_s,
|
||||
size: const Size.square(16),
|
||||
color: isHovering
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
? Theme.of(context).colorScheme.onSecondary
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user