mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 19:17:43 +03:00
feat: workspace settings page (#5225)
* 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 * feat: settings workspace page * chore: fixes after merge * fix: recent views bugfix * fix: make sure text buttons have color * test: add test for delete workspace in settings * test: remove pumpAndSettle for create workspace * test: longer pump duration * test: attempt with large pump duration * test: attempt workaround * chore: clean code * fix: missing language key * test: add one more check * test: pump * test: more pump * test: attempt pumpAndSettle * chore: code review * fix: persist single workspace on patch * fix: listen to workspace changes * chore: remove redundant builder * test: remove unstable test * fix: changes after merge * chore: changes after merge * feat: support changing cursor and selection color * chore: move members up in menu * feat: clean code and beautify dialogs * fix: fix test and make show selected font --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
parent
f47c88b022
commit
a0ed043cb8
BIN
frontend/appflowy_flutter/assets/images/appearance/dark.png
Normal file
BIN
frontend/appflowy_flutter/assets/images/appearance/dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/appflowy_flutter/assets/images/appearance/light.png
Normal file
BIN
frontend/appflowy_flutter/assets/images/appearance/light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
frontend/appflowy_flutter/assets/images/appearance/system.png
Normal file
BIN
frontend/appflowy_flutter/assets/images/appearance/system.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
@ -60,9 +60,12 @@ void main() {
|
||||
|
||||
Finder success;
|
||||
|
||||
final Finder items = find.byType(WorkspaceMenuItem);
|
||||
|
||||
// delete the newly created workspace
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
final Finder items = find.byType(WorkspaceMenuItem);
|
||||
await tester.pumpUntilFound(items);
|
||||
|
||||
expect(items, findsNWidgets(2));
|
||||
expect(
|
||||
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||
|
@ -1,11 +1,9 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'notifications_settings_test.dart' as notifications_settings_test;
|
||||
import 'user_language_test.dart' as user_language_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
notifications_settings_test.main();
|
||||
user_language_test.main();
|
||||
}
|
||||
|
@ -1,70 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Settings: user language tests', () {
|
||||
testWidgets('select language, language changed', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
await tester.openSettings();
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.language);
|
||||
|
||||
final userLanguageFinder = find.descendant(
|
||||
of: find.byType(SettingsLanguageView),
|
||||
matching: find.byType(LanguageSelector),
|
||||
);
|
||||
|
||||
// Grab current locale
|
||||
LanguageSelector userLanguage =
|
||||
tester.widget<LanguageSelector>(userLanguageFinder);
|
||||
Locale currentLocale = userLanguage.currentLocale;
|
||||
|
||||
// Open language selector
|
||||
await tester.tap(userLanguageFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select first option that isn't default
|
||||
await tester.tap(find.byType(LanguageItem).at(1));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Make sure the new locale is not the same as previous one
|
||||
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
|
||||
expect(
|
||||
userLanguage.currentLocale,
|
||||
isNot(equals(currentLocale)),
|
||||
reason: "new language shouldn't equal the previous selected language",
|
||||
);
|
||||
|
||||
// Update the current locale to a new one
|
||||
currentLocale = userLanguage.currentLocale;
|
||||
|
||||
// Tried the same flow for the second time
|
||||
// Open language selector
|
||||
await tester.tap(userLanguageFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Select second option that isn't default
|
||||
await tester.tap(find.byType(LanguageItem).at(2));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Make sure the new locale is not the same as previous one
|
||||
userLanguage = tester.widget<LanguageSelector>(userLanguageFinder);
|
||||
expect(
|
||||
userLanguage.currentLocale,
|
||||
isNot(equals(currentLocale)),
|
||||
reason: "new language shouldn't equal the previous selected language",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
import 'package:appflowy/util/font_family_extension.dart';
|
||||
import 'package:appflowy/workspace/application/appearance_defaults.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/settings_appearance.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('appearance settings tests', () {
|
||||
testWidgets('after editing text field, button should be able to be clicked',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
await tester.openSettings();
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
final dropDown = find.byKey(ThemeFontFamilySetting.popoverKey);
|
||||
await tester.tap(dropDown);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final textField = find.byKey(ThemeFontFamilySetting.textFieldKey);
|
||||
await tester.tap(textField);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(textField, 'Abel');
|
||||
await tester.pumpAndSettle();
|
||||
final fontFamilyButton = find.byKey(const Key('Abel'));
|
||||
|
||||
expect(fontFamilyButton, findsOneWidget);
|
||||
await tester.tap(fontFamilyButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// just switch the page and verify that the font family was set after that
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
expect(find.textContaining('Abel'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('reset the font family', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
await tester.openSettings();
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
final dropDown = find.byKey(ThemeFontFamilySetting.popoverKey);
|
||||
await tester.tap(dropDown);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final textField = find.byKey(ThemeFontFamilySetting.textFieldKey);
|
||||
await tester.tap(textField);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(textField, 'Abel');
|
||||
await tester.pumpAndSettle();
|
||||
final fontFamilyButton = find.byKey(const Key('Abel'));
|
||||
|
||||
expect(fontFamilyButton, findsOneWidget);
|
||||
await tester.tap(fontFamilyButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// just switch the page and verify that the font family was set after that
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
final resetButton = find.byKey(ThemeFontFamilySetting.resetButtonKey);
|
||||
await tester.tap(resetButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// just switch the page and verify that the font family was set after that
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
|
||||
expect(
|
||||
find.textContaining(
|
||||
DefaultAppearanceSettings.kDefaultFontFamily.fontFamilyDisplayName,
|
||||
),
|
||||
findsNWidgets(2),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@ -23,31 +25,35 @@ void main() {
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.appearance);
|
||||
await tester.openSettingsPage(SettingsPage.workspace);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectToSeeText(
|
||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
||||
);
|
||||
final appFinder = find.byType(MaterialApp).first;
|
||||
ThemeMode? themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
|
||||
expect(themeMode, ThemeMode.system);
|
||||
|
||||
await tester.tapButton(
|
||||
find.bySemanticsLabel(
|
||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
||||
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
|
||||
await tester.tapButton(
|
||||
find.bySemanticsLabel(
|
||||
LocaleKeys.settings_appearance_themeMode_dark.tr(),
|
||||
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.dark);
|
||||
|
||||
await tester.tap(find.byType(SettingsDialog));
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||
@ -60,12 +66,10 @@ void main() {
|
||||
],
|
||||
tester: tester,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectToSeeText(
|
||||
LocaleKeys.settings_appearance_themeMode_light.tr(),
|
||||
);
|
||||
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||
expect(themeMode, ThemeMode.light);
|
||||
});
|
||||
|
||||
testWidgets('show or hide home menu', (tester) async {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy/user/presentation/screens/skip_log_in_screen.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
|
@ -3,8 +3,6 @@ import 'package:integration_test/integration_test.dart';
|
||||
import 'desktop/board/board_test_runner.dart' as board_test_runner;
|
||||
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
|
||||
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
|
||||
import 'desktop/uncategorized/appearance_settings_test.dart'
|
||||
as appearance_test_runner;
|
||||
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
|
||||
import 'desktop/uncategorized/empty_test.dart' as first_test;
|
||||
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
|
||||
@ -26,7 +24,6 @@ Future<void> runIntegration3OnDesktop() async {
|
||||
emoji_shortcut_test.main();
|
||||
hotkeys_test.main();
|
||||
emoji_shortcut_test.main();
|
||||
appearance_test_runner.main();
|
||||
settings_test_runner.main();
|
||||
share_markdown_test.main();
|
||||
import_files_test.main();
|
||||
|
@ -173,27 +173,39 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = false,
|
||||
int milliseconds = 500,
|
||||
bool pumpAndSettle = true,
|
||||
}) async {
|
||||
await tap(
|
||||
finder,
|
||||
buttons: buttons,
|
||||
warnIfMissed: warnIfMissed,
|
||||
);
|
||||
await pumpAndSettle(
|
||||
Duration(milliseconds: milliseconds),
|
||||
EnginePhase.sendSemanticsUpdate,
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
if (pumpAndSettle) {
|
||||
await this.pumpAndSettle(
|
||||
Duration(milliseconds: milliseconds),
|
||||
EnginePhase.sendSemanticsUpdate,
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithName(String tr, {int milliseconds = 500}) async {
|
||||
Future<void> tapButtonWithName(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
bool pumpAndSettle = true,
|
||||
}) 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);
|
||||
await tapButton(
|
||||
button,
|
||||
milliseconds: milliseconds,
|
||||
pumpAndSettle: pumpAndSettle,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> doubleTapAt(
|
||||
|
@ -22,7 +22,6 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.
|
||||
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';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@ -511,8 +510,9 @@ extension CommonOperations on WidgetTester {
|
||||
|
||||
final workspace = find.byType(SidebarWorkspace);
|
||||
expect(workspace, findsOneWidget);
|
||||
// click it
|
||||
await tapButton(workspace, milliseconds: 2000);
|
||||
|
||||
await tapButton(workspace, pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
Future<void> createCollaborativeWorkspace(String name) async {
|
||||
@ -527,7 +527,8 @@ extension CommonOperations on WidgetTester {
|
||||
// click the create button
|
||||
final createButton = find.byKey(createWorkspaceButtonKey);
|
||||
expect(createButton, findsOneWidget);
|
||||
await tapButton(createButton);
|
||||
await tapButton(createButton, pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
|
||||
// see the create workspace dialog
|
||||
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
|
||||
@ -536,7 +537,8 @@ extension CommonOperations on WidgetTester {
|
||||
// input the workspace name
|
||||
await enterText(find.byType(TextField), name);
|
||||
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false);
|
||||
await pump(const Duration(seconds: 5));
|
||||
}
|
||||
|
||||
// For mobile platform to launch the app in anonymous mode
|
||||
|
@ -3,9 +3,10 @@ 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/pages/settings_workspace_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/widgets/toggle/toggle.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';
|
||||
@ -13,6 +14,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import '../desktop/board/board_hide_groups_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'common_operations.dart';
|
||||
|
||||
extension AppFlowySettings on WidgetTester {
|
||||
/// Open settings page
|
||||
@ -77,12 +79,21 @@ extension AppFlowySettings on WidgetTester {
|
||||
// go to settings page and toggle enable RTL toolbar items
|
||||
Future<void> toggleEnableRTLToolbarItems() async {
|
||||
await openSettings();
|
||||
await openSettingsPage(SettingsPage.appearance);
|
||||
await openSettingsPage(SettingsPage.workspace);
|
||||
|
||||
final switchButton =
|
||||
find.byKey(EnableRTLToolbarItemsSetting.enableRTLSwitchKey);
|
||||
expect(switchButton, findsOneWidget);
|
||||
await tapButton(switchButton);
|
||||
final scrollable = find.findSettingsScrollable();
|
||||
await scrollUntilVisible(
|
||||
find.byType(EnableRTLItemsSwitcher),
|
||||
0,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
|
||||
final switcher = find.descendant(
|
||||
of: find.byType(EnableRTLItemsSwitcher),
|
||||
matching: find.byType(Toggle),
|
||||
);
|
||||
|
||||
await tap(switcher);
|
||||
|
||||
// tap anywhere to close the settings page
|
||||
await tapAt(Offset.zero);
|
||||
|
1042
frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart
Normal file
1042
frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -127,6 +127,12 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
workspace: currentWorkspace,
|
||||
iconSize: 26,
|
||||
enableEdit: false,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
currentWorkspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
|
@ -1,12 +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/mobile/presentation/widgets/widgets.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/members/workspace_member_bloc.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';
|
||||
|
||||
// Only works on mobile.
|
||||
@ -105,6 +107,12 @@ class _WorkspaceMenuItem extends StatelessWidget {
|
||||
enableEdit: false,
|
||||
iconSize: 26,
|
||||
workspace: workspace,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
workspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: workspace.workspaceId == currentWorkspace.workspaceId
|
||||
? const FlowySvg(
|
||||
|
@ -1,13 +1,28 @@
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.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/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/shared/google_fonts_extension.dart';
|
||||
import 'package:appflowy/util/font_family_extension.dart';
|
||||
import 'package:appflowy/util/levenshtein.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/setting_list_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.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/style_widget/text_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
final customizeFontToolbarItem = ToolbarItem(
|
||||
id: 'editor.font',
|
||||
@ -16,10 +31,12 @@ final customizeFontToolbarItem = ToolbarItem(
|
||||
builder: (context, editorState, highlightColor, _) {
|
||||
final selection = editorState.selection!;
|
||||
final popoverController = PopoverController();
|
||||
final String? currentFontFamily = editorState
|
||||
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily);
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: FontFamilyDropDown(
|
||||
currentFontFamily: '',
|
||||
currentFontFamily: currentFontFamily ?? '',
|
||||
offset: const Offset(0, 12),
|
||||
popoverController: popoverController,
|
||||
onOpen: () => keepEditorFocusNotifier.increase(),
|
||||
@ -35,8 +52,11 @@ final customizeFontToolbarItem = ToolbarItem(
|
||||
Log.error('Failed to set font family: $e');
|
||||
}
|
||||
},
|
||||
onResetFont: () async => editorState
|
||||
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}),
|
||||
onResetFont: () async {
|
||||
popoverController.close();
|
||||
await editorState
|
||||
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FlowyTooltip(
|
||||
@ -52,3 +72,227 @@ final customizeFontToolbarItem = ToolbarItem(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
class ThemeFontFamilySetting extends StatefulWidget {
|
||||
const ThemeFontFamilySetting({
|
||||
super.key,
|
||||
required this.currentFontFamily,
|
||||
});
|
||||
|
||||
final String currentFontFamily;
|
||||
static Key textFieldKey = const Key('FontFamilyTextField');
|
||||
static Key resetButtonkey = const Key('FontFamilyResetButton');
|
||||
static Key popoverKey = const Key('FontFamilyPopover');
|
||||
|
||||
@override
|
||||
State<ThemeFontFamilySetting> createState() => _ThemeFontFamilySettingState();
|
||||
}
|
||||
|
||||
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingListTile(
|
||||
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
|
||||
resetButtonKey: ThemeFontFamilySetting.resetButtonkey,
|
||||
onResetRequested: () {
|
||||
context.read<AppearanceSettingsCubit>().resetFontFamily();
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
|
||||
},
|
||||
trailing: [
|
||||
FontFamilyDropDown(currentFontFamily: widget.currentFontFamily),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FontFamilyDropDown extends StatefulWidget {
|
||||
const FontFamilyDropDown({
|
||||
super.key,
|
||||
required this.currentFontFamily,
|
||||
this.onOpen,
|
||||
this.onClose,
|
||||
this.onFontFamilyChanged,
|
||||
this.child,
|
||||
this.popoverController,
|
||||
this.offset,
|
||||
this.showResetButton = false,
|
||||
this.onResetFont,
|
||||
});
|
||||
|
||||
final String currentFontFamily;
|
||||
final VoidCallback? onOpen;
|
||||
final VoidCallback? onClose;
|
||||
final void Function(String fontFamily)? onFontFamilyChanged;
|
||||
final Widget? child;
|
||||
final PopoverController? popoverController;
|
||||
final Offset? offset;
|
||||
final bool showResetButton;
|
||||
final VoidCallback? onResetFont;
|
||||
|
||||
@override
|
||||
State<FontFamilyDropDown> createState() => _FontFamilyDropDownState();
|
||||
}
|
||||
|
||||
class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
|
||||
final List<String> availableFonts = [
|
||||
defaultFontFamily,
|
||||
...GoogleFonts.asMap().keys,
|
||||
];
|
||||
final ValueNotifier<String> query = ValueNotifier('');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
query.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentValue = widget.currentFontFamily.fontFamilyDisplayName;
|
||||
return SettingValueDropDown(
|
||||
popoverKey: ThemeFontFamilySetting.popoverKey,
|
||||
popoverController: widget.popoverController,
|
||||
currentValue: currentValue,
|
||||
onClose: () {
|
||||
query.value = '';
|
||||
widget.onClose?.call();
|
||||
},
|
||||
offset: widget.offset,
|
||||
child: widget.child,
|
||||
popupBuilder: (_) {
|
||||
widget.onOpen?.call();
|
||||
return CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
if (widget.showResetButton)
|
||||
SliverPersistentHeader(
|
||||
delegate: _ResetFontButton(onPressed: widget.onResetFont),
|
||||
pinned: true,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: FlowyTextField(
|
||||
key: ThemeFontFamilySetting.textFieldKey,
|
||||
hintText:
|
||||
LocaleKeys.settings_appearance_fontFamily_search.tr(),
|
||||
autoFocus: false,
|
||||
debounceDuration: const Duration(milliseconds: 300),
|
||||
onChanged: (value) {
|
||||
query.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 4),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: query,
|
||||
builder: (context, value, child) {
|
||||
var displayed = availableFonts;
|
||||
if (value.isNotEmpty) {
|
||||
displayed = availableFonts
|
||||
.where(
|
||||
(font) => font
|
||||
.toLowerCase()
|
||||
.contains(value.toLowerCase().toString()),
|
||||
)
|
||||
.sorted((a, b) => levenshtein(a, b))
|
||||
.toList();
|
||||
}
|
||||
return SliverFixedExtentList.builder(
|
||||
itemBuilder: (context, index) => _fontFamilyItemButton(
|
||||
context,
|
||||
getGoogleFontSafely(displayed[index]),
|
||||
),
|
||||
itemCount: displayed.length,
|
||||
itemExtent: 32,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fontFamilyItemButton(
|
||||
BuildContext context,
|
||||
TextStyle style,
|
||||
) {
|
||||
final buttonFontFamily =
|
||||
style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily;
|
||||
return Tooltip(
|
||||
message: buttonFontFamily,
|
||||
waitDuration: const Duration(milliseconds: 150),
|
||||
child: SizedBox(
|
||||
key: ValueKey(buttonFontFamily),
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
onHover: (_) => FocusScope.of(context).unfocus(),
|
||||
text: FlowyText.medium(
|
||||
buttonFontFamily.fontFamilyDisplayName,
|
||||
fontFamily: buttonFontFamily,
|
||||
),
|
||||
rightIcon:
|
||||
buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
|
||||
? const FlowySvg(FlowySvgs.check_s)
|
||||
: null,
|
||||
onTap: () {
|
||||
if (widget.onFontFamilyChanged != null) {
|
||||
widget.onFontFamilyChanged!(buttonFontFamily);
|
||||
} else {
|
||||
if (widget.currentFontFamily.parseFontFamilyName() !=
|
||||
buttonFontFamily) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setFontFamily(buttonFontFamily);
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncFontFamily(buttonFontFamily);
|
||||
}
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ResetFontButton extends SliverPersistentHeaderDelegate {
|
||||
_ResetFontButton({this.onPressed});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8, bottom: 8.0),
|
||||
child: FlowyTextButton(
|
||||
LocaleKeys.document_toolbar_resetToDefaultFont.tr(),
|
||||
fontColor: AFThemeExtension.of(context).textColor,
|
||||
fontHoverColor: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => 35;
|
||||
|
||||
@override
|
||||
double get minExtent => 35;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||
true;
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
@ -14,8 +17,6 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@ -48,9 +49,9 @@ class EditorStyleCustomizer {
|
||||
return EditorStyle.desktop(
|
||||
padding: padding,
|
||||
cursorColor: appearance.cursorColor ??
|
||||
DefaultAppearanceSettings.getDefaultDocumentCursorColor(context),
|
||||
DefaultAppearanceSettings.getDefaultCursorColor(context),
|
||||
selectionColor: appearance.selectionColor ??
|
||||
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(context),
|
||||
DefaultAppearanceSettings.getDefaultSelectionColor(context),
|
||||
defaultTextDirection: appearance.defaultTextDirection,
|
||||
textStyleConfiguration: TextStyleConfiguration(
|
||||
text: baseTextStyle(fontFamily).copyWith(
|
||||
|
@ -16,7 +16,10 @@ import 'package:appflowy_backend/rust_stream.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
typedef DidUserWorkspaceUpdateCallback = void Function(
|
||||
typedef DidUpdateUserWorkspaceCallback = void Function(
|
||||
UserWorkspacePB workspace,
|
||||
);
|
||||
typedef DidUpdateUserWorkspacesCallback = void Function(
|
||||
RepeatedUserWorkspacePB workspaces,
|
||||
);
|
||||
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
||||
@ -31,11 +34,19 @@ class UserListener {
|
||||
UserNotificationParser? _userParser;
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
|
||||
DidUserWorkspaceUpdateCallback? didUpdateUserWorkspaces;
|
||||
|
||||
/// Update notification about _all_ of the users workspaces
|
||||
///
|
||||
DidUpdateUserWorkspacesCallback? didUpdateUserWorkspaces;
|
||||
|
||||
/// Update notification about _one_ workspace
|
||||
///
|
||||
DidUpdateUserWorkspaceCallback? didUpdateUserWorkspace;
|
||||
|
||||
void start({
|
||||
void Function(UserProfileNotifyValue)? onProfileUpdated,
|
||||
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
|
||||
void Function(UserWorkspacePB)? didUpdateUserWorkspace,
|
||||
}) {
|
||||
if (onProfileUpdated != null) {
|
||||
_profileNotifier?.addPublishListener(onProfileUpdated);
|
||||
@ -45,6 +56,10 @@ class UserListener {
|
||||
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
|
||||
}
|
||||
|
||||
if (didUpdateUserWorkspace != null) {
|
||||
this.didUpdateUserWorkspace = didUpdateUserWorkspace;
|
||||
}
|
||||
|
||||
_userParser = UserNotificationParser(
|
||||
id: _userProfile.id.toString(),
|
||||
callback: _userNotificationCallback,
|
||||
@ -81,6 +96,11 @@ class UserListener {
|
||||
},
|
||||
);
|
||||
break;
|
||||
case user.UserNotification.DidUpdateUserWorkspace:
|
||||
result.map(
|
||||
(r) => didUpdateUserWorkspace?.call(UserWorkspacePB.fromBuffer(r)),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/presentation/router.dart';
|
||||
import 'package:appflowy/user/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
@ -36,9 +35,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const _SkipLoginMoveWindow(),
|
||||
body: Center(
|
||||
child: _renderBody(context),
|
||||
),
|
||||
body: Center(child: _renderBody(context)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -73,9 +70,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
||||
SizedBox(
|
||||
width: size.width * 0.7,
|
||||
child: FolderWidget(
|
||||
createFolderCallback: () async {
|
||||
_didCustomizeFolder = true;
|
||||
},
|
||||
createFolderCallback: () async => _didCustomizeFolder = true,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
@ -88,24 +83,16 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
||||
Future<void> _autoRegister(BuildContext context) async {
|
||||
final result = await getIt<AuthService>().signUpAsGuest();
|
||||
result.fold(
|
||||
(user) {
|
||||
getIt<AuthRouter>().goHomeScreen(context, user);
|
||||
},
|
||||
(error) {
|
||||
Log.error(error);
|
||||
},
|
||||
(user) => getIt<AuthRouter>().goHomeScreen(context, user),
|
||||
(error) => Log.error(error),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _relaunchAppAndAutoRegister() async {
|
||||
await runAppFlowy(isAnon: true);
|
||||
}
|
||||
Future<void> _relaunchAppAndAutoRegister() async => runAppFlowy(isAnon: true);
|
||||
}
|
||||
|
||||
class SkipLoginPageFooter extends StatelessWidget {
|
||||
const SkipLoginPageFooter({
|
||||
super.key,
|
||||
});
|
||||
const SkipLoginPageFooter({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -135,9 +122,7 @@ class SkipLoginPageFooter extends StatelessWidget {
|
||||
}
|
||||
|
||||
class SubscribeButtons extends StatelessWidget {
|
||||
const SubscribeButtons({
|
||||
super.key,
|
||||
});
|
||||
const SubscribeButtons({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -168,10 +153,7 @@ class SubscribeButtons extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText.regular(
|
||||
LocaleKeys.and.tr(),
|
||||
fontSize: FontSizes.s12,
|
||||
),
|
||||
FlowyText.regular(LocaleKeys.and.tr(), fontSize: FontSizes.s12),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.subscribeNewsletterText.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
@ -190,9 +172,7 @@ class SubscribeButtons extends StatelessWidget {
|
||||
}
|
||||
|
||||
class LanguageSelectorOnWelcomePage extends StatelessWidget {
|
||||
const LanguageSelectorOnWelcomePage({
|
||||
super.key,
|
||||
});
|
||||
const LanguageSelectorOnWelcomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -205,24 +185,16 @@ class LanguageSelectorOnWelcomePage extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.ethernet_m,
|
||||
size: Size.square(20),
|
||||
),
|
||||
const FlowySvg(FlowySvgs.ethernet_m, size: Size.square(20)),
|
||||
const HSpace(4),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final currentLocale =
|
||||
context.watch<AppearanceSettingsCubit>().state.locale;
|
||||
return FlowyText(
|
||||
languageFromLocale(currentLocale),
|
||||
);
|
||||
return FlowyText(languageFromLocale(currentLocale));
|
||||
},
|
||||
),
|
||||
const FlowySvg(
|
||||
FlowySvgs.drop_menu_hide_m,
|
||||
size: Size.square(20),
|
||||
),
|
||||
const FlowySvg(FlowySvgs.drop_menu_hide_m, size: Size.square(20)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -231,15 +203,68 @@ class LanguageSelectorOnWelcomePage extends StatelessWidget {
|
||||
if (easyLocalization == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final allLocales = easyLocalization.supportedLocales;
|
||||
|
||||
return LanguageItemsListView(
|
||||
allLocales: allLocales,
|
||||
allLocales: easyLocalization.supportedLocales,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageItemsListView extends StatelessWidget {
|
||||
const LanguageItemsListView({super.key, required this.allLocales});
|
||||
|
||||
final List<Locale> allLocales;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// get current locale from cubit
|
||||
final state = context.watch<AppearanceSettingsCubit>().state;
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
child: ListView.builder(
|
||||
itemCount: allLocales.length,
|
||||
itemBuilder: (context, index) {
|
||||
final locale = allLocales[index];
|
||||
return LanguageItem(locale: locale, currentLocale: state.locale);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageItem extends StatelessWidget {
|
||||
const LanguageItem({
|
||||
super.key,
|
||||
required this.locale,
|
||||
required this.currentLocale,
|
||||
});
|
||||
|
||||
final Locale locale;
|
||||
final Locale currentLocale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
languageFromLocale(locale),
|
||||
),
|
||||
rightIcon:
|
||||
currentLocale == locale ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||
onTap: () {
|
||||
if (currentLocale != locale) {
|
||||
context.read<AppearanceSettingsCubit>().setLocale(context, locale);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GoButton extends StatelessWidget {
|
||||
const GoButton({super.key, required this.onPressed});
|
||||
|
||||
@ -248,10 +273,7 @@ class GoButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => AnonUserBloc()
|
||||
..add(
|
||||
const AnonUserEvent.initial(),
|
||||
),
|
||||
create: (context) => AnonUserBloc()..add(const AnonUserEvent.initial()),
|
||||
child: BlocListener<AnonUserBloc, AnonUserState>(
|
||||
listener: (context, state) async {
|
||||
if (state.openedAnonUser != null) {
|
||||
@ -265,7 +287,6 @@ class GoButton extends StatelessWidget {
|
||||
: LocaleKeys.signIn_continueAnonymousUser.tr();
|
||||
|
||||
final textWidget = Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
@ -274,22 +295,6 @@ class GoButton extends StatelessWidget {
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
// Tooltip(
|
||||
// message: LocaleKeys.settings_menu_configServerGuide.tr(),
|
||||
// child: Container(
|
||||
// width: 30.0,
|
||||
// decoration: const BoxDecoration(
|
||||
// shape: BoxShape.circle,
|
||||
// ),
|
||||
// child: Center(
|
||||
// child: Icon(
|
||||
// Icons.help,
|
||||
// color: Colors.white,
|
||||
// weight: 2,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
|
||||
@ -325,15 +330,8 @@ class _SkipLoginMoveWindow extends StatelessWidget
|
||||
const _SkipLoginMoveWindow();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MoveWindowDetector(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) =>
|
||||
const Row(children: [Expanded(child: MoveWindowDetector())]);
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(55.0);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A class for the default appearance settings for the app
|
||||
class DefaultAppearanceSettings {
|
||||
@ -9,11 +10,11 @@ class DefaultAppearanceSettings {
|
||||
static const kDefaultThemeName = "Default";
|
||||
static const kDefaultTheme = BuiltInTheme.defaultTheme;
|
||||
|
||||
static Color getDefaultDocumentCursorColor(BuildContext context) {
|
||||
static Color getDefaultCursorColor(BuildContext context) {
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
|
||||
static Color getDefaultDocumentSelectionColor(BuildContext context) {
|
||||
static Color getDefaultSelectionColor(BuildContext context) {
|
||||
return Theme.of(context).colorScheme.primary.withOpacity(0.2);
|
||||
}
|
||||
}
|
||||
|
@ -47,11 +47,13 @@ class CachedRecentService {
|
||||
Future<FlowyResult<void, FlowyError>> updateRecentViews(
|
||||
List<String> viewIds,
|
||||
bool addInRecent,
|
||||
) async {
|
||||
return FolderEventUpdateRecentViews(
|
||||
UpdateRecentViewPayloadPB(viewIds: viewIds, addInRecent: addInRecent),
|
||||
).send();
|
||||
}
|
||||
) async =>
|
||||
FolderEventUpdateRecentViews(
|
||||
UpdateRecentViewPayloadPB(
|
||||
viewIds: viewIds,
|
||||
addInRecent: addInRecent,
|
||||
),
|
||||
).send();
|
||||
|
||||
Future<FlowyResult<RepeatedViewPB, FlowyError>> _readRecentViews() =>
|
||||
FolderEventReadRecentViews().send();
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/shared/google_fonts_extension.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// the default font family is empty, so we can use the default font family of the platform
|
||||
// the system will choose the default font family of the platform
|
||||
|
@ -0,0 +1,13 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
extension TimeFormatter on UserTimeFormatPB {
|
||||
DateFormat get toFormat => _toFormat[this]!;
|
||||
|
||||
String formatTime(DateTime date) => toFormat.format(date);
|
||||
}
|
||||
|
||||
final _toFormat = {
|
||||
UserTimeFormatPB.TwelveHour: DateFormat.Hm(),
|
||||
UserTimeFormatPB.TwentyFourHour: DateFormat.jm(),
|
||||
};
|
@ -11,11 +11,9 @@ part 'settings_dialog_bloc.freezed.dart';
|
||||
enum SettingsPage {
|
||||
// NEW
|
||||
account,
|
||||
workspace,
|
||||
// OLD
|
||||
appearance,
|
||||
language,
|
||||
files,
|
||||
// user,
|
||||
notifications,
|
||||
cloud,
|
||||
shortcuts,
|
||||
|
@ -0,0 +1,153 @@
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
part 'workspace_settings_bloc.freezed.dart';
|
||||
|
||||
class WorkspaceSettingsBloc
|
||||
extends Bloc<WorkspaceSettingsEvent, WorkspaceSettingsState> {
|
||||
WorkspaceSettingsBloc() : super(WorkspaceSettingsState.initial()) {
|
||||
on<WorkspaceSettingsEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: (userProfile, workspace) async {
|
||||
_userService = UserBackendService(userId: userProfile.id);
|
||||
|
||||
try {
|
||||
final currentWorkspace =
|
||||
await _userService!.getCurrentWorkspace().getOrThrow();
|
||||
|
||||
final workspaces =
|
||||
await _userService!.getWorkspaces().getOrThrow();
|
||||
if (workspaces.isEmpty) {
|
||||
workspaces.add(
|
||||
UserWorkspacePB.create()
|
||||
..workspaceId = currentWorkspace.id
|
||||
..name = currentWorkspace.name
|
||||
..createdAtTimestamp = currentWorkspace.createTime,
|
||||
);
|
||||
}
|
||||
|
||||
final currentWorkspaceInList = workspaces.firstWhereOrNull(
|
||||
(e) => e.workspaceId == currentWorkspace.id,
|
||||
) ??
|
||||
workspaces.firstOrNull;
|
||||
|
||||
// We emit here because the next event might take longer.
|
||||
emit(state.copyWith(workspace: currentWorkspaceInList));
|
||||
|
||||
if (currentWorkspaceInList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final members = await _getWorkspaceMembers(
|
||||
currentWorkspaceInList.workspaceId,
|
||||
);
|
||||
|
||||
final role = members
|
||||
.firstWhereOrNull((e) => e.email == userProfile.email)
|
||||
?.role ??
|
||||
AFRolePB.Guest;
|
||||
|
||||
emit(state.copyWith(members: members, myRole: role));
|
||||
} catch (e) {
|
||||
Log.error('Failed to get or create current workspace');
|
||||
}
|
||||
},
|
||||
updateWorkspaceName: (name) async {
|
||||
final request = RenameWorkspacePB(
|
||||
workspaceId: state.workspace?.workspaceId,
|
||||
newName: name,
|
||||
);
|
||||
final result = await UserEventRenameWorkspace(request).send();
|
||||
|
||||
state.workspace!.freeze();
|
||||
final update = state.workspace!.rebuild((p0) => p0.name = name);
|
||||
|
||||
result.fold(
|
||||
(_) => emit(state.copyWith(workspace: update)),
|
||||
(e) => Log.error('Failed to rename workspace: $e'),
|
||||
);
|
||||
},
|
||||
updateWorkspaceIcon: (icon) async {
|
||||
if (state.workspace == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final request = ChangeWorkspaceIconPB()
|
||||
..workspaceId = state.workspace!.workspaceId
|
||||
..newIcon = icon;
|
||||
final result = await UserEventChangeWorkspaceIcon(request).send();
|
||||
|
||||
result.fold(
|
||||
(_) {
|
||||
state.workspace!.freeze();
|
||||
final newWorkspace =
|
||||
state.workspace!.rebuild((p0) => p0.icon = icon);
|
||||
|
||||
return emit(state.copyWith(workspace: newWorkspace));
|
||||
},
|
||||
(e) => Log.error('Failed to update workspace icon: $e'),
|
||||
);
|
||||
},
|
||||
deleteWorkspace: () async =>
|
||||
emit(state.copyWith(deleteWorkspace: true)),
|
||||
leaveWorkspace: () async =>
|
||||
emit(state.copyWith(leaveWorkspace: true)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
UserBackendService? _userService;
|
||||
|
||||
Future<List<WorkspaceMemberPB>> _getWorkspaceMembers(
|
||||
String workspaceId,
|
||||
) async {
|
||||
final data = QueryWorkspacePB()..workspaceId = workspaceId;
|
||||
final result = await UserEventGetWorkspaceMember(data).send();
|
||||
return result.fold(
|
||||
(s) => s.items,
|
||||
(e) {
|
||||
Log.error('Failed to read workspace members: $e');
|
||||
return [];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class WorkspaceSettingsEvent with _$WorkspaceSettingsEvent {
|
||||
const factory WorkspaceSettingsEvent.initial({
|
||||
required UserProfilePB userProfile,
|
||||
@Default(null) UserWorkspacePB? workspace,
|
||||
}) = Initial;
|
||||
|
||||
// Workspace itself
|
||||
const factory WorkspaceSettingsEvent.updateWorkspaceName(String name) =
|
||||
UpdateWorkspaceName;
|
||||
const factory WorkspaceSettingsEvent.updateWorkspaceIcon(String icon) =
|
||||
UpdateWorkspaceIcon;
|
||||
const factory WorkspaceSettingsEvent.deleteWorkspace() = DeleteWorkspace;
|
||||
const factory WorkspaceSettingsEvent.leaveWorkspace() = LeaveWorkspace;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class WorkspaceSettingsState with _$WorkspaceSettingsState {
|
||||
const factory WorkspaceSettingsState({
|
||||
@Default(null) UserWorkspacePB? workspace,
|
||||
@Default([]) List<WorkspaceMemberPB> members,
|
||||
@Default(AFRolePB.Guest) AFRolePB myRole,
|
||||
@Default(false) bool deleteWorkspace,
|
||||
@Default(false) bool leaveWorkspace,
|
||||
}) = _WorkspaceSettingsState;
|
||||
|
||||
factory WorkspaceSettingsState.initial() => const WorkspaceSettingsState();
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/user/application/user_listener.dart';
|
||||
@ -10,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
@ -27,11 +28,18 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_listener
|
||||
..didUpdateUserWorkspaces = (workspaces) {
|
||||
add(UserWorkspaceEvent.updateWorkspaces(workspaces));
|
||||
}
|
||||
..start();
|
||||
_listener.start(
|
||||
didUpdateUserWorkspaces: (workspaces) =>
|
||||
add(UserWorkspaceEvent.updateWorkspaces(workspaces)),
|
||||
didUpdateUserWorkspace: (workspace) {
|
||||
// If currentWorkspace is updated, eg. Icon or Name, we should notify
|
||||
// the UI to render the updated information.
|
||||
final currentWorkspace = state.currentWorkspace;
|
||||
if (currentWorkspace?.workspaceId == workspace.workspaceId) {
|
||||
add(UserWorkspaceEvent.updateCurrentWorkspace(workspace));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
final result = await _fetchWorkspaces();
|
||||
final currentWorkspace = result.$1;
|
||||
@ -337,6 +345,25 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
updateCurrentWorkspace: (workspace) async {
|
||||
final workspaces = [...state.workspaces];
|
||||
final index = workspaces
|
||||
.indexWhere((e) => e.workspaceId == workspace.workspaceId);
|
||||
if (index != -1) {
|
||||
workspaces[index] = workspace;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentWorkspace: workspace,
|
||||
workspaces: workspaces
|
||||
..sort(
|
||||
(a, b) =>
|
||||
a.createdAtTimestamp.compareTo(b.createdAtTimestamp),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -413,6 +440,9 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent {
|
||||
const factory UserWorkspaceEvent.updateWorkspaces(
|
||||
RepeatedUserWorkspacePB workspaces,
|
||||
) = UpdateWorkspaces;
|
||||
const factory UserWorkspaceEvent.updateCurrentWorkspace(
|
||||
UserWorkspacePB workspace,
|
||||
) = UpdateCurrentWorkspace;
|
||||
}
|
||||
|
||||
enum UserWorkspaceActionType {
|
||||
|
@ -88,40 +88,38 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: HomeHotKeys(
|
||||
userProfile: userProfile,
|
||||
child: Scaffold(
|
||||
floatingActionButton: enableMemoryLeakDetect
|
||||
? const FloatingActionButton(
|
||||
onPressed: dumpMemoryLeak,
|
||||
child: Icon(Icons.memory),
|
||||
)
|
||||
: null,
|
||||
body: BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.latestView != c.latestView,
|
||||
listener: (context, state) {
|
||||
final view = state.latestView;
|
||||
if (view != null) {
|
||||
// Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
|
||||
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
|
||||
final currentPageManager =
|
||||
context.read<TabsBloc>().state.currentPageManager;
|
||||
child: Scaffold(
|
||||
floatingActionButton: enableMemoryLeakDetect
|
||||
? const FloatingActionButton(
|
||||
onPressed: dumpMemoryLeak,
|
||||
child: Icon(Icons.memory),
|
||||
)
|
||||
: null,
|
||||
body: BlocListener<HomeBloc, HomeState>(
|
||||
listenWhen: (p, c) => p.latestView != c.latestView,
|
||||
listener: (context, state) {
|
||||
final view = state.latestView;
|
||||
if (view != null) {
|
||||
// Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
|
||||
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
|
||||
final currentPageManager =
|
||||
context.read<TabsBloc>().state.currentPageManager;
|
||||
|
||||
if (currentPageManager.plugin.pluginType ==
|
||||
PluginType.blank) {
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(plugin: view.plugin()),
|
||||
);
|
||||
}
|
||||
if (currentPageManager.plugin.pluginType ==
|
||||
PluginType.blank) {
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(plugin: view.plugin()),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, state) => BlocProvider(
|
||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(
|
||||
const UserWorkspaceEvent.initial(),
|
||||
),
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, state) => BlocProvider(
|
||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||
..add(const UserWorkspaceEvent.initial()),
|
||||
child: HomeHotKeys(
|
||||
userProfile: userProfile,
|
||||
child: FlowyContainer(
|
||||
Theme.of(context).colorScheme.surface,
|
||||
child: _buildBody(context, userProfile, workspaceSetting),
|
||||
|
@ -136,9 +136,9 @@ class HomeSideBar extends StatelessWidget {
|
||||
workspaceSetting.workspaceId,
|
||||
),
|
||||
);
|
||||
context.read<FavoriteBloc>().add(
|
||||
const FavoriteEvent.fetchFavorites(),
|
||||
);
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(const FavoriteEvent.fetchFavorites());
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@ -65,9 +66,14 @@ class UserSettingButton extends StatelessWidget {
|
||||
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) =>
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => BlocProvider<DocumentAppearanceCubit>.value(
|
||||
builder: (dialogContext) => MultiBlocProvider(
|
||||
key: _settingsDialogKey,
|
||||
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
||||
providers: [
|
||||
BlocProvider<DocumentAppearanceCubit>.value(
|
||||
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
||||
),
|
||||
BlocProvider.value(value: context.read<UserWorkspaceBloc>()),
|
||||
],
|
||||
child: SettingsDialog(
|
||||
userProfile,
|
||||
didLogout: () async {
|
||||
|
@ -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/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
@ -14,14 +16,10 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.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:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SidebarWorkspace extends StatefulWidget {
|
||||
const SidebarWorkspace({
|
||||
super.key,
|
||||
required this.userProfile,
|
||||
});
|
||||
const SidebarWorkspace({super.key, required this.userProfile});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@ -197,6 +195,12 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget {
|
||||
workspace: currentWorkspace,
|
||||
iconSize: 20,
|
||||
enableEdit: false,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
currentWorkspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(6),
|
||||
|
@ -1,25 +1,26 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
import 'package:appflowy/util/color_generator/color_generator.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class WorkspaceIcon extends StatefulWidget {
|
||||
const WorkspaceIcon({
|
||||
super.key,
|
||||
required this.workspace,
|
||||
required this.enableEdit,
|
||||
required this.iconSize,
|
||||
required this.workspace,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final UserWorkspacePB workspace;
|
||||
final double iconSize;
|
||||
final bool enableEdit;
|
||||
final void Function(EmojiPickerResult) onSelected;
|
||||
|
||||
@override
|
||||
State<WorkspaceIcon> createState() => _WorkspaceIconState();
|
||||
@ -45,7 +46,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
||||
height: max(widget.iconSize, 26),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorGenerator(widget.workspace.name).toColor(),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: FlowyText(
|
||||
widget.workspace.name.isEmpty
|
||||
@ -55,6 +56,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
||||
color: Colors.black,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.enableEdit) {
|
||||
child = AppFlowyPopover(
|
||||
offset: const Offset(0, 8),
|
||||
@ -62,19 +64,12 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(360, 380)),
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return FlowyIconPicker(
|
||||
onSelected: (result) {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
widget.workspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
);
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
popupBuilder: (_) => FlowyIconPicker(
|
||||
onSelected: (result) {
|
||||
widget.onSelected(result);
|
||||
controller.close();
|
||||
},
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
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/workspace/application/user/user_workspace_bloc.dart';
|
||||
@ -11,7 +13,6 @@ 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:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@visibleForTesting
|
||||
@ -152,6 +153,12 @@ class WorkspaceMenuItem extends StatelessWidget {
|
||||
workspace: workspace,
|
||||
iconSize: 26,
|
||||
enableEdit: true,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
workspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -10,8 +10,6 @@ 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/widgets/setting_third_party_login.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
@ -58,11 +56,9 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
||||
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_accountPage_title.tr(),
|
||||
description: LocaleKeys.settings_accountPage_description.tr(),
|
||||
children: [
|
||||
SettingsHeader(
|
||||
title: LocaleKeys.settings_accountPage_title.tr(),
|
||||
description: LocaleKeys.settings_accountPage_description.tr(),
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_accountPage_general_title.tr(),
|
||||
children: [
|
||||
@ -140,7 +136,6 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
const SettingsCategorySpacer(),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_accountPage_keys_title.tr(),
|
||||
children: [
|
||||
@ -174,7 +169,6 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SettingsCategorySpacer(),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_accountPage_login_title.tr(),
|
||||
children: [
|
||||
@ -409,10 +403,10 @@ class _UserProfileSettingState extends State<UserProfileSetting> {
|
||||
width: 360,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: FlowyIconPicker(
|
||||
onSelected: (result) {
|
||||
context.read<SettingsUserViewBloc>().add(
|
||||
SettingsUserEvent.updateUserIcon(iconUrl: result.emoji),
|
||||
);
|
||||
onSelected: (r) {
|
||||
context
|
||||
.read<SettingsUserViewBloc>()
|
||||
.add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji));
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
),
|
||||
|
@ -0,0 +1,906 @@
|
||||
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/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||
import 'package:appflowy/util/font_family_extension.dart';
|
||||
import 'package:appflowy/workspace/application/appearance_defaults.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/document_color_setting_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.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_dashed_divider.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/language.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra/theme_extension.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/dialog/styled_dialogs.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class SettingsWorkspaceView extends StatefulWidget {
|
||||
const SettingsWorkspaceView({super.key, required this.userProfile});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
State<SettingsWorkspaceView> createState() => _SettingsWorkspaceViewState();
|
||||
}
|
||||
|
||||
class _SettingsWorkspaceViewState extends State<SettingsWorkspaceView> {
|
||||
final TextEditingController _workspaceNameController =
|
||||
TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_workspaceNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<WorkspaceSettingsBloc>(
|
||||
create: (context) => WorkspaceSettingsBloc()
|
||||
..add(WorkspaceSettingsEvent.initial(userProfile: widget.userProfile)),
|
||||
child: BlocConsumer<WorkspaceSettingsBloc, WorkspaceSettingsState>(
|
||||
listener: (context, state) {
|
||||
if ((state.workspace?.name ?? '') != _workspaceNameController.text) {
|
||||
_workspaceNameController.text = state.workspace?.name ?? '';
|
||||
}
|
||||
|
||||
if (state.deleteWorkspace) {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.deleteWorkspace(
|
||||
state.workspace!.workspaceId,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
if (state.leaveWorkspace) {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.leaveWorkspace(
|
||||
state.workspace!.workspaceId,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_workspacePage_title.tr(),
|
||||
description: LocaleKeys.settings_workspacePage_description.tr(),
|
||||
children: [
|
||||
// We don't allow changing workspace name/icon for local/offline
|
||||
if (state.workspace != null &&
|
||||
widget.userProfile.authenticator !=
|
||||
AuthenticatorPB.Local) ...[
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_workspacePage_workspaceName_title
|
||||
.tr(),
|
||||
children: [
|
||||
SettingsActionableInput(
|
||||
controller: _workspaceNameController,
|
||||
onSave: (value) => _saveWorkspaceName(
|
||||
context,
|
||||
current: state.workspace!.name,
|
||||
name: value,
|
||||
),
|
||||
actions: [
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: FlowyTextButton(
|
||||
LocaleKeys.button_save.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: () => _saveWorkspaceName(
|
||||
context,
|
||||
current: state.workspace!.name,
|
||||
name: _workspaceNameController.text,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_workspacePage_workspaceIcon_title
|
||||
.tr(),
|
||||
description: LocaleKeys
|
||||
.settings_workspacePage_workspaceIcon_description
|
||||
.tr(),
|
||||
children: [
|
||||
_WorkspaceIconSetting(workspace: state.workspace!),
|
||||
],
|
||||
),
|
||||
],
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_workspacePage_appearance_title.tr(),
|
||||
children: const [AppearanceSelector()],
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_workspacePage_theme_title.tr(),
|
||||
description:
|
||||
LocaleKeys.settings_workspacePage_theme_description.tr(),
|
||||
children: const [
|
||||
_ThemeDropdown(),
|
||||
SettingsDashedDivider(),
|
||||
_DocumentCursorColorSetting(),
|
||||
_DocumentSelectionColorSetting(),
|
||||
],
|
||||
),
|
||||
SettingsCategory(
|
||||
title:
|
||||
LocaleKeys.settings_workspacePage_workspaceFont_title.tr(),
|
||||
children: const [_FontSelectorDropdown()],
|
||||
),
|
||||
SettingsCategory(
|
||||
title:
|
||||
LocaleKeys.settings_workspacePage_textDirection_title.tr(),
|
||||
children: const [
|
||||
_TextDirectionSelect(),
|
||||
EnableRTLItemsSwitcher(),
|
||||
],
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_workspacePage_layoutDirection_title
|
||||
.tr(),
|
||||
children: const [_LayoutDirectionSelect()],
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_workspacePage_dateTime_title.tr(),
|
||||
children: [
|
||||
const _DateTimeFormatLabel(),
|
||||
const _TimeFormatSwitcher(),
|
||||
SettingsDashedDivider(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
const _DateFormatDropdown(),
|
||||
],
|
||||
),
|
||||
SettingsCategory(
|
||||
title: LocaleKeys.settings_workspacePage_language_title.tr(),
|
||||
children: const [LanguageDropdown()],
|
||||
),
|
||||
if (state.workspace != null &&
|
||||
widget.userProfile.authenticator !=
|
||||
AuthenticatorPB.Local) ...[
|
||||
SingleSettingAction(
|
||||
label: LocaleKeys.settings_workspacePage_manageWorkspace_title
|
||||
.tr(),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
onPressed: () => SettingsAlertDialog(
|
||||
title: state.myRole.isOwner
|
||||
? LocaleKeys
|
||||
.settings_workspacePage_deleteWorkspacePrompt_title
|
||||
.tr()
|
||||
: LocaleKeys
|
||||
.settings_workspacePage_leaveWorkspacePrompt_title
|
||||
.tr(),
|
||||
subtitle: state.myRole.isOwner
|
||||
? LocaleKeys
|
||||
.settings_workspacePage_deleteWorkspacePrompt_content
|
||||
.tr()
|
||||
: LocaleKeys
|
||||
.settings_workspacePage_leaveWorkspacePrompt_content
|
||||
.tr(),
|
||||
isDangerous: true,
|
||||
confirm: () {
|
||||
context.read<WorkspaceSettingsBloc>().add(
|
||||
state.myRole.isOwner
|
||||
? const WorkspaceSettingsEvent.deleteWorkspace()
|
||||
: const WorkspaceSettingsEvent.leaveWorkspace(),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
).show(context),
|
||||
isDangerous: true,
|
||||
buttonLabel: state.myRole.isOwner
|
||||
? LocaleKeys
|
||||
.settings_workspacePage_manageWorkspace_deleteWorkspace
|
||||
.tr()
|
||||
: LocaleKeys
|
||||
.settings_workspacePage_manageWorkspace_leaveWorkspace
|
||||
.tr(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _saveWorkspaceName(
|
||||
BuildContext context, {
|
||||
required String current,
|
||||
required String name,
|
||||
}) {
|
||||
if (name.isNotEmpty && name != current) {
|
||||
context.read<WorkspaceSettingsBloc>().add(
|
||||
WorkspaceSettingsEvent.updateWorkspaceName(
|
||||
_workspaceNameController.text,
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_workspacePage_workspaceName_savedMessage.tr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class LanguageDropdown extends StatelessWidget {
|
||||
const LanguageDropdown({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return SettingsDropdown<Locale>(
|
||||
key: const Key('LanguageDropdown'),
|
||||
expandWidth: false,
|
||||
onChanged: (locale) => context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLocale(context, locale),
|
||||
selectedOption: state.locale,
|
||||
options: EasyLocalization.of(context)!
|
||||
.supportedLocales
|
||||
.map(
|
||||
(locale) => buildDropdownMenuEntry<Locale>(
|
||||
context,
|
||||
selectedValue: state.locale,
|
||||
value: locale,
|
||||
label: languageFromLocale(locale),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WorkspaceIconSetting extends StatelessWidget {
|
||||
const _WorkspaceIconSetting({required this.workspace});
|
||||
|
||||
final UserWorkspacePB workspace;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 64,
|
||||
width: 64,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outline),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: WorkspaceIcon(
|
||||
workspace: workspace,
|
||||
iconSize: workspace.icon.isNotEmpty == true ? 46 : 20,
|
||||
enableEdit: true,
|
||||
onSelected: (r) => context
|
||||
.read<WorkspaceSettingsBloc>()
|
||||
.add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextDirectionSelect extends StatelessWidget {
|
||||
const _TextDirectionSelect();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
final selectedItem = state.textDirection ?? AppFlowyTextDirection.auto;
|
||||
|
||||
return SettingsRadioSelect<AppFlowyTextDirection>(
|
||||
onChanged: (item) => context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setTextDirection(item.value),
|
||||
items: [
|
||||
SettingsRadioItem(
|
||||
value: AppFlowyTextDirection.ltr,
|
||||
icon: const FlowySvg(FlowySvgs.textdirection_ltr_m),
|
||||
label: LocaleKeys.settings_workspacePage_textDirection_leftToRight
|
||||
.tr(),
|
||||
isSelected: selectedItem == AppFlowyTextDirection.ltr,
|
||||
),
|
||||
SettingsRadioItem(
|
||||
value: AppFlowyTextDirection.rtl,
|
||||
icon: const FlowySvg(FlowySvgs.textdirection_rtl_m),
|
||||
label: LocaleKeys.settings_workspacePage_textDirection_rightToLeft
|
||||
.tr(),
|
||||
isSelected: selectedItem == AppFlowyTextDirection.rtl,
|
||||
),
|
||||
SettingsRadioItem(
|
||||
value: AppFlowyTextDirection.auto,
|
||||
icon: const FlowySvg(FlowySvgs.textdirection_auto_m),
|
||||
label: LocaleKeys.settings_workspacePage_textDirection_auto.tr(),
|
||||
isSelected: selectedItem == AppFlowyTextDirection.auto,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class EnableRTLItemsSwitcher extends StatelessWidget {
|
||||
const EnableRTLItemsSwitcher({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.settings_workspacePage_textDirection_enableRTLItems.tr(),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const HSpace(16),
|
||||
Toggle(
|
||||
style: ToggleStyle.big,
|
||||
value: context
|
||||
.watch<AppearanceSettingsCubit>()
|
||||
.state
|
||||
.enableRtlToolbarItems,
|
||||
onChanged: (value) => context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setEnableRTLToolbarItems(!value),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LayoutDirectionSelect extends StatelessWidget {
|
||||
const _LayoutDirectionSelect();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return SettingsRadioSelect<LayoutDirection>(
|
||||
onChanged: (item) => context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLayoutDirection(item.value),
|
||||
items: [
|
||||
SettingsRadioItem(
|
||||
value: LayoutDirection.ltrLayout,
|
||||
icon: const FlowySvg(FlowySvgs.textdirection_ltr_m),
|
||||
label: LocaleKeys
|
||||
.settings_workspacePage_layoutDirection_leftToRight
|
||||
.tr(),
|
||||
isSelected: state.layoutDirection == LayoutDirection.ltrLayout,
|
||||
),
|
||||
SettingsRadioItem(
|
||||
value: LayoutDirection.rtlLayout,
|
||||
icon: const FlowySvg(FlowySvgs.textdirection_rtl_m),
|
||||
label: LocaleKeys
|
||||
.settings_workspacePage_layoutDirection_rightToLeft
|
||||
.tr(),
|
||||
isSelected: state.layoutDirection == LayoutDirection.rtlLayout,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DateFormatDropdown extends StatelessWidget {
|
||||
const _DateFormatDropdown();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.regular(
|
||||
LocaleKeys.settings_workspacePage_dateTime_dateFormat_label
|
||||
.tr(),
|
||||
fontSize: 16,
|
||||
),
|
||||
const VSpace(8),
|
||||
SettingsDropdown<UserDateFormatPB>(
|
||||
key: const Key('DateFormatDropdown'),
|
||||
expandWidth: false,
|
||||
onChanged: (format) => context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setDateFormat(format),
|
||||
selectedOption: state.dateFormat,
|
||||
options: UserDateFormatPB.values
|
||||
.map(
|
||||
(format) => buildDropdownMenuEntry<UserDateFormatPB>(
|
||||
context,
|
||||
value: format,
|
||||
label: _formatLabel(format),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _formatLabel(UserDateFormatPB format) => switch (format) {
|
||||
UserDateFormatPB.Locally =>
|
||||
LocaleKeys.settings_workspacePage_dateTime_dateFormat_local.tr(),
|
||||
UserDateFormatPB.US =>
|
||||
LocaleKeys.settings_workspacePage_dateTime_dateFormat_us.tr(),
|
||||
UserDateFormatPB.ISO =>
|
||||
LocaleKeys.settings_workspacePage_dateTime_dateFormat_iso.tr(),
|
||||
UserDateFormatPB.Friendly =>
|
||||
LocaleKeys.settings_workspacePage_dateTime_dateFormat_friendly.tr(),
|
||||
UserDateFormatPB.DayMonthYear =>
|
||||
LocaleKeys.settings_workspacePage_dateTime_dateFormat_dmy.tr(),
|
||||
_ => "Unknown format",
|
||||
};
|
||||
}
|
||||
|
||||
class _DateTimeFormatLabel extends StatelessWidget {
|
||||
const _DateTimeFormatLabel();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
|
||||
return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
|
||||
builder: (context, state) {
|
||||
return FlowyText.regular(
|
||||
LocaleKeys.settings_workspacePage_dateTime_example.tr(
|
||||
args: [
|
||||
state.dateFormat.formatDate(now, false),
|
||||
state.timeFormat.formatTime(now),
|
||||
now.timeZoneName,
|
||||
],
|
||||
),
|
||||
fontSize: 16,
|
||||
color: AFThemeExtension.of(context).secondaryTextColor,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimeFormatSwitcher extends StatelessWidget {
|
||||
const _TimeFormatSwitcher();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.settings_workspacePage_dateTime_24HourTime.tr(),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const HSpace(16),
|
||||
Toggle(
|
||||
style: ToggleStyle.big,
|
||||
value: context.watch<AppearanceSettingsCubit>().state.timeFormat ==
|
||||
UserTimeFormatPB.TwentyFourHour,
|
||||
onChanged: (value) =>
|
||||
context.read<AppearanceSettingsCubit>().setTimeFormat(
|
||||
value
|
||||
? UserTimeFormatPB.TwelveHour
|
||||
: UserTimeFormatPB.TwentyFourHour,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeDropdown extends StatelessWidget {
|
||||
const _ThemeDropdown();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<DynamicPluginBloc>(
|
||||
create: (context) => DynamicPluginBloc()..add(DynamicPluginEvent.load()),
|
||||
child: BlocBuilder<DynamicPluginBloc, DynamicPluginState>(
|
||||
buildWhen: (_, current) => current is Ready,
|
||||
builder: (context, state) {
|
||||
final appearance = context.watch<AppearanceSettingsCubit>().state;
|
||||
final isLightMode = Theme.of(context).brightness == Brightness.light;
|
||||
|
||||
final customThemes = state.whenOrNull(
|
||||
ready: (ps) => ps.map((p) => p.theme).whereType<AppTheme>(),
|
||||
);
|
||||
|
||||
return SettingsDropdown<String>(
|
||||
key: const Key('ThemeSelectorDropdown'),
|
||||
actions: [
|
||||
SettingAction(
|
||||
tooltip: 'Upload a custom theme',
|
||||
icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(16)),
|
||||
onPressed: () => Dialogs.show(
|
||||
context,
|
||||
child: BlocProvider<DynamicPluginBloc>.value(
|
||||
value: context.read<DynamicPluginBloc>(),
|
||||
child: const FlowyDialog(
|
||||
constraints: BoxConstraints(maxHeight: 300),
|
||||
child: ThemeUploadWidget(),
|
||||
),
|
||||
),
|
||||
).then((val) {
|
||||
if (val != null) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_appearance_themeUpload_uploadSuccess
|
||||
.tr(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
SettingAction(
|
||||
icon: const FlowySvg(FlowySvgs.restore_s),
|
||||
label: LocaleKeys.settings_common_reset.tr(),
|
||||
onPressed: () => context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setTheme(AppTheme.builtins.first.themeName),
|
||||
),
|
||||
],
|
||||
onChanged: (theme) =>
|
||||
context.read<AppearanceSettingsCubit>().setTheme(theme),
|
||||
selectedOption: appearance.appTheme.themeName,
|
||||
options: [
|
||||
...AppTheme.builtins.map(
|
||||
(t) {
|
||||
final theme = isLightMode ? t.lightTheme : t.darkTheme;
|
||||
|
||||
return buildDropdownMenuEntry<String>(
|
||||
context,
|
||||
selectedValue: appearance.appTheme.themeName,
|
||||
value: t.themeName,
|
||||
label: t.themeName,
|
||||
leadingWidget: _ThemeLeading(color: theme.sidebarBg),
|
||||
);
|
||||
},
|
||||
),
|
||||
...?customThemes?.map(
|
||||
(t) {
|
||||
final theme = isLightMode ? t.lightTheme : t.darkTheme;
|
||||
|
||||
return buildDropdownMenuEntry<String>(
|
||||
context,
|
||||
selectedValue: appearance.appTheme.themeName,
|
||||
value: t.themeName,
|
||||
label: t.themeName,
|
||||
leadingWidget: _ThemeLeading(color: theme.sidebarBg),
|
||||
trailingWidget: FlowyIconButton(
|
||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSurface,
|
||||
onPressed: () {
|
||||
context.read<DynamicPluginBloc>().add(
|
||||
DynamicPluginEvent.removePlugin(
|
||||
name: t.themeName,
|
||||
),
|
||||
);
|
||||
|
||||
if (appearance.appTheme.themeName == t.themeName) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setTheme(AppTheme.builtins.first.themeName);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThemeLeading extends StatelessWidget {
|
||||
const _ThemeLeading({required this.color});
|
||||
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: Corners.s4Border,
|
||||
border: Border.all(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class AppearanceSelector extends StatelessWidget {
|
||||
const AppearanceSelector({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeMode = context.read<AppearanceSettingsCubit>().state.themeMode;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...ThemeMode.values.map(
|
||||
(t) => Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () =>
|
||||
context.read<AppearanceSettingsCubit>().setThemeMode(t),
|
||||
child: FlowyHover(
|
||||
style: HoverStyle.transparent(
|
||||
foregroundColorOnHover:
|
||||
AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 88,
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: t == themeMode
|
||||
? Theme.of(context).colorScheme.onSecondary
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: Corners.s4Border,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: AssetImage(
|
||||
'assets/images/appearance/${t.name.toLowerCase()}.png',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(6),
|
||||
FlowyText.regular(getLabel(t), textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String getLabel(ThemeMode t) => switch (t) {
|
||||
ThemeMode.system =>
|
||||
LocaleKeys.settings_workspacePage_appearance_options_system.tr(),
|
||||
ThemeMode.light =>
|
||||
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
|
||||
ThemeMode.dark =>
|
||||
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
|
||||
};
|
||||
}
|
||||
|
||||
class _FontSelectorDropdown extends StatelessWidget {
|
||||
const _FontSelectorDropdown();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appearance = context.watch<AppearanceSettingsCubit>().state;
|
||||
return SettingsDropdown<String>(
|
||||
key: const Key('FontSelectorDropdown'),
|
||||
actions: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setFontFamily(defaultFontFamily),
|
||||
child: SizedBox(
|
||||
height: 26,
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
const FlowySvg(FlowySvgs.restore_s),
|
||||
const HSpace(4),
|
||||
FlowyText.regular(LocaleKeys.settings_common_reset.tr()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (font) =>
|
||||
context.read<AppearanceSettingsCubit>().setFontFamily(font),
|
||||
selectedOption: appearance.font,
|
||||
options: [defaultFontFamily, ...GoogleFonts.asMap().keys]
|
||||
.map(
|
||||
(font) => buildDropdownMenuEntry<String>(
|
||||
context,
|
||||
selectedValue: appearance.font,
|
||||
value: font,
|
||||
label: font.fontFamilyDisplayName,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DocumentCursorColorSetting extends StatelessWidget {
|
||||
const _DocumentCursorColorSetting();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label =
|
||||
LocaleKeys.settings_appearance_documentSettings_cursorColor.tr();
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (context, state) {
|
||||
return SettingListTile(
|
||||
label: label,
|
||||
resetButtonKey: const Key('DocumentCursorColorResetButton'),
|
||||
onResetRequested: () => context
|
||||
..read<AppearanceSettingsCubit>().resetDocumentCursorColor()
|
||||
..read<DocumentAppearanceCubit>().syncCursorColor(null),
|
||||
trailing: [
|
||||
DocumentColorSettingButton(
|
||||
key: const Key('DocumentCursorColorSettingButton'),
|
||||
currentColor: state.cursorColor ??
|
||||
DefaultAppearanceSettings.getDefaultCursorColor(context),
|
||||
previewWidgetBuilder: (color) => _CursorColorValueWidget(
|
||||
cursorColor: color ??
|
||||
DefaultAppearanceSettings.getDefaultCursorColor(context),
|
||||
),
|
||||
dialogTitle: label,
|
||||
onApply: (color) => context
|
||||
..read<AppearanceSettingsCubit>().setDocumentCursorColor(color)
|
||||
..read<DocumentAppearanceCubit>().syncCursorColor(color),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CursorColorValueWidget extends StatelessWidget {
|
||||
const _CursorColorValueWidget({required this.cursorColor});
|
||||
|
||||
final Color cursorColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(color: cursorColor, width: 2, height: 16),
|
||||
FlowyText(
|
||||
LocaleKeys.appName.tr(),
|
||||
// To avoid the text color changes when it is hovered in dark mode
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DocumentSelectionColorSetting extends StatelessWidget {
|
||||
const _DocumentSelectionColorSetting();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label =
|
||||
LocaleKeys.settings_appearance_documentSettings_selectionColor.tr();
|
||||
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (context, state) {
|
||||
return SettingListTile(
|
||||
label: label,
|
||||
resetButtonKey: const Key('DocumentSelectionColorResetButton'),
|
||||
onResetRequested: () => context
|
||||
..read<AppearanceSettingsCubit>().resetDocumentSelectionColor()
|
||||
..read<DocumentAppearanceCubit>().syncSelectionColor(null),
|
||||
trailing: [
|
||||
DocumentColorSettingButton(
|
||||
currentColor: state.selectionColor ??
|
||||
DefaultAppearanceSettings.getDefaultSelectionColor(context),
|
||||
previewWidgetBuilder: (color) => _SelectionColorValueWidget(
|
||||
selectionColor: color ??
|
||||
DefaultAppearanceSettings.getDefaultSelectionColor(context),
|
||||
),
|
||||
dialogTitle: label,
|
||||
onApply: (c) => context
|
||||
..read<AppearanceSettingsCubit>().setDocumentSelectionColor(c)
|
||||
..read<DocumentAppearanceCubit>().syncSelectionColor(c),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectionColorValueWidget extends StatelessWidget {
|
||||
const _SelectionColorValueWidget({required this.selectionColor});
|
||||
|
||||
final Color selectionColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// To avoid the text color changes when it is hovered in dark mode
|
||||
final textColor = Theme.of(context).colorScheme.onBackground;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
color: selectionColor,
|
||||
child: FlowyText(
|
||||
LocaleKeys.settings_appearance_documentSettings_app.tr(),
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
FlowyText(
|
||||
LocaleKeys.settings_appearance_documentSettings_flowy.tr(),
|
||||
color: textColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -3,12 +3,11 @@ 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/pages/settings_workspace_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';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
@ -78,10 +77,8 @@ class SettingsDialog extends StatelessWidget {
|
||||
didLogout: didLogout,
|
||||
didLogin: dismissDialog,
|
||||
);
|
||||
case SettingsPage.appearance:
|
||||
return const SettingsAppearanceView();
|
||||
case SettingsPage.language:
|
||||
return const SettingsLanguageView();
|
||||
case SettingsPage.workspace:
|
||||
return SettingsWorkspaceView(userProfile: user);
|
||||
case SettingsPage.files:
|
||||
return const SettingsFileSystemView();
|
||||
case SettingsPage.notifications:
|
||||
|
@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
DropdownMenuEntry<T> buildDropdownMenuEntry<T>(
|
||||
BuildContext context, {
|
||||
required T value,
|
||||
required String label,
|
||||
T? selectedValue,
|
||||
Widget? leadingWidget,
|
||||
Widget? trailingWidget,
|
||||
}) {
|
||||
return DropdownMenuEntry<T>(
|
||||
style: ButtonStyle(
|
||||
foregroundColor:
|
||||
MaterialStatePropertyAll(Theme.of(context).colorScheme.primary),
|
||||
padding: MaterialStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
),
|
||||
minimumSize: const MaterialStatePropertyAll(Size(double.infinity, 29)),
|
||||
maximumSize: const MaterialStatePropertyAll(Size(double.infinity, 29)),
|
||||
),
|
||||
value: value,
|
||||
label: label,
|
||||
leadingIcon: leadingWidget,
|
||||
labelWidget: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: FlowyText.medium(label, fontSize: 14, textAlign: TextAlign.start),
|
||||
),
|
||||
trailingIcon: Row(
|
||||
children: [
|
||||
if (trailingWidget != null) ...[
|
||||
trailingWidget,
|
||||
const HSpace(8),
|
||||
],
|
||||
value == selectedValue
|
||||
? const FlowySvg(FlowySvgs.check_s)
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/util/color_to_hex_string.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DocumentColorSettingButton extends StatelessWidget {
|
||||
class DocumentColorSettingButton extends StatefulWidget {
|
||||
const DocumentColorSettingButton({
|
||||
super.key,
|
||||
required this.currentColor,
|
||||
@ -27,41 +28,53 @@ class DocumentColorSettingButton extends StatelessWidget {
|
||||
|
||||
final void Function(Color selectedColorOnDialog) onApply;
|
||||
|
||||
@override
|
||||
State<DocumentColorSettingButton> createState() =>
|
||||
_DocumentColorSettingButtonState();
|
||||
}
|
||||
|
||||
class _DocumentColorSettingButtonState
|
||||
extends State<DocumentColorSettingButton> {
|
||||
late Color newColor = widget.currentColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
margin: const EdgeInsets.all(8),
|
||||
text: previewWidgetBuilder.call(currentColor),
|
||||
text: widget.previewWidgetBuilder.call(widget.currentColor),
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
expandText: false,
|
||||
onTap: () => Dialogs.show(
|
||||
context,
|
||||
child: _DocumentColorSettingDialog(
|
||||
currentColor: currentColor,
|
||||
previewWidgetBuilder: previewWidgetBuilder,
|
||||
dialogTitle: dialogTitle,
|
||||
onApply: onApply,
|
||||
),
|
||||
),
|
||||
onTap: () => SettingsAlertDialog(
|
||||
title: widget.dialogTitle,
|
||||
confirm: () {
|
||||
widget.onApply(newColor);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
children: [
|
||||
_DocumentColorSettingDialog(
|
||||
formKey: GlobalKey<FormState>(),
|
||||
currentColor: widget.currentColor,
|
||||
previewWidgetBuilder: widget.previewWidgetBuilder,
|
||||
onChanged: (color) => newColor = color,
|
||||
),
|
||||
],
|
||||
).show(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DocumentColorSettingDialog extends StatefulWidget {
|
||||
const _DocumentColorSettingDialog({
|
||||
required this.formKey,
|
||||
required this.currentColor,
|
||||
required this.previewWidgetBuilder,
|
||||
required this.dialogTitle,
|
||||
required this.onApply,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final GlobalKey<FormState> formKey;
|
||||
final Color currentColor;
|
||||
|
||||
final Widget Function(Color?) previewWidgetBuilder;
|
||||
|
||||
final String dialogTitle;
|
||||
|
||||
final void Function(Color selectedColorOnDialog) onApply;
|
||||
final void Function(Color selectedColor) onChanged;
|
||||
|
||||
@override
|
||||
State<_DocumentColorSettingDialog> createState() =>
|
||||
@ -76,16 +89,16 @@ class DocumentColorSettingDialogState
|
||||
late String currentColorHexString;
|
||||
late TextEditingController hexController;
|
||||
late TextEditingController opacityController;
|
||||
final _formKey = GlobalKey<FormState>(debugLabel: 'colorSettingForm');
|
||||
|
||||
void updateSelectedColor() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
if (widget.formKey.currentState!.validate()) {
|
||||
setState(() {
|
||||
final colorValue = int.tryParse(
|
||||
hexController.text.combineHexWithOpacity(opacityController.text),
|
||||
);
|
||||
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
|
||||
selectedColorOnDialog = Color(colorValue!);
|
||||
widget.onChanged(selectedColorOnDialog!);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -112,74 +125,43 @@ class DocumentColorSettingDialogState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyDialog(
|
||||
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 320),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
FlowyText(widget.dialogTitle),
|
||||
const VSpace(8),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 40,
|
||||
child: Center(
|
||||
child: widget.previewWidgetBuilder(
|
||||
selectedColorOnDialog,
|
||||
),
|
||||
),
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
height: 40,
|
||||
child: Center(
|
||||
child: widget.previewWidgetBuilder(
|
||||
selectedColorOnDialog,
|
||||
),
|
||||
const VSpace(8),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
_ColorSettingTextField(
|
||||
controller: hexController,
|
||||
labelText: LocaleKeys.editor_hexValue.tr(),
|
||||
hintText: '6fc9e7',
|
||||
onFieldSubmitted: (_) => updateSelectedColor(),
|
||||
validator: (hexValue) => validateHexValue(
|
||||
hexValue,
|
||||
opacityController.text,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
_ColorSettingTextField(
|
||||
controller: opacityController,
|
||||
labelText: LocaleKeys.editor_opacity.tr(),
|
||||
hintText: '50',
|
||||
onFieldSubmitted: (_) => updateSelectedColor(),
|
||||
validator: (value) => validateOpacityValue(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
RoundedTextButton(
|
||||
title: LocaleKeys.settings_appearance_documentSettings_apply.tr(),
|
||||
width: 100,
|
||||
height: 30,
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
if (selectedColorOnDialog != null &&
|
||||
selectedColorOnDialog != widget.currentColor) {
|
||||
widget.onApply.call(selectedColorOnDialog!);
|
||||
}
|
||||
} else {
|
||||
// error message will be shown below the text field
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
Form(
|
||||
key: widget.formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
_ColorSettingTextField(
|
||||
controller: hexController,
|
||||
labelText: LocaleKeys.editor_hexValue.tr(),
|
||||
hintText: '6fc9e7',
|
||||
onChanged: (_) => updateSelectedColor(),
|
||||
onFieldSubmitted: (_) => updateSelectedColor(),
|
||||
validator: (v) => validateHexValue(v, opacityController.text),
|
||||
),
|
||||
const VSpace(8),
|
||||
_ColorSettingTextField(
|
||||
controller: opacityController,
|
||||
labelText: LocaleKeys.editor_opacity.tr(),
|
||||
hintText: '50',
|
||||
onChanged: (_) => updateSelectedColor(),
|
||||
onFieldSubmitted: (_) => updateSelectedColor(),
|
||||
validator: (value) => validateOpacityValue(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -190,14 +172,15 @@ class _ColorSettingTextField extends StatelessWidget {
|
||||
required this.labelText,
|
||||
required this.hintText,
|
||||
required this.onFieldSubmitted,
|
||||
required this.validator,
|
||||
this.onChanged,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String labelText;
|
||||
final String hintText;
|
||||
|
||||
final void Function(String) onFieldSubmitted;
|
||||
final void Function(String)? onChanged;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
@ -209,17 +192,14 @@ class _ColorSettingTextField extends StatelessWidget {
|
||||
labelText: labelText,
|
||||
hintText: hintText,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: style.colorScheme.outline,
|
||||
),
|
||||
borderSide: BorderSide(color: style.colorScheme.outline),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: style.colorScheme.outline,
|
||||
),
|
||||
borderSide: BorderSide(color: style.colorScheme.outline),
|
||||
),
|
||||
),
|
||||
style: style.textTheme.bodyMedium,
|
||||
onChanged: onChanged,
|
||||
onFieldSubmitted: onFieldSubmitted,
|
||||
validator: validator,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
@ -227,10 +207,7 @@ class _ColorSettingTextField extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String? validateHexValue(
|
||||
String? hexValue,
|
||||
String opacityValue,
|
||||
) {
|
||||
String? validateHexValue(String? hexValue, String opacityValue) {
|
||||
if (hexValue == null || hexValue.isEmpty) {
|
||||
return LocaleKeys.settings_appearance_documentSettings_hexEmptyError.tr();
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.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/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
|
||||
class SettingAction extends StatelessWidget {
|
||||
const SettingAction({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.icon,
|
||||
this.label,
|
||||
this.tooltip,
|
||||
});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final Widget icon;
|
||||
final String? label;
|
||||
final String? tooltip;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconWidget = tooltip != null && tooltip!.isNotEmpty
|
||||
? FlowyTooltip(message: tooltip, child: icon)
|
||||
: icon;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onPressed,
|
||||
child: SizedBox(
|
||||
height: 26,
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
iconWidget,
|
||||
if (label != null) ...[
|
||||
const HSpace(4),
|
||||
FlowyText.regular(label!),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.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';
|
||||
|
||||
class FlowySettingListTile extends StatelessWidget {
|
||||
const FlowySettingListTile({
|
||||
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_ui/flowy_infra_ui.dart';
|
||||
|
||||
class SettingListTile extends StatelessWidget {
|
||||
const SettingListTile({
|
||||
super.key,
|
||||
this.resetTooltipText,
|
||||
this.resetButtonKey,
|
||||
@ -67,54 +67,3 @@ class FlowySettingListTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FlowySettingValueDropDown extends StatefulWidget {
|
||||
const FlowySettingValueDropDown({
|
||||
super.key,
|
||||
required this.currentValue,
|
||||
required this.popupBuilder,
|
||||
this.popoverKey,
|
||||
this.onClose,
|
||||
this.child,
|
||||
this.popoverController,
|
||||
this.offset,
|
||||
});
|
||||
|
||||
final String currentValue;
|
||||
final Key? popoverKey;
|
||||
final Widget Function(BuildContext) popupBuilder;
|
||||
final void Function()? onClose;
|
||||
final Widget? child;
|
||||
final PopoverController? popoverController;
|
||||
final Offset? offset;
|
||||
|
||||
@override
|
||||
State<FlowySettingValueDropDown> createState() =>
|
||||
_FlowySettingValueDropDownState();
|
||||
}
|
||||
|
||||
class _FlowySettingValueDropDownState extends State<FlowySettingValueDropDown> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
key: widget.popoverKey,
|
||||
controller: widget.popoverController,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: widget.popupBuilder,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 80,
|
||||
maxWidth: 160,
|
||||
maxHeight: 400,
|
||||
),
|
||||
offset: widget.offset,
|
||||
onClose: widget.onClose,
|
||||
child: widget.child ??
|
||||
FlowyTextButton(
|
||||
widget.currentValue,
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
class SettingValueDropDown extends StatefulWidget {
|
||||
const SettingValueDropDown({
|
||||
super.key,
|
||||
required this.currentValue,
|
||||
required this.popupBuilder,
|
||||
this.popoverKey,
|
||||
this.onClose,
|
||||
this.child,
|
||||
this.popoverController,
|
||||
this.offset,
|
||||
});
|
||||
|
||||
final String currentValue;
|
||||
final Key? popoverKey;
|
||||
final Widget Function(BuildContext) popupBuilder;
|
||||
final void Function()? onClose;
|
||||
final Widget? child;
|
||||
final PopoverController? popoverController;
|
||||
final Offset? offset;
|
||||
|
||||
@override
|
||||
State<SettingValueDropDown> createState() => _SettingValueDropDownState();
|
||||
}
|
||||
|
||||
class _SettingValueDropDownState extends State<SettingValueDropDown> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
key: widget.popoverKey,
|
||||
controller: widget.popoverController,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: widget.popupBuilder,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 80,
|
||||
maxWidth: 160,
|
||||
maxHeight: 400,
|
||||
),
|
||||
offset: widget.offset,
|
||||
onClose: widget.onClose,
|
||||
child: widget.child ??
|
||||
FlowyTextButton(
|
||||
widget.currentValue,
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
class SettingsActionableInput extends StatelessWidget {
|
||||
const SettingsActionableInput({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.focusNode,
|
||||
this.placeholder,
|
||||
this.onSave,
|
||||
this.actions = const [],
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final FocusNode? focusNode;
|
||||
final String? placeholder;
|
||||
final Function(String)? onSave;
|
||||
final List<Widget> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: FlowyTextField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
hintText: placeholder,
|
||||
autoFocus: false,
|
||||
isDense: false,
|
||||
suffixIconConstraints:
|
||||
BoxConstraints.tight(const Size(23 + 18, 24)),
|
||||
textStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
onSubmitted: onSave,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (actions.isNotEmpty) ...[
|
||||
const HSpace(8),
|
||||
SeparatedRow(
|
||||
separatorBuilder: () => const HSpace(16),
|
||||
children: actions,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,11 +1,19 @@
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
class SettingsBody extends StatelessWidget {
|
||||
const SettingsBody({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? description;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
@ -14,8 +22,18 @@ class SettingsBody extends StatelessWidget {
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
children: [
|
||||
SettingsHeader(title: title, description: description),
|
||||
Flexible(
|
||||
child: SeparatedColumn(
|
||||
separatorBuilder: () => const SettingsCategorySpacer(),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Renders a dashed divider
|
||||
///
|
||||
/// The length of each dash is the same as the gap.
|
||||
///
|
||||
class SettingsDashedDivider extends StatelessWidget {
|
||||
const SettingsDashedDivider({
|
||||
super.key,
|
||||
this.color,
|
||||
this.height,
|
||||
this.strokeWidth = 1.0,
|
||||
this.gap = 3.0,
|
||||
this.direction = Axis.horizontal,
|
||||
});
|
||||
|
||||
// The color of the divider, defaults to the theme's divider color
|
||||
final Color? color;
|
||||
|
||||
// The height of the divider, this will surround the divider equally
|
||||
final double? height;
|
||||
|
||||
// Thickness of the divider
|
||||
final double strokeWidth;
|
||||
|
||||
// Gap between the dashes
|
||||
final double gap;
|
||||
|
||||
// Direction of the divider
|
||||
final Axis direction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double padding =
|
||||
height != null && height! > 0 ? (height! - strokeWidth) / 2 : 0;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final items = _calculateItems(constraints);
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: direction == Axis.horizontal ? padding : 0,
|
||||
horizontal: direction == Axis.vertical ? padding : 0,
|
||||
),
|
||||
child: Wrap(
|
||||
direction: direction,
|
||||
children: List.generate(
|
||||
items,
|
||||
(index) => Container(
|
||||
margin: EdgeInsets.only(
|
||||
right: direction == Axis.horizontal ? gap : 0,
|
||||
bottom: direction == Axis.vertical ? gap : 0,
|
||||
),
|
||||
width: direction == Axis.horizontal ? gap : strokeWidth,
|
||||
height: direction == Axis.vertical ? gap : strokeWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: color ?? Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(1.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int _calculateItems(BoxConstraints constraints) {
|
||||
final double totalLength = direction == Axis.horizontal
|
||||
? constraints.maxWidth
|
||||
: constraints.maxHeight;
|
||||
|
||||
return (totalLength / (gap * 2)).floor();
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/flutter/af_dropdown_menu.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
class SettingsDropdown<T> extends StatefulWidget {
|
||||
const SettingsDropdown({
|
||||
super.key,
|
||||
required this.selectedOption,
|
||||
required this.options,
|
||||
this.onChanged,
|
||||
this.actions,
|
||||
this.expandWidth = true,
|
||||
});
|
||||
|
||||
final T selectedOption;
|
||||
final List<DropdownMenuEntry<T>> options;
|
||||
final void Function(T)? onChanged;
|
||||
final List<Widget>? actions;
|
||||
final bool expandWidth;
|
||||
|
||||
@override
|
||||
State<SettingsDropdown<T>> createState() => _SettingsDropdownState<T>();
|
||||
}
|
||||
|
||||
class _SettingsDropdownState<T> extends State<SettingsDropdown<T>> {
|
||||
late final TextEditingController controller = TextEditingController(
|
||||
text: widget.selectedOption is String
|
||||
? widget.selectedOption as String
|
||||
: widget.options
|
||||
.firstWhereOrNull((e) => e.value == widget.selectedOption)
|
||||
?.label ??
|
||||
'',
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AFDropdownMenu<T>(
|
||||
controller: controller,
|
||||
expandedInsets: widget.expandWidth ? EdgeInsets.zero : null,
|
||||
initialSelection: widget.selectedOption,
|
||||
dropdownMenuEntries: widget.options,
|
||||
menuStyle: MenuStyle(
|
||||
maximumSize:
|
||||
const MaterialStatePropertyAll(Size(double.infinity, 250)),
|
||||
elevation: const MaterialStatePropertyAll(10),
|
||||
shadowColor:
|
||||
MaterialStatePropertyAll(Colors.black.withOpacity(0.4)),
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
Theme.of(context).cardColor,
|
||||
),
|
||||
padding: const MaterialStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
),
|
||||
alignment: Alignment.bottomLeft,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 18,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
borderRadius: Corners.s8Border,
|
||||
),
|
||||
),
|
||||
onSelected: (v) async {
|
||||
v != null ? widget.onChanged?.call(v) : null;
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.actions?.isNotEmpty == true) ...[
|
||||
const HSpace(16),
|
||||
SeparatedRow(
|
||||
separatorBuilder: () => const HSpace(8),
|
||||
children: widget.actions!,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
|
||||
class SettingsRadioItem<T> {
|
||||
const SettingsRadioItem({
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.isSelected,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
final T value;
|
||||
final String label;
|
||||
final bool isSelected;
|
||||
final Widget? icon;
|
||||
}
|
||||
|
||||
class SettingsRadioSelect<T> extends StatelessWidget {
|
||||
const SettingsRadioSelect({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
this.selectedItem,
|
||||
});
|
||||
|
||||
final List<SettingsRadioItem<T>> items;
|
||||
final void Function(SettingsRadioItem<T>) onChanged;
|
||||
final SettingsRadioItem<T>? selectedItem;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
spacing: 24,
|
||||
runSpacing: 8,
|
||||
children: items
|
||||
.map(
|
||||
(i) => GestureDetector(
|
||||
onTap: () => onChanged(i),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: i.isSelected
|
||||
? AFThemeExtension.of(context).textColor
|
||||
: Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
if (i.icon != null) ...[i.icon!, const HSpace(4)],
|
||||
FlowyText.regular(i.label, fontSize: 14),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,8 +3,6 @@ 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';
|
||||
|
||||
class FeatureFlagsPage extends StatelessWidget {
|
||||
@ -15,15 +13,14 @@ class FeatureFlagsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsBody(
|
||||
title: 'Feature flags',
|
||||
children: [
|
||||
const SettingsHeader(title: 'Feature flags'),
|
||||
SeparatedColumn(
|
||||
children: FeatureFlag.data.entries
|
||||
.where((e) => e.key != FeatureFlag.unknown)
|
||||
.map((e) => _FeatureFlagItem(featureFlag: e.key))
|
||||
.toList(),
|
||||
),
|
||||
const SettingsCategorySpacer(),
|
||||
FlowyTextButton(
|
||||
'Restart the app to apply changes',
|
||||
fontSize: 16.0,
|
||||
|
@ -1,10 +1,11 @@
|
||||
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';
|
||||
@ -16,7 +17,6 @@ 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';
|
||||
|
||||
@ -34,11 +34,8 @@ class WorkspaceMembersPage extends StatelessWidget {
|
||||
listener: _showResultDialog,
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_appearance_members_title.tr(),
|
||||
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(),
|
||||
|
@ -9,7 +9,6 @@ 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';
|
||||
@ -41,10 +40,8 @@ class SettingCloud extends StatelessWidget {
|
||||
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||
children: [
|
||||
SettingsHeader(
|
||||
title: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||
),
|
||||
if (Env.enableCustomCloud)
|
||||
Row(
|
||||
children: [
|
||||
@ -55,17 +52,12 @@ class SettingCloud extends StatelessWidget {
|
||||
),
|
||||
CloudTypeSwitcher(
|
||||
cloudType: state.cloudType,
|
||||
onSelected: (newCloudType) {
|
||||
context.read<CloudSettingBloc>().add(
|
||||
CloudSettingEvent.updateCloudType(
|
||||
newCloudType,
|
||||
),
|
||||
);
|
||||
},
|
||||
onSelected: (type) => context
|
||||
.read<CloudSettingBloc>()
|
||||
.add(CloudSettingEvent.updateCloudType(type)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(8),
|
||||
_viewFromCloudType(state.cloudType),
|
||||
],
|
||||
);
|
||||
@ -73,9 +65,7 @@ class SettingCloud extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -1,67 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/util/theme_mode_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'theme_setting_entry_template.dart';
|
||||
|
||||
class BrightnessSetting extends StatelessWidget {
|
||||
const BrightnessSetting({required this.currentThemeMode, super.key});
|
||||
|
||||
final ThemeMode currentThemeMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_themeMode_label.tr(),
|
||||
hint: hintText,
|
||||
onResetRequested: context.read<AppearanceSettingsCubit>().resetThemeMode,
|
||||
trailing: [
|
||||
FlowySettingValueDropDown(
|
||||
currentValue: currentThemeMode.labelText,
|
||||
popupBuilder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_themeModeItemButton(context, ThemeMode.light),
|
||||
_themeModeItemButton(context, ThemeMode.dark),
|
||||
_themeModeItemButton(context, ThemeMode.system),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String get hintText =>
|
||||
'${LocaleKeys.settings_files_change.tr()} ${LocaleKeys.settings_appearance_themeMode_label.tr()} : ${Platform.isMacOS ? '⌘+Shift+L' : 'Ctrl+Shift+L'}';
|
||||
|
||||
Widget _themeModeItemButton(
|
||||
BuildContext context,
|
||||
ThemeMode themeMode,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(themeMode.labelText),
|
||||
rightIcon: currentThemeMode == themeMode
|
||||
? const FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentThemeMode != themeMode) {
|
||||
context.read<AppearanceSettingsCubit>().setThemeMode(themeMode);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
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/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_event.dart';
|
||||
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ColorSchemeSetting extends StatelessWidget {
|
||||
const ColorSchemeSetting({
|
||||
super.key,
|
||||
required this.currentTheme,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final String currentTheme;
|
||||
final DynamicPluginBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_theme.tr(),
|
||||
onResetRequested: context.read<AppearanceSettingsCubit>().resetTheme,
|
||||
trailing: [
|
||||
ColorSchemeUploadPopover(currentTheme: currentTheme, bloc: bloc),
|
||||
ColorSchemeUploadOverlayButton(bloc: bloc),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorSchemeUploadOverlayButton extends StatelessWidget {
|
||||
const ColorSchemeUploadOverlayButton({super.key, required this.bloc});
|
||||
|
||||
final DynamicPluginBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
width: 24,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.folder_m,
|
||||
size: const Size.square(16),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onPrimary,
|
||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
tooltipText: LocaleKeys.settings_appearance_themeUpload_uploadTheme.tr(),
|
||||
onPressed: () => Dialogs.show(
|
||||
context,
|
||||
child: BlocProvider<DynamicPluginBloc>.value(
|
||||
value: bloc,
|
||||
child: const FlowyDialog(
|
||||
constraints: BoxConstraints(maxHeight: 300),
|
||||
child: ThemeUploadWidget(),
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value == null) return;
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_appearance_themeUpload_uploadSuccess.tr(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ColorSchemeUploadPopover extends StatelessWidget {
|
||||
const ColorSchemeUploadPopover({
|
||||
super.key,
|
||||
required this.currentTheme,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final String currentTheme;
|
||||
final DynamicPluginBloc bloc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
child: FlowyTextButton(
|
||||
currentTheme,
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return IntrinsicWidth(
|
||||
child: BlocBuilder<DynamicPluginBloc, DynamicPluginState>(
|
||||
bloc: bloc..add(DynamicPluginEvent.load()),
|
||||
buildWhen: (previous, current) => current is Ready,
|
||||
builder: (context, state) {
|
||||
return state.maybeWhen(
|
||||
ready: (plugins) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...AppTheme.builtins.map(
|
||||
(theme) => _themeItemButton(context, theme.themeName),
|
||||
),
|
||||
if (plugins.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
...plugins
|
||||
.map((plugin) => plugin.theme)
|
||||
.whereType<AppTheme>()
|
||||
.map(
|
||||
(theme) => _themeItemButton(
|
||||
context,
|
||||
theme.themeName,
|
||||
false,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _themeItemButton(
|
||||
BuildContext context,
|
||||
String theme, [
|
||||
bool isBuiltin = true,
|
||||
]) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(theme),
|
||||
rightIcon: currentTheme == theme
|
||||
? const FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
)
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentTheme != theme) {
|
||||
context.read<AppearanceSettingsCubit>().setTheme(theme);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
),
|
||||
// when the custom theme is not the current theme, show the remove button
|
||||
if (!isBuiltin && currentTheme != theme)
|
||||
FlowyIconButton(
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.close_s,
|
||||
),
|
||||
width: 20,
|
||||
onPressed: () =>
|
||||
bloc.add(DynamicPluginEvent.removePlugin(name: theme)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/application/settings/prelude.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';
|
||||
|
||||
bool _prevSetting = false;
|
||||
|
||||
class CreateFileSettings extends StatelessWidget {
|
||||
CreateFileSettings({
|
||||
super.key,
|
||||
});
|
||||
|
||||
final cubit = CreateFileSettingsCubit(_prevSetting);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySettingListTile(
|
||||
label:
|
||||
LocaleKeys.settings_appearance_showNamingDialogWhenCreatingPage.tr(),
|
||||
trailing: [
|
||||
BlocProvider.value(
|
||||
value: cubit,
|
||||
child: BlocBuilder<CreateFileSettingsCubit, bool>(
|
||||
builder: (context, state) {
|
||||
_prevSetting = state;
|
||||
return Switch(
|
||||
value: state,
|
||||
splashRadius: 0,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (value) {
|
||||
cubit.toggle(value: value);
|
||||
_prevSetting = value;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
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_backend/protobuf/flowy-user/date_time.pbenum.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 'theme_setting_entry_template.dart';
|
||||
|
||||
class DateFormatSetting extends StatelessWidget {
|
||||
const DateFormatSetting({
|
||||
super.key,
|
||||
required this.currentFormat,
|
||||
});
|
||||
|
||||
final UserDateFormatPB currentFormat;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_dateFormat_label.tr(),
|
||||
trailing: [
|
||||
FlowySettingValueDropDown(
|
||||
currentValue: _formatLabel(currentFormat),
|
||||
popupBuilder: (_) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_formatItem(context, UserDateFormatPB.Locally),
|
||||
_formatItem(context, UserDateFormatPB.US),
|
||||
_formatItem(context, UserDateFormatPB.ISO),
|
||||
_formatItem(context, UserDateFormatPB.Friendly),
|
||||
_formatItem(context, UserDateFormatPB.DayMonthYear),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _formatItem(BuildContext context, UserDateFormatPB format) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(_formatLabel(format)),
|
||||
rightIcon:
|
||||
currentFormat == format ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||
onTap: () {
|
||||
if (currentFormat != format) {
|
||||
context.read<AppearanceSettingsCubit>().setDateFormat(format);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatLabel(UserDateFormatPB format) {
|
||||
switch (format) {
|
||||
case (UserDateFormatPB.Locally):
|
||||
return LocaleKeys.settings_appearance_dateFormat_local.tr();
|
||||
case (UserDateFormatPB.US):
|
||||
return LocaleKeys.settings_appearance_dateFormat_us.tr();
|
||||
case (UserDateFormatPB.ISO):
|
||||
return LocaleKeys.settings_appearance_dateFormat_iso.tr();
|
||||
case (UserDateFormatPB.Friendly):
|
||||
return LocaleKeys.settings_appearance_dateFormat_friendly.tr();
|
||||
case (UserDateFormatPB.DayMonthYear):
|
||||
return LocaleKeys.settings_appearance_dateFormat_dmy.tr();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,173 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'theme_setting_entry_template.dart';
|
||||
|
||||
class LayoutDirectionSetting extends StatelessWidget {
|
||||
const LayoutDirectionSetting({
|
||||
super.key,
|
||||
required this.currentLayoutDirection,
|
||||
});
|
||||
|
||||
final LayoutDirection currentLayoutDirection;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_layoutDirection_label.tr(),
|
||||
hint: LocaleKeys.settings_appearance_layoutDirection_hint.tr(),
|
||||
trailing: [
|
||||
FlowySettingValueDropDown(
|
||||
currentValue: _layoutDirectionLabelText(currentLayoutDirection),
|
||||
popupBuilder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_layoutDirectionItemButton(context, LayoutDirection.ltrLayout),
|
||||
_layoutDirectionItemButton(context, LayoutDirection.rtlLayout),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _layoutDirectionItemButton(
|
||||
BuildContext context,
|
||||
LayoutDirection direction,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(_layoutDirectionLabelText(direction)),
|
||||
rightIcon: currentLayoutDirection == direction
|
||||
? const FlowySvg(FlowySvgs.check_s)
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentLayoutDirection != direction) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLayoutDirection(direction);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _layoutDirectionLabelText(LayoutDirection direction) {
|
||||
switch (direction) {
|
||||
case (LayoutDirection.ltrLayout):
|
||||
return LocaleKeys.settings_appearance_layoutDirection_ltr.tr();
|
||||
case (LayoutDirection.rtlLayout):
|
||||
return LocaleKeys.settings_appearance_layoutDirection_rtl.tr();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TextDirectionSetting extends StatelessWidget {
|
||||
const TextDirectionSetting({
|
||||
super.key,
|
||||
required this.currentTextDirection,
|
||||
});
|
||||
|
||||
final AppFlowyTextDirection? currentTextDirection;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_textDirection_label.tr(),
|
||||
hint: LocaleKeys.settings_appearance_textDirection_hint.tr(),
|
||||
trailing: [
|
||||
FlowySettingValueDropDown(
|
||||
currentValue: _textDirectionLabelText(currentTextDirection),
|
||||
popupBuilder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_textDirectionItemButton(context, null),
|
||||
_textDirectionItemButton(context, AppFlowyTextDirection.ltr),
|
||||
_textDirectionItemButton(context, AppFlowyTextDirection.rtl),
|
||||
_textDirectionItemButton(context, AppFlowyTextDirection.auto),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _textDirectionItemButton(
|
||||
BuildContext context,
|
||||
AppFlowyTextDirection? textDirection,
|
||||
) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(_textDirectionLabelText(textDirection)),
|
||||
rightIcon: currentTextDirection == textDirection
|
||||
? const FlowySvg(FlowySvgs.check_s)
|
||||
: null,
|
||||
onTap: () {
|
||||
if (currentTextDirection != textDirection) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setTextDirection(textDirection);
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncDefaultTextDirection(textDirection?.name);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _textDirectionLabelText(AppFlowyTextDirection? textDirection) {
|
||||
switch (textDirection) {
|
||||
case (AppFlowyTextDirection.ltr):
|
||||
return LocaleKeys.settings_appearance_textDirection_ltr.tr();
|
||||
case (AppFlowyTextDirection.rtl):
|
||||
return LocaleKeys.settings_appearance_textDirection_rtl.tr();
|
||||
case (AppFlowyTextDirection.auto):
|
||||
return LocaleKeys.settings_appearance_textDirection_auto.tr();
|
||||
default:
|
||||
return LocaleKeys.settings_appearance_textDirection_fallback.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EnableRTLToolbarItemsSetting extends StatelessWidget {
|
||||
const EnableRTLToolbarItemsSetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const enableRTLSwitchKey = ValueKey('enable_rtl_toolbar_items_switch');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_enableRTLToolbarItems.tr(),
|
||||
trailing: [
|
||||
Switch(
|
||||
key: enableRTLSwitchKey,
|
||||
value: context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.state
|
||||
.enableRtlToolbarItems,
|
||||
splashRadius: 0,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
onChanged: (value) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setEnableRTLToolbarItems(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.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/widgets/settings_appearance/document_color_setting_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentCursorColorSetting extends StatelessWidget {
|
||||
const DocumentCursorColorSetting({
|
||||
super.key,
|
||||
required this.currentCursorColor,
|
||||
});
|
||||
|
||||
final Color currentCursorColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label =
|
||||
LocaleKeys.settings_appearance_documentSettings_cursorColor.tr();
|
||||
return FlowySettingListTile(
|
||||
label: label,
|
||||
resetButtonKey: const Key('DocumentCursorColorResetButton'),
|
||||
onResetRequested: () {
|
||||
context.read<AppearanceSettingsCubit>().resetDocumentCursorColor();
|
||||
context.read<DocumentAppearanceCubit>().syncCursorColor(null);
|
||||
},
|
||||
trailing: [
|
||||
DocumentColorSettingButton(
|
||||
key: const Key('DocumentCursorColorSettingButton'),
|
||||
currentColor: currentCursorColor,
|
||||
previewWidgetBuilder: (color) => _CursorColorValueWidget(
|
||||
cursorColor: color ??
|
||||
DefaultAppearanceSettings.getDefaultDocumentCursorColor(
|
||||
context,
|
||||
),
|
||||
),
|
||||
dialogTitle: label,
|
||||
onApply: (selectedColorOnDialog) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setDocumentCursorColor(selectedColorOnDialog);
|
||||
// update the state of document appearance cubit with latest cursor color
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncCursorColor(selectedColorOnDialog);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CursorColorValueWidget extends StatelessWidget {
|
||||
const _CursorColorValueWidget({
|
||||
required this.cursorColor,
|
||||
});
|
||||
|
||||
final Color cursorColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
color: cursorColor,
|
||||
width: 2,
|
||||
height: 16,
|
||||
),
|
||||
FlowyText(
|
||||
LocaleKeys.appName.tr(),
|
||||
// To avoid the text color changes when it is hovered in dark mode
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.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/widgets/settings_appearance/document_color_setting_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentSelectionColorSetting extends StatelessWidget {
|
||||
const DocumentSelectionColorSetting({
|
||||
super.key,
|
||||
required this.currentSelectionColor,
|
||||
});
|
||||
|
||||
final Color currentSelectionColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final label =
|
||||
LocaleKeys.settings_appearance_documentSettings_selectionColor.tr();
|
||||
|
||||
return FlowySettingListTile(
|
||||
label: label,
|
||||
resetButtonKey: const Key('DocumentSelectionColorResetButton'),
|
||||
onResetRequested: () {
|
||||
context.read<AppearanceSettingsCubit>().resetDocumentSelectionColor();
|
||||
context.read<DocumentAppearanceCubit>().syncSelectionColor(null);
|
||||
},
|
||||
trailing: [
|
||||
DocumentColorSettingButton(
|
||||
currentColor: currentSelectionColor,
|
||||
previewWidgetBuilder: (color) => _SelectionColorValueWidget(
|
||||
selectionColor: color ??
|
||||
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(
|
||||
context,
|
||||
),
|
||||
),
|
||||
dialogTitle: label,
|
||||
onApply: (selectedColorOnDialog) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setDocumentSelectionColor(selectedColorOnDialog);
|
||||
// update the state of document appearance cubit with latest selection color
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncSelectionColor(selectedColorOnDialog);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectionColorValueWidget extends StatelessWidget {
|
||||
const _SelectionColorValueWidget({
|
||||
required this.selectionColor,
|
||||
});
|
||||
|
||||
final Color selectionColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// To avoid the text color changes when it is hovered in dark mode
|
||||
final textColor = Theme.of(context).colorScheme.onBackground;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
color: selectionColor,
|
||||
child: FlowyText(
|
||||
LocaleKeys.settings_appearance_documentSettings_app.tr(),
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
FlowyText(
|
||||
LocaleKeys.settings_appearance_documentSettings_flowy.tr(),
|
||||
color: textColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,247 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/shared/google_fonts_extension.dart';
|
||||
import 'package:appflowy/util/font_family_extension.dart';
|
||||
import 'package:appflowy/workspace/application/appearance_defaults.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.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/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import 'levenshtein.dart';
|
||||
import 'theme_setting_entry_template.dart';
|
||||
|
||||
class ThemeFontFamilySetting extends StatefulWidget {
|
||||
const ThemeFontFamilySetting({
|
||||
super.key,
|
||||
required this.currentFontFamily,
|
||||
});
|
||||
|
||||
final String currentFontFamily;
|
||||
static Key textFieldKey = const Key('FontFamilyTextField');
|
||||
static Key resetButtonKey = const Key('FontFamilyResetButton');
|
||||
static Key popoverKey = const Key('FontFamilyPopover');
|
||||
|
||||
@override
|
||||
State<ThemeFontFamilySetting> createState() => _ThemeFontFamilySettingState();
|
||||
}
|
||||
|
||||
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
|
||||
resetButtonKey: ThemeFontFamilySetting.resetButtonKey,
|
||||
onResetRequested: () {
|
||||
context.read<AppearanceSettingsCubit>().resetFontFamily();
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
|
||||
},
|
||||
trailing: [
|
||||
FontFamilyDropDown(
|
||||
currentFontFamily: widget.currentFontFamily,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FontFamilyDropDown extends StatefulWidget {
|
||||
const FontFamilyDropDown({
|
||||
super.key,
|
||||
required this.currentFontFamily,
|
||||
this.onOpen,
|
||||
this.onClose,
|
||||
this.onFontFamilyChanged,
|
||||
this.child,
|
||||
this.popoverController,
|
||||
this.offset,
|
||||
this.showResetButton = false,
|
||||
this.onResetFont,
|
||||
});
|
||||
|
||||
final String currentFontFamily;
|
||||
final VoidCallback? onOpen;
|
||||
final VoidCallback? onClose;
|
||||
final void Function(String fontFamily)? onFontFamilyChanged;
|
||||
final Widget? child;
|
||||
final PopoverController? popoverController;
|
||||
final Offset? offset;
|
||||
final bool showResetButton;
|
||||
final VoidCallback? onResetFont;
|
||||
|
||||
@override
|
||||
State<FontFamilyDropDown> createState() => _FontFamilyDropDownState();
|
||||
}
|
||||
|
||||
class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
|
||||
final List<String> availableFonts = [
|
||||
defaultFontFamily,
|
||||
...GoogleFonts.asMap().keys,
|
||||
];
|
||||
final ValueNotifier<String> query = ValueNotifier('');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
query.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentValue = widget.currentFontFamily.fontFamilyDisplayName;
|
||||
return FlowySettingValueDropDown(
|
||||
popoverKey: ThemeFontFamilySetting.popoverKey,
|
||||
popoverController: widget.popoverController,
|
||||
currentValue: currentValue,
|
||||
onClose: () {
|
||||
query.value = '';
|
||||
widget.onClose?.call();
|
||||
},
|
||||
offset: widget.offset,
|
||||
child: widget.child,
|
||||
popupBuilder: (_) {
|
||||
widget.onOpen?.call();
|
||||
return CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
if (widget.showResetButton)
|
||||
SliverPersistentHeader(
|
||||
delegate: _ResetFontButton(
|
||||
onPressed: widget.onResetFont,
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: FlowyTextField(
|
||||
key: ThemeFontFamilySetting.textFieldKey,
|
||||
hintText:
|
||||
LocaleKeys.settings_appearance_fontFamily_search.tr(),
|
||||
autoFocus: false,
|
||||
debounceDuration: const Duration(milliseconds: 300),
|
||||
onChanged: (value) {
|
||||
query.value = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 4),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: query,
|
||||
builder: (context, value, child) {
|
||||
var displayed = availableFonts;
|
||||
if (value.isNotEmpty) {
|
||||
displayed = availableFonts
|
||||
.where(
|
||||
(font) => font
|
||||
.toLowerCase()
|
||||
.contains(value.toLowerCase().toString()),
|
||||
)
|
||||
.sorted((a, b) => levenshtein(a, b))
|
||||
.toList();
|
||||
}
|
||||
return SliverFixedExtentList.builder(
|
||||
itemBuilder: (context, index) => _fontFamilyItemButton(
|
||||
context,
|
||||
getGoogleFontSafely(displayed[index]),
|
||||
),
|
||||
itemCount: displayed.length,
|
||||
itemExtent: 32,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _fontFamilyItemButton(
|
||||
BuildContext context,
|
||||
TextStyle style,
|
||||
) {
|
||||
final buttonFontFamily =
|
||||
style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily;
|
||||
return Tooltip(
|
||||
message: buttonFontFamily,
|
||||
waitDuration: const Duration(milliseconds: 150),
|
||||
child: SizedBox(
|
||||
key: ValueKey(buttonFontFamily),
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
onHover: (_) => FocusScope.of(context).unfocus(),
|
||||
text: FlowyText.medium(
|
||||
buttonFontFamily.fontFamilyDisplayName,
|
||||
fontFamily: buttonFontFamily,
|
||||
),
|
||||
rightIcon:
|
||||
buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
|
||||
? const FlowySvg(FlowySvgs.check_s)
|
||||
: null,
|
||||
onTap: () {
|
||||
if (widget.onFontFamilyChanged != null) {
|
||||
widget.onFontFamilyChanged!(buttonFontFamily);
|
||||
} else {
|
||||
if (widget.currentFontFamily.parseFontFamilyName() !=
|
||||
buttonFontFamily) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setFontFamily(buttonFontFamily);
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncFontFamily(buttonFontFamily);
|
||||
}
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ResetFontButton extends SliverPersistentHeaderDelegate {
|
||||
_ResetFontButton({this.onPressed});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8, bottom: 8.0),
|
||||
child: FlowyTextButton(
|
||||
LocaleKeys.document_toolbar_resetToDefaultFont.tr(),
|
||||
fontColor: AFThemeExtension.of(context).textColor,
|
||||
fontHoverColor: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 12,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => 35;
|
||||
|
||||
@override
|
||||
double get minExtent => 35;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||
true;
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export 'brightness_setting.dart';
|
||||
export 'font_family_setting.dart';
|
||||
export 'color_scheme.dart';
|
||||
export 'direction_setting.dart';
|
||||
export 'document_cursor_color_setting.dart';
|
||||
export 'document_selection_color_setting.dart';
|
@ -1,63 +0,0 @@
|
||||
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_backend/protobuf/flowy-user/date_time.pbenum.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 'theme_setting_entry_template.dart';
|
||||
|
||||
class TimeFormatSetting extends StatelessWidget {
|
||||
const TimeFormatSetting({
|
||||
super.key,
|
||||
required this.currentFormat,
|
||||
});
|
||||
|
||||
final UserTimeFormatPB currentFormat;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => FlowySettingListTile(
|
||||
label: LocaleKeys.settings_appearance_timeFormat_label.tr(),
|
||||
trailing: [
|
||||
FlowySettingValueDropDown(
|
||||
currentValue: _formatLabel(currentFormat),
|
||||
popupBuilder: (_) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_formatItem(context, UserTimeFormatPB.TwentyFourHour),
|
||||
_formatItem(context, UserTimeFormatPB.TwelveHour),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Widget _formatItem(BuildContext context, UserTimeFormatPB format) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(_formatLabel(format)),
|
||||
rightIcon:
|
||||
currentFormat == format ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||
onTap: () {
|
||||
if (currentFormat != format) {
|
||||
context.read<AppearanceSettingsCubit>().setTimeFormat(format);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatLabel(UserTimeFormatPB format) {
|
||||
switch (format) {
|
||||
case (UserTimeFormatPB.TwentyFourHour):
|
||||
return LocaleKeys.settings_appearance_timeFormat_twentyFourHour.tr();
|
||||
case (UserTimeFormatPB.TwelveHour):
|
||||
return LocaleKeys.settings_appearance_timeFormat_twelveHour.tr();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
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_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'settings_appearance/settings_appearance.dart';
|
||||
|
||||
class SettingsAppearanceView extends StatelessWidget {
|
||||
const SettingsAppearanceView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/strin
|
||||
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/theme_extension.dart';
|
||||
@ -22,10 +21,8 @@ class SettingsShortcutsView extends StatelessWidget {
|
||||
create: (_) =>
|
||||
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
|
||||
child: SettingsBody(
|
||||
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||
children: [
|
||||
SettingsHeader(
|
||||
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||
),
|
||||
BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
||||
builder: (_, state) => switch (state.status) {
|
||||
ShortcutsStatus.initial ||
|
||||
@ -110,8 +107,8 @@ class ShortcutsListTile extends StatelessWidget {
|
||||
),
|
||||
FlowyTextButton(
|
||||
shortcutEvent.command,
|
||||
fillColor: Colors.transparent,
|
||||
fontColor: AFThemeExtension.of(context).textColor,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () => showKeyListenerDialog(context),
|
||||
),
|
||||
],
|
||||
|
@ -4,7 +4,6 @@ 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';
|
||||
@ -17,16 +16,16 @@ class SettingsFileSystemView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsBody(
|
||||
children: [
|
||||
SettingsHeader(title: LocaleKeys.settings_menu_files.tr()),
|
||||
const SettingsFileLocationCustomizer(),
|
||||
const SettingsCategorySpacer(),
|
||||
title: LocaleKeys.settings_menu_files.tr(),
|
||||
children: const [
|
||||
SettingsFileLocationCustomizer(),
|
||||
SettingsCategorySpacer(),
|
||||
if (kDebugMode) ...[
|
||||
const SettingsExportFileWidget(),
|
||||
SettingsExportFileWidget(),
|
||||
],
|
||||
const ImportAppFlowyData(),
|
||||
const SettingsCategorySpacer(),
|
||||
const SettingsFileCacheWidget(),
|
||||
ImportAppFlowyData(),
|
||||
SettingsCategorySpacer(),
|
||||
SettingsFileCacheWidget(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,119 +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/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/language.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsLanguageView extends StatelessWidget {
|
||||
const SettingsLanguageView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageSelector extends StatelessWidget {
|
||||
const LanguageSelector({super.key, required this.currentLocale});
|
||||
|
||||
final Locale currentLocale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
child: FlowyTextButton(
|
||||
languageFromLocale(currentLocale),
|
||||
fontColor: Theme.of(context).colorScheme.onBackground,
|
||||
fillColor: Colors.transparent,
|
||||
onPressed: () {},
|
||||
),
|
||||
popupBuilder: (BuildContext context) {
|
||||
final allLocales = EasyLocalization.of(context)!.supportedLocales;
|
||||
return LanguageItemsListView(allLocales: allLocales);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageItemsListView extends StatelessWidget {
|
||||
const LanguageItemsListView({
|
||||
super.key,
|
||||
required this.allLocales,
|
||||
});
|
||||
|
||||
final List<Locale> allLocales;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// get current locale from cubit
|
||||
final state = context.watch<AppearanceSettingsCubit>().state;
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, index) {
|
||||
final locale = allLocales[index];
|
||||
return LanguageItem(
|
||||
locale: locale,
|
||||
currentLocale: state.locale,
|
||||
);
|
||||
},
|
||||
itemCount: allLocales.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LanguageItem extends StatelessWidget {
|
||||
const LanguageItem({
|
||||
super.key,
|
||||
required this.locale,
|
||||
required this.currentLocale,
|
||||
});
|
||||
|
||||
final Locale locale;
|
||||
final Locale currentLocale;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
languageFromLocale(locale),
|
||||
),
|
||||
rightIcon:
|
||||
currentLocale == locale ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||
onTap: () {
|
||||
if (currentLocale != locale) {
|
||||
context.read<AppearanceSettingsCubit>().setLocale(context, locale);
|
||||
}
|
||||
PopoverContainer.of(context).close();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -55,19 +55,22 @@ class SettingsMenu extends StatelessWidget {
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.appearance,
|
||||
page: SettingsPage.workspace,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_appearance.tr(),
|
||||
icon: const Icon(Icons.brightness_4),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.language,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_menu_language.tr(),
|
||||
icon: const Icon(Icons.translate),
|
||||
label: LocaleKeys.settings_workspacePage_menuLabel.tr(),
|
||||
icon: const FlowySvg(FlowySvgs.settings_workplace_m),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
if (FeatureFlag.membersSettings.isOn &&
|
||||
userProfile.authenticator ==
|
||||
AuthenticatorPB.AppFlowyCloud)
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.member,
|
||||
selectedPage: currentPage,
|
||||
label: LocaleKeys.settings_appearance_members_label.tr(),
|
||||
icon: const Icon(Icons.people),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
SettingsMenuElement(
|
||||
page: SettingsPage.files,
|
||||
selectedPage: currentPage,
|
||||
@ -96,16 +99,6 @@ class SettingsMenu extends StatelessWidget {
|
||||
icon: const 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: const Icon(Icons.people),
|
||||
changeSelectedPage: changeSelectedPage,
|
||||
),
|
||||
if (kDebugMode)
|
||||
SettingsMenuElement(
|
||||
// no need to translate this page
|
||||
|
@ -2,9 +2,8 @@ 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/setting_list_tile.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_bloc/flutter_bloc.dart';
|
||||
|
||||
@ -16,9 +15,9 @@ class SettingsNotificationsView extends StatelessWidget {
|
||||
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
|
||||
builder: (context, state) {
|
||||
return SettingsBody(
|
||||
title: LocaleKeys.settings_menu_notifications.tr(),
|
||||
children: [
|
||||
SettingsHeader(title: LocaleKeys.settings_menu_notifications.tr()),
|
||||
FlowySettingListTile(
|
||||
SettingListTile(
|
||||
label: LocaleKeys.settings_notifications_enableNotifications_label
|
||||
.tr(),
|
||||
hint: LocaleKeys.settings_notifications_enableNotifications_hint
|
||||
|
@ -240,6 +240,7 @@ flutter:
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/images/appearance/
|
||||
- assets/images/built_in_cover_images/
|
||||
- assets/flowy_icons/
|
||||
- assets/flowy_icons/16x/
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/levenshtein.dart';
|
||||
import 'package:appflowy/util/levenshtein.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.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:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||
|
||||
# Set fallback configurations for older versions of the flutter tool.
|
||||
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||
endif()
|
||||
|
||||
# === Flutter Library ===
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||
|
||||
@ -92,7 +97,7 @@ add_custom_command(
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||
windows-x64 $<CONFIG>
|
||||
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
|
@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_679_28469" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<rect width="24" height="24" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_679_28469)">
|
||||
<path d="M6 22L2 18L6 14L7.425 15.4L5.825 17H18.175L16.6 15.4L18 14L22 18L18 22L16.575 20.6L18.175 19H5.825L7.4 20.6L6 22ZM6.9 13L11 2H13L17.1 13H15.2L14.25 10.2H9.8L8.8 13H6.9ZM10.35 8.6H13.65L12.05 4.05H11.95L10.35 8.6Z" fill="#1C1B1F"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 549 B |
8
frontend/resources/flowy_icons/24x/textdirection_ltr.svg
Normal file
8
frontend/resources/flowy_icons/24x/textdirection_ltr.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_679_28451" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<rect width="24" height="24" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_679_28451)">
|
||||
<path d="M9 15V10C7.9 10 6.95833 9.60833 6.175 8.825C5.39167 8.04167 5 7.1 5 6C5 4.9 5.39167 3.95833 6.175 3.175C6.95833 2.39167 7.9 2 9 2H17V4H15V15H13V4H11V15H9ZM9 8V4C8.45 4 7.97917 4.19583 7.5875 4.5875C7.19583 4.97917 7 5.45 7 6C7 6.55 7.19583 7.02083 7.5875 7.4125C7.97917 7.80417 8.45 8 9 8ZM17 22L15.6 20.6L17.2 19H3V17H17.2L15.6 15.4L17 14L21 18L17 22Z" fill="#1C1B1F"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 689 B |
8
frontend/resources/flowy_icons/24x/textdirection_rtl.svg
Normal file
8
frontend/resources/flowy_icons/24x/textdirection_rtl.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_679_28460" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
|
||||
<rect width="24" height="24" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_679_28460)">
|
||||
<path d="M9 15V10C7.9 10 6.95833 9.60833 6.175 8.825C5.39167 8.04167 5 7.1 5 6C5 4.9 5.39167 3.95833 6.175 3.175C6.95833 2.39167 7.9 2 9 2H17V4H15V15H13V4H11V15H9ZM6.8 19L8.4 20.6L7 22L3 18L7 14L8.4 15.4L6.8 17H21V19H6.8ZM9 8V4C8.45 4 7.97917 4.19583 7.5875 4.5875C7.19583 4.97917 7 5.45 7 6C7 6.55 7.19583 7.02083 7.5875 7.4125C7.97917 7.80417 8.45 8 9 8Z" fill="#1C1B1F"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 684 B |
@ -347,6 +347,79 @@
|
||||
"logoutLabel": "Log out"
|
||||
}
|
||||
},
|
||||
"workspacePage": {
|
||||
"menuLabel": "Workspace",
|
||||
"title": "Workspace",
|
||||
"description": "Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.",
|
||||
"workspaceName": {
|
||||
"title": "Workspace name",
|
||||
"savedMessage": "Saved workspace name"
|
||||
},
|
||||
"workspaceIcon": {
|
||||
"title": "Workspace icon",
|
||||
"description": "Customize your workspace appearance, theme, font, text layout, date, time, and language."
|
||||
},
|
||||
"appearance": {
|
||||
"title": "Appearance",
|
||||
"description": "Customize your workspace appearance, theme, font, text layout, date, time, and language.",
|
||||
"options": {
|
||||
"system": "Auto",
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"description": "Select a preset theme, or upload your own custom theme."
|
||||
},
|
||||
"workspaceFont": {
|
||||
"title": "Workspace font"
|
||||
},
|
||||
"textDirection": {
|
||||
"title": "Text direction",
|
||||
"leftToRight": "Left to right",
|
||||
"rightToLeft": "Right to left",
|
||||
"auto": "Auto",
|
||||
"enableRTLItems": "Enable RTL toolbar items"
|
||||
},
|
||||
"layoutDirection": {
|
||||
"title": "Layout direction",
|
||||
"leftToRight": "Left to right",
|
||||
"rightToLeft": "Right to left"
|
||||
},
|
||||
"dateTime": {
|
||||
"title": "Date & time",
|
||||
"example": "{} at {} ({})",
|
||||
"24HourTime": "24-hour time",
|
||||
"dateFormat": {
|
||||
"label": "Date format",
|
||||
"local": "Local",
|
||||
"us": "US",
|
||||
"iso": "ISO",
|
||||
"friendly": "Friendly",
|
||||
"dmy": "D/M/Y"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"title": "Language"
|
||||
},
|
||||
"deleteWorkspacePrompt": {
|
||||
"title": "Delete workspace",
|
||||
"content": "Are you sure you want to delete this workspace? This action cannot be undone."
|
||||
},
|
||||
"leaveWorkspacePrompt": {
|
||||
"title": "Leave workspace",
|
||||
"content": "Are you sure you want to leave this workspace? You will lose access to all pages and data within it."
|
||||
},
|
||||
"manageWorkspace": {
|
||||
"title": "Manage workspace",
|
||||
"leaveWorkspace": "Leave workspace",
|
||||
"deleteWorkspace": "Delete workspace"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"reset": "Reset"
|
||||
},
|
||||
"menu": {
|
||||
"appearance": "Appearance",
|
||||
"language": "Language",
|
||||
|
@ -13,6 +13,7 @@ pub(crate) enum UserNotification {
|
||||
DidUpdateUserProfile = 2,
|
||||
DidUpdateUserWorkspaces = 3,
|
||||
DidUpdateCloudConfig = 4,
|
||||
DidUpdateUserWorkspace = 5,
|
||||
}
|
||||
|
||||
impl std::convert::From<UserNotification> for i32 {
|
||||
|
@ -39,11 +39,13 @@ use crate::services::collab_interact::{CollabInteract, DefaultCollabInteract};
|
||||
|
||||
use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset};
|
||||
use crate::user_manager::manager_user_encryption::validate_encryption_sign;
|
||||
use crate::user_manager::manager_user_workspace::save_user_workspaces;
|
||||
use crate::user_manager::manager_user_workspace::save_all_user_workspaces;
|
||||
use crate::user_manager::user_login_state::UserAuthProcess;
|
||||
use crate::{errors::FlowyError, notification::*};
|
||||
use flowy_user_pub::session::Session;
|
||||
|
||||
use super::manager_user_workspace::save_user_workspace;
|
||||
|
||||
pub struct UserManager {
|
||||
pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>,
|
||||
pub(crate) store_preferences: Arc<StorePreferences>,
|
||||
@ -708,7 +710,7 @@ impl UserManager {
|
||||
self.set_anon_user(session.clone());
|
||||
}
|
||||
|
||||
save_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?;
|
||||
save_all_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?;
|
||||
info!(
|
||||
"Save new user profile to disk, authenticator: {:?}",
|
||||
authenticator
|
||||
@ -779,13 +781,13 @@ impl UserManager {
|
||||
}
|
||||
|
||||
// Save the old user workspace setting.
|
||||
save_user_workspaces(
|
||||
save_user_workspace(
|
||||
old_user.session.user_id,
|
||||
self
|
||||
.authenticate_user
|
||||
.database
|
||||
.get_connection(old_user.session.user_id)?,
|
||||
&[old_user.session.user_workspace.clone()],
|
||||
&old_user.session.user_workspace.clone(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use flowy_user_pub::entities::{
|
||||
};
|
||||
use lib_dispatch::prelude::af_spawn;
|
||||
|
||||
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB};
|
||||
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB, UserWorkspacePB};
|
||||
use crate::migrations::AnonUser;
|
||||
use crate::notification::{send_notification, UserNotification};
|
||||
use crate::services::data_import::{
|
||||
@ -239,7 +239,14 @@ impl UserManager {
|
||||
user_workspace.icon = new_workspace_icon.to_string();
|
||||
}
|
||||
|
||||
save_user_workspaces(uid, conn, &[user_workspace])
|
||||
let _ = save_user_workspace(uid, conn, &user_workspace);
|
||||
|
||||
let payload: UserWorkspacePB = user_workspace.clone().into();
|
||||
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace)
|
||||
.payload(payload)
|
||||
.send();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(self), err)]
|
||||
@ -371,7 +378,7 @@ impl UserManager {
|
||||
af_spawn(async move {
|
||||
if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await {
|
||||
if let Ok(conn) = pool.get() {
|
||||
let _ = save_user_workspaces(uid, conn, &new_user_workspaces);
|
||||
let _ = save_all_user_workspaces(uid, conn, &new_user_workspaces);
|
||||
let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces);
|
||||
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces)
|
||||
.payload(repeated_workspace_pbs)
|
||||
@ -403,7 +410,48 @@ impl UserManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_user_workspaces(
|
||||
/// This method is used to save one user workspace to the SQLite database
|
||||
///
|
||||
/// If the workspace is already persisted in the database, it will be overridden.
|
||||
///
|
||||
/// Consider using [save_all_user_workspaces] if you need to override all workspaces of the user.
|
||||
///
|
||||
pub fn save_user_workspace(
|
||||
uid: i64,
|
||||
mut conn: DBConnection,
|
||||
user_workspace: &UserWorkspace,
|
||||
) -> FlowyResult<()> {
|
||||
conn.immediate_transaction(|conn| {
|
||||
let user_workspace = UserWorkspaceTable::try_from((uid, user_workspace))?;
|
||||
let affected_rows = diesel::update(
|
||||
user_workspace_table::dsl::user_workspace_table
|
||||
.filter(user_workspace_table::id.eq(&user_workspace.id)),
|
||||
)
|
||||
.set((
|
||||
user_workspace_table::name.eq(&user_workspace.name),
|
||||
user_workspace_table::created_at.eq(&user_workspace.created_at),
|
||||
user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id),
|
||||
user_workspace_table::icon.eq(&user_workspace.icon),
|
||||
))
|
||||
.execute(conn)?;
|
||||
|
||||
if affected_rows == 0 {
|
||||
diesel::insert_into(user_workspace_table::table)
|
||||
.values(user_workspace)
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
Ok::<(), FlowyError>(())
|
||||
})
|
||||
}
|
||||
|
||||
/// This method is used to save the user workspaces (plural) to the SQLite database
|
||||
///
|
||||
/// The workspaces provided in [user_workspaces] will override the existing workspaces in the database.
|
||||
///
|
||||
/// Consider using [save_user_workspace] if you only need to save a single workspace.
|
||||
///
|
||||
pub fn save_all_user_workspaces(
|
||||
uid: i64,
|
||||
mut conn: DBConnection,
|
||||
user_workspaces: &[UserWorkspace],
|
||||
|
Loading…
Reference in New Issue
Block a user