mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00: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;
|
Finder success;
|
||||||
|
|
||||||
|
final Finder items = find.byType(WorkspaceMenuItem);
|
||||||
|
|
||||||
// delete the newly created workspace
|
// delete the newly created workspace
|
||||||
await tester.openCollaborativeWorkspaceMenu();
|
await tester.openCollaborativeWorkspaceMenu();
|
||||||
final Finder items = find.byType(WorkspaceMenuItem);
|
await tester.pumpUntilFound(items);
|
||||||
|
|
||||||
expect(items, findsNWidgets(2));
|
expect(items, findsNWidgets(2));
|
||||||
expect(
|
expect(
|
||||||
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
import 'notifications_settings_test.dart' as notifications_settings_test;
|
import 'notifications_settings_test.dart' as notifications_settings_test;
|
||||||
import 'user_language_test.dart' as user_language_test;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
notifications_settings_test.main();
|
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 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
@ -23,31 +25,35 @@ void main() {
|
|||||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||||
|
|
||||||
await tester.openSettings();
|
await tester.openSettings();
|
||||||
await tester.openSettingsPage(SettingsPage.appearance);
|
await tester.openSettingsPage(SettingsPage.workspace);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
tester.expectToSeeText(
|
final appFinder = find.byType(MaterialApp).first;
|
||||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
ThemeMode? themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||||
);
|
|
||||||
|
expect(themeMode, ThemeMode.system);
|
||||||
|
|
||||||
await tester.tapButton(
|
await tester.tapButton(
|
||||||
find.bySemanticsLabel(
|
find.bySemanticsLabel(
|
||||||
LocaleKeys.settings_appearance_themeMode_system.tr(),
|
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||||
|
expect(themeMode, ThemeMode.light);
|
||||||
|
|
||||||
await tester.tapButton(
|
await tester.tapButton(
|
||||||
find.bySemanticsLabel(
|
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.tap(find.byType(SettingsDialog));
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await FlowyTestKeyboard.simulateKeyDownEvent(
|
await FlowyTestKeyboard.simulateKeyDownEvent(
|
||||||
@ -60,12 +66,10 @@ void main() {
|
|||||||
],
|
],
|
||||||
tester: tester,
|
tester: tester,
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
tester.expectToSeeText(
|
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
|
||||||
LocaleKeys.settings_appearance_themeMode_light.tr(),
|
expect(themeMode, ThemeMode.light);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('show or hide home menu', (tester) async {
|
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:flutter_test/flutter_test.dart';
|
||||||
import 'package:integration_test/integration_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/board/board_test_runner.dart' as board_test_runner;
|
||||||
import 'desktop/settings/settings_runner.dart' as settings_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/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/emoji_shortcut_test.dart' as emoji_shortcut_test;
|
||||||
import 'desktop/uncategorized/empty_test.dart' as first_test;
|
import 'desktop/uncategorized/empty_test.dart' as first_test;
|
||||||
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
|
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
|
||||||
@ -26,7 +24,6 @@ Future<void> runIntegration3OnDesktop() async {
|
|||||||
emoji_shortcut_test.main();
|
emoji_shortcut_test.main();
|
||||||
hotkeys_test.main();
|
hotkeys_test.main();
|
||||||
emoji_shortcut_test.main();
|
emoji_shortcut_test.main();
|
||||||
appearance_test_runner.main();
|
|
||||||
settings_test_runner.main();
|
settings_test_runner.main();
|
||||||
share_markdown_test.main();
|
share_markdown_test.main();
|
||||||
import_files_test.main();
|
import_files_test.main();
|
||||||
|
@ -173,27 +173,39 @@ extension AppFlowyTestBase on WidgetTester {
|
|||||||
int buttons = kPrimaryButton,
|
int buttons = kPrimaryButton,
|
||||||
bool warnIfMissed = false,
|
bool warnIfMissed = false,
|
||||||
int milliseconds = 500,
|
int milliseconds = 500,
|
||||||
|
bool pumpAndSettle = true,
|
||||||
}) async {
|
}) async {
|
||||||
await tap(
|
await tap(
|
||||||
finder,
|
finder,
|
||||||
buttons: buttons,
|
buttons: buttons,
|
||||||
warnIfMissed: warnIfMissed,
|
warnIfMissed: warnIfMissed,
|
||||||
);
|
);
|
||||||
await pumpAndSettle(
|
|
||||||
Duration(milliseconds: milliseconds),
|
if (pumpAndSettle) {
|
||||||
EnginePhase.sendSemanticsUpdate,
|
await this.pumpAndSettle(
|
||||||
const Duration(seconds: 5),
|
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);
|
Finder button = find.text(tr, findRichText: true, skipOffstage: false);
|
||||||
if (button.evaluate().isEmpty) {
|
if (button.evaluate().isEmpty) {
|
||||||
button = find.byWidgetPredicate(
|
button = find.byWidgetPredicate(
|
||||||
(widget) => widget is FlowyText && widget.text == tr,
|
(widget) => widget is FlowyText && widget.text == tr,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await tapButton(button, milliseconds: milliseconds);
|
await tapButton(
|
||||||
|
button,
|
||||||
|
milliseconds: milliseconds,
|
||||||
|
pumpAndSettle: pumpAndSettle,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> doubleTapAt(
|
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_button.dart';
|
||||||
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
import 'package:appflowy/workspace/presentation/settings/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/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
@ -511,8 +510,9 @@ extension CommonOperations on WidgetTester {
|
|||||||
|
|
||||||
final workspace = find.byType(SidebarWorkspace);
|
final workspace = find.byType(SidebarWorkspace);
|
||||||
expect(workspace, findsOneWidget);
|
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 {
|
Future<void> createCollaborativeWorkspace(String name) async {
|
||||||
@ -527,7 +527,8 @@ extension CommonOperations on WidgetTester {
|
|||||||
// click the create button
|
// click the create button
|
||||||
final createButton = find.byKey(createWorkspaceButtonKey);
|
final createButton = find.byKey(createWorkspaceButtonKey);
|
||||||
expect(createButton, findsOneWidget);
|
expect(createButton, findsOneWidget);
|
||||||
await tapButton(createButton);
|
await tapButton(createButton, pumpAndSettle: false);
|
||||||
|
await pump(const Duration(seconds: 5));
|
||||||
|
|
||||||
// see the create workspace dialog
|
// see the create workspace dialog
|
||||||
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
|
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
|
||||||
@ -536,7 +537,8 @@ extension CommonOperations on WidgetTester {
|
|||||||
// input the workspace name
|
// input the workspace name
|
||||||
await enterText(find.byType(TextField), 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
|
// 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/application/settings/prelude.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/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/settings_dialog.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||||
import 'package:flutter_test/flutter_test.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 '../desktop/board/board_hide_groups_test.dart';
|
||||||
|
|
||||||
import 'base.dart';
|
import 'base.dart';
|
||||||
|
import 'common_operations.dart';
|
||||||
|
|
||||||
extension AppFlowySettings on WidgetTester {
|
extension AppFlowySettings on WidgetTester {
|
||||||
/// Open settings page
|
/// Open settings page
|
||||||
@ -77,12 +79,21 @@ extension AppFlowySettings on WidgetTester {
|
|||||||
// go to settings page and toggle enable RTL toolbar items
|
// go to settings page and toggle enable RTL toolbar items
|
||||||
Future<void> toggleEnableRTLToolbarItems() async {
|
Future<void> toggleEnableRTLToolbarItems() async {
|
||||||
await openSettings();
|
await openSettings();
|
||||||
await openSettingsPage(SettingsPage.appearance);
|
await openSettingsPage(SettingsPage.workspace);
|
||||||
|
|
||||||
final switchButton =
|
final scrollable = find.findSettingsScrollable();
|
||||||
find.byKey(EnableRTLToolbarItemsSetting.enableRTLSwitchKey);
|
await scrollUntilVisible(
|
||||||
expect(switchButton, findsOneWidget);
|
find.byType(EnableRTLItemsSwitcher),
|
||||||
await tapButton(switchButton);
|
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
|
// tap anywhere to close the settings page
|
||||||
await tapAt(Offset.zero);
|
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,
|
workspace: currentWorkspace,
|
||||||
iconSize: 26,
|
iconSize: 26,
|
||||||
enableEdit: false,
|
enableEdit: false,
|
||||||
|
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||||
|
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||||
|
currentWorkspace.workspaceId,
|
||||||
|
result.emoji,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(8),
|
const HSpace(8),
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/widgets.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/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
// Only works on mobile.
|
// Only works on mobile.
|
||||||
@ -105,6 +107,12 @@ class _WorkspaceMenuItem extends StatelessWidget {
|
|||||||
enableEdit: false,
|
enableEdit: false,
|
||||||
iconSize: 26,
|
iconSize: 26,
|
||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
|
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||||
|
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||||
|
workspace.workspaceId,
|
||||||
|
result.emoji,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
trailing: workspace.workspaceId == currentWorkspace.workspaceId
|
trailing: workspace.workspaceId == currentWorkspace.workspaceId
|
||||||
? const FlowySvg(
|
? const FlowySvg(
|
||||||
|
@ -1,13 +1,28 @@
|
|||||||
|
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/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_backend/log.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/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:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
final customizeFontToolbarItem = ToolbarItem(
|
final customizeFontToolbarItem = ToolbarItem(
|
||||||
id: 'editor.font',
|
id: 'editor.font',
|
||||||
@ -16,10 +31,12 @@ final customizeFontToolbarItem = ToolbarItem(
|
|||||||
builder: (context, editorState, highlightColor, _) {
|
builder: (context, editorState, highlightColor, _) {
|
||||||
final selection = editorState.selection!;
|
final selection = editorState.selection!;
|
||||||
final popoverController = PopoverController();
|
final popoverController = PopoverController();
|
||||||
|
final String? currentFontFamily = editorState
|
||||||
|
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily);
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: FontFamilyDropDown(
|
child: FontFamilyDropDown(
|
||||||
currentFontFamily: '',
|
currentFontFamily: currentFontFamily ?? '',
|
||||||
offset: const Offset(0, 12),
|
offset: const Offset(0, 12),
|
||||||
popoverController: popoverController,
|
popoverController: popoverController,
|
||||||
onOpen: () => keepEditorFocusNotifier.increase(),
|
onOpen: () => keepEditorFocusNotifier.increase(),
|
||||||
@ -35,8 +52,11 @@ final customizeFontToolbarItem = ToolbarItem(
|
|||||||
Log.error('Failed to set font family: $e');
|
Log.error('Failed to set font family: $e');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResetFont: () async => editorState
|
onResetFont: () async {
|
||||||
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}),
|
popoverController.close();
|
||||||
|
await editorState
|
||||||
|
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null});
|
||||||
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: FlowyTooltip(
|
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 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.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/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
@ -48,9 +49,9 @@ class EditorStyleCustomizer {
|
|||||||
return EditorStyle.desktop(
|
return EditorStyle.desktop(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
cursorColor: appearance.cursorColor ??
|
cursorColor: appearance.cursorColor ??
|
||||||
DefaultAppearanceSettings.getDefaultDocumentCursorColor(context),
|
DefaultAppearanceSettings.getDefaultCursorColor(context),
|
||||||
selectionColor: appearance.selectionColor ??
|
selectionColor: appearance.selectionColor ??
|
||||||
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(context),
|
DefaultAppearanceSettings.getDefaultSelectionColor(context),
|
||||||
defaultTextDirection: appearance.defaultTextDirection,
|
defaultTextDirection: appearance.defaultTextDirection,
|
||||||
textStyleConfiguration: TextStyleConfiguration(
|
textStyleConfiguration: TextStyleConfiguration(
|
||||||
text: baseTextStyle(fontFamily).copyWith(
|
text: baseTextStyle(fontFamily).copyWith(
|
||||||
|
@ -16,7 +16,10 @@ import 'package:appflowy_backend/rust_stream.dart';
|
|||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:flowy_infra/notifier.dart';
|
import 'package:flowy_infra/notifier.dart';
|
||||||
|
|
||||||
typedef DidUserWorkspaceUpdateCallback = void Function(
|
typedef DidUpdateUserWorkspaceCallback = void Function(
|
||||||
|
UserWorkspacePB workspace,
|
||||||
|
);
|
||||||
|
typedef DidUpdateUserWorkspacesCallback = void Function(
|
||||||
RepeatedUserWorkspacePB workspaces,
|
RepeatedUserWorkspacePB workspaces,
|
||||||
);
|
);
|
||||||
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
||||||
@ -31,11 +34,19 @@ class UserListener {
|
|||||||
UserNotificationParser? _userParser;
|
UserNotificationParser? _userParser;
|
||||||
StreamSubscription<SubscribeObject>? _subscription;
|
StreamSubscription<SubscribeObject>? _subscription;
|
||||||
PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
|
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 start({
|
||||||
void Function(UserProfileNotifyValue)? onProfileUpdated,
|
void Function(UserProfileNotifyValue)? onProfileUpdated,
|
||||||
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
|
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
|
||||||
|
void Function(UserWorkspacePB)? didUpdateUserWorkspace,
|
||||||
}) {
|
}) {
|
||||||
if (onProfileUpdated != null) {
|
if (onProfileUpdated != null) {
|
||||||
_profileNotifier?.addPublishListener(onProfileUpdated);
|
_profileNotifier?.addPublishListener(onProfileUpdated);
|
||||||
@ -45,6 +56,10 @@ class UserListener {
|
|||||||
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
|
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (didUpdateUserWorkspace != null) {
|
||||||
|
this.didUpdateUserWorkspace = didUpdateUserWorkspace;
|
||||||
|
}
|
||||||
|
|
||||||
_userParser = UserNotificationParser(
|
_userParser = UserNotificationParser(
|
||||||
id: _userProfile.id.toString(),
|
id: _userProfile.id.toString(),
|
||||||
callback: _userNotificationCallback,
|
callback: _userNotificationCallback,
|
||||||
@ -81,6 +96,11 @@ class UserListener {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case user.UserNotification.DidUpdateUserWorkspace:
|
||||||
|
result.map(
|
||||||
|
(r) => didUpdateUserWorkspace?.call(UserWorkspacePB.fromBuffer(r)),
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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/router.dart';
|
||||||
import 'package:appflowy/user/presentation/widgets/widgets.dart';
|
import 'package:appflowy/user/presentation/widgets/widgets.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
|
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
@ -36,9 +35,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const _SkipLoginMoveWindow(),
|
appBar: const _SkipLoginMoveWindow(),
|
||||||
body: Center(
|
body: Center(child: _renderBody(context)),
|
||||||
child: _renderBody(context),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,9 +70,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: size.width * 0.7,
|
width: size.width * 0.7,
|
||||||
child: FolderWidget(
|
child: FolderWidget(
|
||||||
createFolderCallback: () async {
|
createFolderCallback: () async => _didCustomizeFolder = true,
|
||||||
_didCustomizeFolder = true;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@ -88,24 +83,16 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
|
|||||||
Future<void> _autoRegister(BuildContext context) async {
|
Future<void> _autoRegister(BuildContext context) async {
|
||||||
final result = await getIt<AuthService>().signUpAsGuest();
|
final result = await getIt<AuthService>().signUpAsGuest();
|
||||||
result.fold(
|
result.fold(
|
||||||
(user) {
|
(user) => getIt<AuthRouter>().goHomeScreen(context, user),
|
||||||
getIt<AuthRouter>().goHomeScreen(context, user);
|
(error) => Log.error(error),
|
||||||
},
|
|
||||||
(error) {
|
|
||||||
Log.error(error);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _relaunchAppAndAutoRegister() async {
|
Future<void> _relaunchAppAndAutoRegister() async => runAppFlowy(isAnon: true);
|
||||||
await runAppFlowy(isAnon: true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SkipLoginPageFooter extends StatelessWidget {
|
class SkipLoginPageFooter extends StatelessWidget {
|
||||||
const SkipLoginPageFooter({
|
const SkipLoginPageFooter({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -135,9 +122,7 @@ class SkipLoginPageFooter extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SubscribeButtons extends StatelessWidget {
|
class SubscribeButtons extends StatelessWidget {
|
||||||
const SubscribeButtons({
|
const SubscribeButtons({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -168,10 +153,7 @@ class SubscribeButtons extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
FlowyText.regular(
|
FlowyText.regular(LocaleKeys.and.tr(), fontSize: FontSizes.s12),
|
||||||
LocaleKeys.and.tr(),
|
|
||||||
fontSize: FontSizes.s12,
|
|
||||||
),
|
|
||||||
FlowyTextButton(
|
FlowyTextButton(
|
||||||
LocaleKeys.subscribeNewsletterText.tr(),
|
LocaleKeys.subscribeNewsletterText.tr(),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
@ -190,9 +172,7 @@ class SubscribeButtons extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LanguageSelectorOnWelcomePage extends StatelessWidget {
|
class LanguageSelectorOnWelcomePage extends StatelessWidget {
|
||||||
const LanguageSelectorOnWelcomePage({
|
const LanguageSelectorOnWelcomePage({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -205,24 +185,16 @@ class LanguageSelectorOnWelcomePage extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
const FlowySvg(
|
const FlowySvg(FlowySvgs.ethernet_m, size: Size.square(20)),
|
||||||
FlowySvgs.ethernet_m,
|
|
||||||
size: Size.square(20),
|
|
||||||
),
|
|
||||||
const HSpace(4),
|
const HSpace(4),
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final currentLocale =
|
final currentLocale =
|
||||||
context.watch<AppearanceSettingsCubit>().state.locale;
|
context.watch<AppearanceSettingsCubit>().state.locale;
|
||||||
return FlowyText(
|
return FlowyText(languageFromLocale(currentLocale));
|
||||||
languageFromLocale(currentLocale),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const FlowySvg(
|
const FlowySvg(FlowySvgs.drop_menu_hide_m, size: Size.square(20)),
|
||||||
FlowySvgs.drop_menu_hide_m,
|
|
||||||
size: Size.square(20),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -231,15 +203,68 @@ class LanguageSelectorOnWelcomePage extends StatelessWidget {
|
|||||||
if (easyLocalization == null) {
|
if (easyLocalization == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
final allLocales = easyLocalization.supportedLocales;
|
|
||||||
return LanguageItemsListView(
|
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 {
|
class GoButton extends StatelessWidget {
|
||||||
const GoButton({super.key, required this.onPressed});
|
const GoButton({super.key, required this.onPressed});
|
||||||
|
|
||||||
@ -248,10 +273,7 @@ class GoButton extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => AnonUserBloc()
|
create: (context) => AnonUserBloc()..add(const AnonUserEvent.initial()),
|
||||||
..add(
|
|
||||||
const AnonUserEvent.initial(),
|
|
||||||
),
|
|
||||||
child: BlocListener<AnonUserBloc, AnonUserState>(
|
child: BlocListener<AnonUserBloc, AnonUserState>(
|
||||||
listener: (context, state) async {
|
listener: (context, state) async {
|
||||||
if (state.openedAnonUser != null) {
|
if (state.openedAnonUser != null) {
|
||||||
@ -265,7 +287,6 @@ class GoButton extends StatelessWidget {
|
|||||||
: LocaleKeys.signIn_continueAnonymousUser.tr();
|
: LocaleKeys.signIn_continueAnonymousUser.tr();
|
||||||
|
|
||||||
final textWidget = Row(
|
final textWidget = Row(
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
@ -274,22 +295,6 @@ class GoButton extends StatelessWidget {
|
|||||||
fontSize: 14,
|
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();
|
const _SkipLoginMoveWindow();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) =>
|
||||||
return const Row(
|
const Row(children: [Expanded(child: MoveWindowDetector())]);
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MoveWindowDetector(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(55.0);
|
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:appflowy/workspace/application/settings/appearance/base_appearance.dart';
|
||||||
import 'package:flowy_infra/theme.dart';
|
import 'package:flowy_infra/theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// A class for the default appearance settings for the app
|
/// A class for the default appearance settings for the app
|
||||||
class DefaultAppearanceSettings {
|
class DefaultAppearanceSettings {
|
||||||
@ -9,11 +10,11 @@ class DefaultAppearanceSettings {
|
|||||||
static const kDefaultThemeName = "Default";
|
static const kDefaultThemeName = "Default";
|
||||||
static const kDefaultTheme = BuiltInTheme.defaultTheme;
|
static const kDefaultTheme = BuiltInTheme.defaultTheme;
|
||||||
|
|
||||||
static Color getDefaultDocumentCursorColor(BuildContext context) {
|
static Color getDefaultCursorColor(BuildContext context) {
|
||||||
return Theme.of(context).colorScheme.primary;
|
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);
|
return Theme.of(context).colorScheme.primary.withOpacity(0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,11 +47,13 @@ class CachedRecentService {
|
|||||||
Future<FlowyResult<void, FlowyError>> updateRecentViews(
|
Future<FlowyResult<void, FlowyError>> updateRecentViews(
|
||||||
List<String> viewIds,
|
List<String> viewIds,
|
||||||
bool addInRecent,
|
bool addInRecent,
|
||||||
) async {
|
) async =>
|
||||||
return FolderEventUpdateRecentViews(
|
FolderEventUpdateRecentViews(
|
||||||
UpdateRecentViewPayloadPB(viewIds: viewIds, addInRecent: addInRecent),
|
UpdateRecentViewPayloadPB(
|
||||||
).send();
|
viewIds: viewIds,
|
||||||
}
|
addInRecent: addInRecent,
|
||||||
|
),
|
||||||
|
).send();
|
||||||
|
|
||||||
Future<FlowyResult<RepeatedViewPB, FlowyError>> _readRecentViews() =>
|
Future<FlowyResult<RepeatedViewPB, FlowyError>> _readRecentViews() =>
|
||||||
FolderEventReadRecentViews().send();
|
FolderEventReadRecentViews().send();
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/shared/google_fonts_extension.dart';
|
import 'package:appflowy/shared/google_fonts_extension.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/theme.dart';
|
import 'package:flowy_infra/theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
// the default font family is empty, so we can use the default font family of the platform
|
// 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
|
// 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 {
|
enum SettingsPage {
|
||||||
// NEW
|
// NEW
|
||||||
account,
|
account,
|
||||||
|
workspace,
|
||||||
// OLD
|
// OLD
|
||||||
appearance,
|
|
||||||
language,
|
|
||||||
files,
|
files,
|
||||||
// user,
|
|
||||||
notifications,
|
notifications,
|
||||||
cloud,
|
cloud,
|
||||||
shortcuts,
|
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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/shared/feature_flags.dart';
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/user/application/user_listener.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:appflowy_result/appflowy_result.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
@ -27,11 +28,18 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
|||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
initial: () async {
|
initial: () async {
|
||||||
_listener
|
_listener.start(
|
||||||
..didUpdateUserWorkspaces = (workspaces) {
|
didUpdateUserWorkspaces: (workspaces) =>
|
||||||
add(UserWorkspaceEvent.updateWorkspaces(workspaces));
|
add(UserWorkspaceEvent.updateWorkspaces(workspaces)),
|
||||||
}
|
didUpdateUserWorkspace: (workspace) {
|
||||||
..start();
|
// 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 result = await _fetchWorkspaces();
|
||||||
final currentWorkspace = result.$1;
|
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(
|
const factory UserWorkspaceEvent.updateWorkspaces(
|
||||||
RepeatedUserWorkspacePB workspaces,
|
RepeatedUserWorkspacePB workspaces,
|
||||||
) = UpdateWorkspaces;
|
) = UpdateWorkspaces;
|
||||||
|
const factory UserWorkspaceEvent.updateCurrentWorkspace(
|
||||||
|
UserWorkspacePB workspace,
|
||||||
|
) = UpdateCurrentWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserWorkspaceActionType {
|
enum UserWorkspaceActionType {
|
||||||
|
@ -88,40 +88,38 @@ class DesktopHomeScreen extends StatelessWidget {
|
|||||||
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: HomeHotKeys(
|
child: Scaffold(
|
||||||
userProfile: userProfile,
|
floatingActionButton: enableMemoryLeakDetect
|
||||||
child: Scaffold(
|
? const FloatingActionButton(
|
||||||
floatingActionButton: enableMemoryLeakDetect
|
onPressed: dumpMemoryLeak,
|
||||||
? const FloatingActionButton(
|
child: Icon(Icons.memory),
|
||||||
onPressed: dumpMemoryLeak,
|
)
|
||||||
child: Icon(Icons.memory),
|
: null,
|
||||||
)
|
body: BlocListener<HomeBloc, HomeState>(
|
||||||
: null,
|
listenWhen: (p, c) => p.latestView != c.latestView,
|
||||||
body: BlocListener<HomeBloc, HomeState>(
|
listener: (context, state) {
|
||||||
listenWhen: (p, c) => p.latestView != c.latestView,
|
final view = state.latestView;
|
||||||
listener: (context, state) {
|
if (view != null) {
|
||||||
final view = state.latestView;
|
// Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
|
||||||
if (view != 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.
|
||||||
// Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
|
final currentPageManager =
|
||||||
// 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.
|
context.read<TabsBloc>().state.currentPageManager;
|
||||||
final currentPageManager =
|
|
||||||
context.read<TabsBloc>().state.currentPageManager;
|
|
||||||
|
|
||||||
if (currentPageManager.plugin.pluginType ==
|
if (currentPageManager.plugin.pluginType ==
|
||||||
PluginType.blank) {
|
PluginType.blank) {
|
||||||
getIt<TabsBloc>().add(
|
getIt<TabsBloc>().add(
|
||||||
TabsEvent.openPlugin(plugin: view.plugin()),
|
TabsEvent.openPlugin(plugin: view.plugin()),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
},
|
||||||
buildWhen: (previous, current) => previous != current,
|
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
|
||||||
builder: (context, state) => BlocProvider(
|
buildWhen: (previous, current) => previous != current,
|
||||||
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
builder: (context, state) => BlocProvider(
|
||||||
..add(
|
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
|
||||||
const UserWorkspaceEvent.initial(),
|
..add(const UserWorkspaceEvent.initial()),
|
||||||
),
|
child: HomeHotKeys(
|
||||||
|
userProfile: userProfile,
|
||||||
child: FlowyContainer(
|
child: FlowyContainer(
|
||||||
Theme.of(context).colorScheme.surface,
|
Theme.of(context).colorScheme.surface,
|
||||||
child: _buildBody(context, userProfile, workspaceSetting),
|
child: _buildBody(context, userProfile, workspaceSetting),
|
||||||
|
@ -136,9 +136,9 @@ class HomeSideBar extends StatelessWidget {
|
|||||||
workspaceSetting.workspaceId,
|
workspaceSetting.workspaceId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
context.read<FavoriteBloc>().add(
|
context
|
||||||
const FavoriteEvent.fetchFavorites(),
|
.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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||||
import 'package:appflowy/startup/startup.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/home/hotkeys.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
@ -65,9 +66,14 @@ class UserSettingButton extends StatelessWidget {
|
|||||||
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) =>
|
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) =>
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => BlocProvider<DocumentAppearanceCubit>.value(
|
builder: (dialogContext) => MultiBlocProvider(
|
||||||
key: _settingsDialogKey,
|
key: _settingsDialogKey,
|
||||||
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
providers: [
|
||||||
|
BlocProvider<DocumentAppearanceCubit>.value(
|
||||||
|
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
|
||||||
|
),
|
||||||
|
BlocProvider.value(value: context.read<UserWorkspaceBloc>()),
|
||||||
|
],
|
||||||
child: SettingsDialog(
|
child: SettingsDialog(
|
||||||
userProfile,
|
userProfile,
|
||||||
didLogout: () async {
|
didLogout: () async {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.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:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
class SidebarWorkspace extends StatefulWidget {
|
class SidebarWorkspace extends StatefulWidget {
|
||||||
const SidebarWorkspace({
|
const SidebarWorkspace({super.key, required this.userProfile});
|
||||||
super.key,
|
|
||||||
required this.userProfile,
|
|
||||||
});
|
|
||||||
|
|
||||||
final UserProfilePB userProfile;
|
final UserProfilePB userProfile;
|
||||||
|
|
||||||
@ -197,6 +195,12 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget {
|
|||||||
workspace: currentWorkspace,
|
workspace: currentWorkspace,
|
||||||
iconSize: 20,
|
iconSize: 20,
|
||||||
enableEdit: false,
|
enableEdit: false,
|
||||||
|
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||||
|
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||||
|
currentWorkspace.workspaceId,
|
||||||
|
result.emoji,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(6),
|
const HSpace(6),
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||||
import 'package:appflowy/util/color_generator/color_generator.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_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
|
|
||||||
class WorkspaceIcon extends StatefulWidget {
|
class WorkspaceIcon extends StatefulWidget {
|
||||||
const WorkspaceIcon({
|
const WorkspaceIcon({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.workspace,
|
||||||
required this.enableEdit,
|
required this.enableEdit,
|
||||||
required this.iconSize,
|
required this.iconSize,
|
||||||
required this.workspace,
|
required this.onSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
final UserWorkspacePB workspace;
|
final UserWorkspacePB workspace;
|
||||||
final double iconSize;
|
final double iconSize;
|
||||||
final bool enableEdit;
|
final bool enableEdit;
|
||||||
|
final void Function(EmojiPickerResult) onSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WorkspaceIcon> createState() => _WorkspaceIconState();
|
State<WorkspaceIcon> createState() => _WorkspaceIconState();
|
||||||
@ -45,7 +46,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||||||
height: max(widget.iconSize, 26),
|
height: max(widget.iconSize, 26),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ColorGenerator(widget.workspace.name).toColor(),
|
color: ColorGenerator(widget.workspace.name).toColor(),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
widget.workspace.name.isEmpty
|
widget.workspace.name.isEmpty
|
||||||
@ -55,6 +56,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.enableEdit) {
|
if (widget.enableEdit) {
|
||||||
child = AppFlowyPopover(
|
child = AppFlowyPopover(
|
||||||
offset: const Offset(0, 8),
|
offset: const Offset(0, 8),
|
||||||
@ -62,19 +64,12 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
|
|||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
constraints: BoxConstraints.loose(const Size(360, 380)),
|
constraints: BoxConstraints.loose(const Size(360, 380)),
|
||||||
clickHandler: PopoverClickHandler.gestureDetector,
|
clickHandler: PopoverClickHandler.gestureDetector,
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (_) => FlowyIconPicker(
|
||||||
return FlowyIconPicker(
|
onSelected: (result) {
|
||||||
onSelected: (result) {
|
widget.onSelected(result);
|
||||||
context.read<UserWorkspaceBloc>().add(
|
controller.close();
|
||||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
},
|
||||||
widget.workspace.workspaceId,
|
),
|
||||||
result.emoji,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
controller.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: SystemMouseCursors.click,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
@ -152,6 +153,12 @@ class WorkspaceMenuItem extends StatelessWidget {
|
|||||||
workspace: workspace,
|
workspace: workspace,
|
||||||
iconSize: 26,
|
iconSize: 26,
|
||||||
enableEdit: true,
|
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_alert_dialog.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.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.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
@ -58,11 +56,9 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
|||||||
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
|
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
|
title: LocaleKeys.settings_accountPage_title.tr(),
|
||||||
|
description: LocaleKeys.settings_accountPage_description.tr(),
|
||||||
children: [
|
children: [
|
||||||
SettingsHeader(
|
|
||||||
title: LocaleKeys.settings_accountPage_title.tr(),
|
|
||||||
description: LocaleKeys.settings_accountPage_description.tr(),
|
|
||||||
),
|
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
title: LocaleKeys.settings_accountPage_general_title.tr(),
|
title: LocaleKeys.settings_accountPage_general_title.tr(),
|
||||||
children: [
|
children: [
|
||||||
@ -140,7 +136,6 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
|||||||
// ),
|
// ),
|
||||||
// ],
|
// ],
|
||||||
// ),
|
// ),
|
||||||
const SettingsCategorySpacer(),
|
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
title: LocaleKeys.settings_accountPage_keys_title.tr(),
|
title: LocaleKeys.settings_accountPage_keys_title.tr(),
|
||||||
children: [
|
children: [
|
||||||
@ -174,7 +169,6 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SettingsCategorySpacer(),
|
|
||||||
SettingsCategory(
|
SettingsCategory(
|
||||||
title: LocaleKeys.settings_accountPage_login_title.tr(),
|
title: LocaleKeys.settings_accountPage_login_title.tr(),
|
||||||
children: [
|
children: [
|
||||||
@ -409,10 +403,10 @@ class _UserProfileSettingState extends State<UserProfileSetting> {
|
|||||||
width: 360,
|
width: 360,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12),
|
margin: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: FlowyIconPicker(
|
child: FlowyIconPicker(
|
||||||
onSelected: (result) {
|
onSelected: (r) {
|
||||||
context.read<SettingsUserViewBloc>().add(
|
context
|
||||||
SettingsUserEvent.updateUserIcon(iconUrl: result.emoji),
|
.read<SettingsUserViewBloc>()
|
||||||
);
|
.add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji));
|
||||||
Navigator.of(dialogContext).pop();
|
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/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/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/feature_flags/feature_flag_page.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_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_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_menu.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
@ -78,10 +77,8 @@ class SettingsDialog extends StatelessWidget {
|
|||||||
didLogout: didLogout,
|
didLogout: didLogout,
|
||||||
didLogin: dismissDialog,
|
didLogin: dismissDialog,
|
||||||
);
|
);
|
||||||
case SettingsPage.appearance:
|
case SettingsPage.workspace:
|
||||||
return const SettingsAppearanceView();
|
return SettingsWorkspaceView(userProfile: user);
|
||||||
case SettingsPage.language:
|
|
||||||
return const SettingsLanguageView();
|
|
||||||
case SettingsPage.files:
|
case SettingsPage.files:
|
||||||
return const SettingsFileSystemView();
|
return const SettingsFileSystemView();
|
||||||
case SettingsPage.notifications:
|
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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/util/color_to_hex_string.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:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.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({
|
const DocumentColorSettingButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.currentColor,
|
required this.currentColor,
|
||||||
@ -27,41 +28,53 @@ class DocumentColorSettingButton extends StatelessWidget {
|
|||||||
|
|
||||||
final void Function(Color selectedColorOnDialog) onApply;
|
final void Function(Color selectedColorOnDialog) onApply;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DocumentColorSettingButton> createState() =>
|
||||||
|
_DocumentColorSettingButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DocumentColorSettingButtonState
|
||||||
|
extends State<DocumentColorSettingButton> {
|
||||||
|
late Color newColor = widget.currentColor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
margin: const EdgeInsets.all(8),
|
margin: const EdgeInsets.all(8),
|
||||||
text: previewWidgetBuilder.call(currentColor),
|
text: widget.previewWidgetBuilder.call(widget.currentColor),
|
||||||
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
expandText: false,
|
expandText: false,
|
||||||
onTap: () => Dialogs.show(
|
onTap: () => SettingsAlertDialog(
|
||||||
context,
|
title: widget.dialogTitle,
|
||||||
child: _DocumentColorSettingDialog(
|
confirm: () {
|
||||||
currentColor: currentColor,
|
widget.onApply(newColor);
|
||||||
previewWidgetBuilder: previewWidgetBuilder,
|
Navigator.of(context).pop();
|
||||||
dialogTitle: dialogTitle,
|
},
|
||||||
onApply: onApply,
|
children: [
|
||||||
),
|
_DocumentColorSettingDialog(
|
||||||
),
|
formKey: GlobalKey<FormState>(),
|
||||||
|
currentColor: widget.currentColor,
|
||||||
|
previewWidgetBuilder: widget.previewWidgetBuilder,
|
||||||
|
onChanged: (color) => newColor = color,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).show(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DocumentColorSettingDialog extends StatefulWidget {
|
class _DocumentColorSettingDialog extends StatefulWidget {
|
||||||
const _DocumentColorSettingDialog({
|
const _DocumentColorSettingDialog({
|
||||||
|
required this.formKey,
|
||||||
required this.currentColor,
|
required this.currentColor,
|
||||||
required this.previewWidgetBuilder,
|
required this.previewWidgetBuilder,
|
||||||
required this.dialogTitle,
|
required this.onChanged,
|
||||||
required this.onApply,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final GlobalKey<FormState> formKey;
|
||||||
final Color currentColor;
|
final Color currentColor;
|
||||||
|
|
||||||
final Widget Function(Color?) previewWidgetBuilder;
|
final Widget Function(Color?) previewWidgetBuilder;
|
||||||
|
final void Function(Color selectedColor) onChanged;
|
||||||
final String dialogTitle;
|
|
||||||
|
|
||||||
final void Function(Color selectedColorOnDialog) onApply;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_DocumentColorSettingDialog> createState() =>
|
State<_DocumentColorSettingDialog> createState() =>
|
||||||
@ -76,16 +89,16 @@ class DocumentColorSettingDialogState
|
|||||||
late String currentColorHexString;
|
late String currentColorHexString;
|
||||||
late TextEditingController hexController;
|
late TextEditingController hexController;
|
||||||
late TextEditingController opacityController;
|
late TextEditingController opacityController;
|
||||||
final _formKey = GlobalKey<FormState>(debugLabel: 'colorSettingForm');
|
|
||||||
|
|
||||||
void updateSelectedColor() {
|
void updateSelectedColor() {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (widget.formKey.currentState!.validate()) {
|
||||||
setState(() {
|
setState(() {
|
||||||
final colorValue = int.tryParse(
|
final colorValue = int.tryParse(
|
||||||
hexController.text.combineHexWithOpacity(opacityController.text),
|
hexController.text.combineHexWithOpacity(opacityController.text),
|
||||||
);
|
);
|
||||||
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
|
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
|
||||||
selectedColorOnDialog = Color(colorValue!);
|
selectedColorOnDialog = Color(colorValue!);
|
||||||
|
widget.onChanged(selectedColorOnDialog!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,74 +125,43 @@ class DocumentColorSettingDialogState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FlowyDialog(
|
return Column(
|
||||||
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 320),
|
children: [
|
||||||
child: Padding(
|
SizedBox(
|
||||||
padding: const EdgeInsets.all(24),
|
width: 100,
|
||||||
child: Column(
|
height: 40,
|
||||||
children: [
|
child: Center(
|
||||||
const Spacer(),
|
child: widget.previewWidgetBuilder(
|
||||||
FlowyText(widget.dialogTitle),
|
selectedColorOnDialog,
|
||||||
const VSpace(8),
|
|
||||||
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.labelText,
|
||||||
required this.hintText,
|
required this.hintText,
|
||||||
required this.onFieldSubmitted,
|
required this.onFieldSubmitted,
|
||||||
required this.validator,
|
this.onChanged,
|
||||||
|
this.validator,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String labelText;
|
final String labelText;
|
||||||
final String hintText;
|
final String hintText;
|
||||||
|
|
||||||
final void Function(String) onFieldSubmitted;
|
final void Function(String) onFieldSubmitted;
|
||||||
|
final void Function(String)? onChanged;
|
||||||
final String? Function(String?)? validator;
|
final String? Function(String?)? validator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -209,17 +192,14 @@ class _ColorSettingTextField extends StatelessWidget {
|
|||||||
labelText: labelText,
|
labelText: labelText,
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(color: style.colorScheme.outline),
|
||||||
color: style.colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(color: style.colorScheme.outline),
|
||||||
color: style.colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: style.textTheme.bodyMedium,
|
style: style.textTheme.bodyMedium,
|
||||||
|
onChanged: onChanged,
|
||||||
onFieldSubmitted: onFieldSubmitted,
|
onFieldSubmitted: onFieldSubmitted,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
@ -227,10 +207,7 @@ class _ColorSettingTextField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String? validateHexValue(
|
String? validateHexValue(String? hexValue, String opacityValue) {
|
||||||
String? hexValue,
|
|
||||||
String opacityValue,
|
|
||||||
) {
|
|
||||||
if (hexValue == null || hexValue.isEmpty) {
|
if (hexValue == null || hexValue.isEmpty) {
|
||||||
return LocaleKeys.settings_appearance_documentSettings_hexEmptyError.tr();
|
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';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class FlowySettingListTile extends StatelessWidget {
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
const FlowySettingListTile({
|
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,
|
super.key,
|
||||||
this.resetTooltipText,
|
this.resetTooltipText,
|
||||||
this.resetButtonKey,
|
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: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 {
|
class SettingsBody extends StatelessWidget {
|
||||||
const SettingsBody({
|
const SettingsBody({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
required this.children,
|
required this.children,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
final List<Widget> children;
|
final List<Widget> children;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -14,8 +22,18 @@ class SettingsBody extends StatelessWidget {
|
|||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
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/shared/feature_flags.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
|
||||||
class FeatureFlagsPage extends StatelessWidget {
|
class FeatureFlagsPage extends StatelessWidget {
|
||||||
@ -15,15 +13,14 @@ class FeatureFlagsPage extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
|
title: 'Feature flags',
|
||||||
children: [
|
children: [
|
||||||
const SettingsHeader(title: 'Feature flags'),
|
|
||||||
SeparatedColumn(
|
SeparatedColumn(
|
||||||
children: FeatureFlag.data.entries
|
children: FeatureFlag.data.entries
|
||||||
.where((e) => e.key != FeatureFlag.unknown)
|
.where((e) => e.key != FeatureFlag.unknown)
|
||||||
.map((e) => _FeatureFlagItem(featureFlag: e.key))
|
.map((e) => _FeatureFlagItem(featureFlag: e.key))
|
||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
const SettingsCategorySpacer(),
|
|
||||||
FlowyTextButton(
|
FlowyTextButton(
|
||||||
'Restart the app to apply changes',
|
'Restart the app to apply changes',
|
||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
import 'package:appflowy/shared/af_role_pb_extension.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
@ -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/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:string_validator/string_validator.dart';
|
import 'package:string_validator/string_validator.dart';
|
||||||
|
|
||||||
@ -34,11 +34,8 @@ class WorkspaceMembersPage extends StatelessWidget {
|
|||||||
listener: _showResultDialog,
|
listener: _showResultDialog,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
|
title: LocaleKeys.settings_appearance_members_title.tr(),
|
||||||
children: [
|
children: [
|
||||||
// title
|
|
||||||
SettingsHeader(
|
|
||||||
title: LocaleKeys.settings_appearance_members_title.tr(),
|
|
||||||
),
|
|
||||||
if (state.myRole.canInvite) const _InviteMember(),
|
if (state.myRole.canInvite) const _InviteMember(),
|
||||||
if (state.myRole.canInvite && state.members.isNotEmpty)
|
if (state.myRole.canInvite && state.members.isNotEmpty)
|
||||||
const SettingsCategorySpacer(),
|
const SettingsCategorySpacer(),
|
||||||
|
@ -9,7 +9,6 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
@ -41,10 +40,8 @@ class SettingCloud extends StatelessWidget {
|
|||||||
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
|
title: LocaleKeys.settings_menu_cloudSettings.tr(),
|
||||||
children: [
|
children: [
|
||||||
SettingsHeader(
|
|
||||||
title: LocaleKeys.settings_menu_cloudSettings.tr(),
|
|
||||||
),
|
|
||||||
if (Env.enableCustomCloud)
|
if (Env.enableCustomCloud)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -55,17 +52,12 @@ class SettingCloud extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
CloudTypeSwitcher(
|
CloudTypeSwitcher(
|
||||||
cloudType: state.cloudType,
|
cloudType: state.cloudType,
|
||||||
onSelected: (newCloudType) {
|
onSelected: (type) => context
|
||||||
context.read<CloudSettingBloc>().add(
|
.read<CloudSettingBloc>()
|
||||||
CloudSettingEvent.updateCloudType(
|
.add(CloudSettingEvent.updateCloudType(type)),
|
||||||
newCloudType,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const VSpace(8),
|
|
||||||
_viewFromCloudType(state.cloudType),
|
_viewFromCloudType(state.cloudType),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -73,9 +65,7 @@ class SettingCloud extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
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_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
@ -22,10 +21,8 @@ class SettingsShortcutsView extends StatelessWidget {
|
|||||||
create: (_) =>
|
create: (_) =>
|
||||||
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
|
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
|
||||||
child: SettingsBody(
|
child: SettingsBody(
|
||||||
|
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
||||||
children: [
|
children: [
|
||||||
SettingsHeader(
|
|
||||||
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
|
|
||||||
),
|
|
||||||
BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
BlocBuilder<ShortcutsCubit, ShortcutsState>(
|
||||||
builder: (_, state) => switch (state.status) {
|
builder: (_, state) => switch (state.status) {
|
||||||
ShortcutsStatus.initial ||
|
ShortcutsStatus.initial ||
|
||||||
@ -110,8 +107,8 @@ class ShortcutsListTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
FlowyTextButton(
|
FlowyTextButton(
|
||||||
shortcutEvent.command,
|
shortcutEvent.command,
|
||||||
fillColor: Colors.transparent,
|
|
||||||
fontColor: AFThemeExtension.of(context).textColor,
|
fontColor: AFThemeExtension.of(context).textColor,
|
||||||
|
fillColor: Colors.transparent,
|
||||||
onPressed: () => showKeyListenerDialog(context),
|
onPressed: () => showKeyListenerDialog(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.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_category_spacer.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
|
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart';
|
||||||
@ -17,16 +16,16 @@ class SettingsFileSystemView extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
children: [
|
title: LocaleKeys.settings_menu_files.tr(),
|
||||||
SettingsHeader(title: LocaleKeys.settings_menu_files.tr()),
|
children: const [
|
||||||
const SettingsFileLocationCustomizer(),
|
SettingsFileLocationCustomizer(),
|
||||||
const SettingsCategorySpacer(),
|
SettingsCategorySpacer(),
|
||||||
if (kDebugMode) ...[
|
if (kDebugMode) ...[
|
||||||
const SettingsExportFileWidget(),
|
SettingsExportFileWidget(),
|
||||||
],
|
],
|
||||||
const ImportAppFlowyData(),
|
ImportAppFlowyData(),
|
||||||
const SettingsCategorySpacer(),
|
SettingsCategorySpacer(),
|
||||||
const SettingsFileCacheWidget(),
|
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,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.appearance,
|
page: SettingsPage.workspace,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
label: LocaleKeys.settings_menu_appearance.tr(),
|
label: LocaleKeys.settings_workspacePage_menuLabel.tr(),
|
||||||
icon: const Icon(Icons.brightness_4),
|
icon: const FlowySvg(FlowySvgs.settings_workplace_m),
|
||||||
changeSelectedPage: changeSelectedPage,
|
|
||||||
),
|
|
||||||
SettingsMenuElement(
|
|
||||||
page: SettingsPage.language,
|
|
||||||
selectedPage: currentPage,
|
|
||||||
label: LocaleKeys.settings_menu_language.tr(),
|
|
||||||
icon: const Icon(Icons.translate),
|
|
||||||
changeSelectedPage: changeSelectedPage,
|
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(
|
SettingsMenuElement(
|
||||||
page: SettingsPage.files,
|
page: SettingsPage.files,
|
||||||
selectedPage: currentPage,
|
selectedPage: currentPage,
|
||||||
@ -96,16 +99,6 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.cut),
|
icon: const Icon(Icons.cut),
|
||||||
changeSelectedPage: changeSelectedPage,
|
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)
|
if (kDebugMode)
|
||||||
SettingsMenuElement(
|
SettingsMenuElement(
|
||||||
// no need to translate this page
|
// 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/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@ -16,9 +15,9 @@ class SettingsNotificationsView extends StatelessWidget {
|
|||||||
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
|
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SettingsBody(
|
return SettingsBody(
|
||||||
|
title: LocaleKeys.settings_menu_notifications.tr(),
|
||||||
children: [
|
children: [
|
||||||
SettingsHeader(title: LocaleKeys.settings_menu_notifications.tr()),
|
SettingListTile(
|
||||||
FlowySettingListTile(
|
|
||||||
label: LocaleKeys.settings_notifications_enableNotifications_label
|
label: LocaleKeys.settings_notifications_enableNotifications_label
|
||||||
.tr(),
|
.tr(),
|
||||||
hint: LocaleKeys.settings_notifications_enableNotifications_hint
|
hint: LocaleKeys.settings_notifications_enableNotifications_hint
|
||||||
|
@ -240,6 +240,7 @@ flutter:
|
|||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- assets/images/
|
- assets/images/
|
||||||
|
- assets/images/appearance/
|
||||||
- assets/images/built_in_cover_images/
|
- assets/images/built_in_cover_images/
|
||||||
- assets/flowy_icons/
|
- assets/flowy_icons/
|
||||||
- assets/flowy_icons/16x/
|
- 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';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.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/appearance_cubit.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
|
|||||||
# https://github.com/flutter/flutter/issues/57146.
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
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 ===
|
# === Flutter Library ===
|
||||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||||
|
|
||||||
@ -92,7 +97,7 @@ add_custom_command(
|
|||||||
COMMAND ${CMAKE_COMMAND} -E env
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
${FLUTTER_TOOL_ENVIRONMENT}
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||||
windows-x64 $<CONFIG>
|
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
add_custom_target(flutter_assemble DEPENDS
|
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"
|
"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": {
|
"menu": {
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
@ -13,6 +13,7 @@ pub(crate) enum UserNotification {
|
|||||||
DidUpdateUserProfile = 2,
|
DidUpdateUserProfile = 2,
|
||||||
DidUpdateUserWorkspaces = 3,
|
DidUpdateUserWorkspaces = 3,
|
||||||
DidUpdateCloudConfig = 4,
|
DidUpdateCloudConfig = 4,
|
||||||
|
DidUpdateUserWorkspace = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::convert::From<UserNotification> for i32 {
|
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::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_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::user_manager::user_login_state::UserAuthProcess;
|
||||||
use crate::{errors::FlowyError, notification::*};
|
use crate::{errors::FlowyError, notification::*};
|
||||||
use flowy_user_pub::session::Session;
|
use flowy_user_pub::session::Session;
|
||||||
|
|
||||||
|
use super::manager_user_workspace::save_user_workspace;
|
||||||
|
|
||||||
pub struct UserManager {
|
pub struct UserManager {
|
||||||
pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>,
|
pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>,
|
||||||
pub(crate) store_preferences: Arc<StorePreferences>,
|
pub(crate) store_preferences: Arc<StorePreferences>,
|
||||||
@ -708,7 +710,7 @@ impl UserManager {
|
|||||||
self.set_anon_user(session.clone());
|
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!(
|
info!(
|
||||||
"Save new user profile to disk, authenticator: {:?}",
|
"Save new user profile to disk, authenticator: {:?}",
|
||||||
authenticator
|
authenticator
|
||||||
@ -779,13 +781,13 @@ impl UserManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the old user workspace setting.
|
// Save the old user workspace setting.
|
||||||
save_user_workspaces(
|
save_user_workspace(
|
||||||
old_user.session.user_id,
|
old_user.session.user_id,
|
||||||
self
|
self
|
||||||
.authenticate_user
|
.authenticate_user
|
||||||
.database
|
.database
|
||||||
.get_connection(old_user.session.user_id)?,
|
.get_connection(old_user.session.user_id)?,
|
||||||
&[old_user.session.user_workspace.clone()],
|
&old_user.session.user_workspace.clone(),
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ use flowy_user_pub::entities::{
|
|||||||
};
|
};
|
||||||
use lib_dispatch::prelude::af_spawn;
|
use lib_dispatch::prelude::af_spawn;
|
||||||
|
|
||||||
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB};
|
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB, UserWorkspacePB};
|
||||||
use crate::migrations::AnonUser;
|
use crate::migrations::AnonUser;
|
||||||
use crate::notification::{send_notification, UserNotification};
|
use crate::notification::{send_notification, UserNotification};
|
||||||
use crate::services::data_import::{
|
use crate::services::data_import::{
|
||||||
@ -239,7 +239,14 @@ impl UserManager {
|
|||||||
user_workspace.icon = new_workspace_icon.to_string();
|
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)]
|
#[instrument(level = "info", skip(self), err)]
|
||||||
@ -371,7 +378,7 @@ impl UserManager {
|
|||||||
af_spawn(async move {
|
af_spawn(async move {
|
||||||
if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await {
|
if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await {
|
||||||
if let Ok(conn) = pool.get() {
|
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);
|
let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces);
|
||||||
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces)
|
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces)
|
||||||
.payload(repeated_workspace_pbs)
|
.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,
|
uid: i64,
|
||||||
mut conn: DBConnection,
|
mut conn: DBConnection,
|
||||||
user_workspaces: &[UserWorkspace],
|
user_workspaces: &[UserWorkspace],
|
||||||
|
Loading…
Reference in New Issue
Block a user