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:
Mathias Mogensen 2024-05-10 16:08:32 +02:00 committed by GitHub
parent f47c88b022
commit a0ed043cb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 3434 additions and 1825 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -60,9 +60,12 @@ void main() {
Finder success;
final Finder items = find.byType(WorkspaceMenuItem);
// delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu();
final Finder items = find.byType(WorkspaceMenuItem);
await tester.pumpUntilFound(items);
expect(items, findsNWidgets(2));
expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name,

View File

@ -1,11 +1,9 @@
import 'package:integration_test/integration_test.dart';
import 'notifications_settings_test.dart' as notifications_settings_test;
import 'user_language_test.dart' as user_language_test;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
notifications_settings_test.main();
user_language_test.main();
}

View File

@ -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",
);
});
});
}

View File

@ -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),
);
});
});
}

View File

@ -1,11 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -23,31 +25,35 @@ void main() {
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.appearance);
await tester.openSettingsPage(SettingsPage.workspace);
await tester.pumpAndSettle();
tester.expectToSeeText(
LocaleKeys.settings_appearance_themeMode_system.tr(),
);
final appFinder = find.byType(MaterialApp).first;
ThemeMode? themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.system);
await tester.tapButton(
find.bySemanticsLabel(
LocaleKeys.settings_appearance_themeMode_system.tr(),
LocaleKeys.settings_workspacePage_appearance_options_light.tr(),
),
);
await tester.pumpAndSettle();
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.light);
await tester.tapButton(
find.bySemanticsLabel(
LocaleKeys.settings_appearance_themeMode_dark.tr(),
LocaleKeys.settings_workspacePage_appearance_options_dark.tr(),
),
);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(seconds: 1));
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.dark);
await tester.tap(find.byType(SettingsDialog));
await tester.pumpAndSettle();
await FlowyTestKeyboard.simulateKeyDownEvent(
@ -60,12 +66,10 @@ void main() {
],
tester: tester,
);
await tester.pumpAndSettle();
tester.expectToSeeText(
LocaleKeys.settings_appearance_themeMode_light.tr(),
);
themeMode = tester.widget<MaterialApp>(appFinder).themeMode;
expect(themeMode, ThemeMode.light);
});
testWidgets('show or hide home menu', (tester) async {

View File

@ -1,4 +1,4 @@
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/user/presentation/screens/skip_log_in_screen.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

View File

@ -3,8 +3,6 @@ import 'package:integration_test/integration_test.dart';
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/appearance_settings_test.dart'
as appearance_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
@ -26,7 +24,6 @@ Future<void> runIntegration3OnDesktop() async {
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
appearance_test_runner.main();
settings_test_runner.main();
share_markdown_test.main();
import_files_test.main();

View File

@ -173,27 +173,39 @@ extension AppFlowyTestBase on WidgetTester {
int buttons = kPrimaryButton,
bool warnIfMissed = false,
int milliseconds = 500,
bool pumpAndSettle = true,
}) async {
await tap(
finder,
buttons: buttons,
warnIfMissed: warnIfMissed,
);
await pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 5),
);
if (pumpAndSettle) {
await this.pumpAndSettle(
Duration(milliseconds: milliseconds),
EnginePhase.sendSemanticsUpdate,
const Duration(seconds: 5),
);
}
}
Future<void> tapButtonWithName(String tr, {int milliseconds = 500}) async {
Future<void> tapButtonWithName(
String tr, {
int milliseconds = 500,
bool pumpAndSettle = true,
}) async {
Finder button = find.text(tr, findRichText: true, skipOffstage: false);
if (button.evaluate().isEmpty) {
button = find.byWidgetPredicate(
(widget) => widget is FlowyText && widget.text == tr,
);
}
await tapButton(button, milliseconds: milliseconds);
await tapButton(
button,
milliseconds: milliseconds,
pumpAndSettle: pumpAndSettle,
);
}
Future<void> doubleTapAt(

View File

@ -22,7 +22,6 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@ -511,8 +510,9 @@ extension CommonOperations on WidgetTester {
final workspace = find.byType(SidebarWorkspace);
expect(workspace, findsOneWidget);
// click it
await tapButton(workspace, milliseconds: 2000);
await tapButton(workspace, pumpAndSettle: false);
await pump(const Duration(seconds: 5));
}
Future<void> createCollaborativeWorkspace(String name) async {
@ -527,7 +527,8 @@ extension CommonOperations on WidgetTester {
// click the create button
final createButton = find.byKey(createWorkspaceButtonKey);
expect(createButton, findsOneWidget);
await tapButton(createButton);
await tapButton(createButton, pumpAndSettle: false);
await pump(const Duration(seconds: 5));
// see the create workspace dialog
final createWorkspaceDialog = find.byType(CreateWorkspaceDialog);
@ -536,7 +537,8 @@ extension CommonOperations on WidgetTester {
// input the workspace name
await enterText(find.byType(TextField), name);
await tapButtonWithName(LocaleKeys.button_ok.tr());
await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false);
await pump(const Duration(seconds: 5));
}
// For mobile platform to launch the app in anonymous mode

View File

@ -3,9 +3,10 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/direction_setting.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
@ -13,6 +14,7 @@ import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
import 'common_operations.dart';
extension AppFlowySettings on WidgetTester {
/// Open settings page
@ -77,12 +79,21 @@ extension AppFlowySettings on WidgetTester {
// go to settings page and toggle enable RTL toolbar items
Future<void> toggleEnableRTLToolbarItems() async {
await openSettings();
await openSettingsPage(SettingsPage.appearance);
await openSettingsPage(SettingsPage.workspace);
final switchButton =
find.byKey(EnableRTLToolbarItemsSetting.enableRTLSwitchKey);
expect(switchButton, findsOneWidget);
await tapButton(switchButton);
final scrollable = find.findSettingsScrollable();
await scrollUntilVisible(
find.byType(EnableRTLItemsSwitcher),
0,
scrollable: scrollable,
);
final switcher = find.descendant(
of: find.byType(EnableRTLItemsSwitcher),
matching: find.byType(Toggle),
);
await tap(switcher);
// tap anywhere to close the settings page
await tapAt(Offset.zero);

File diff suppressed because it is too large Load Diff

View File

@ -127,6 +127,12 @@ class _MobileWorkspace extends StatelessWidget {
workspace: currentWorkspace,
iconSize: 26,
enableEdit: false,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId,
result.emoji,
),
),
),
),
const HSpace(8),

View File

@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// Only works on mobile.
@ -105,6 +107,12 @@ class _WorkspaceMenuItem extends StatelessWidget {
enableEdit: false,
iconSize: 26,
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
),
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(

View File

@ -1,13 +1,28 @@
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy/util/font_family_extension.dart';
import 'package:appflowy/util/levenshtein.dart';
import 'package:appflowy/workspace/application/appearance_defaults.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
final customizeFontToolbarItem = ToolbarItem(
id: 'editor.font',
@ -16,10 +31,12 @@ final customizeFontToolbarItem = ToolbarItem(
builder: (context, editorState, highlightColor, _) {
final selection = editorState.selection!;
final popoverController = PopoverController();
final String? currentFontFamily = editorState
.getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily);
return MouseRegion(
cursor: SystemMouseCursors.click,
child: FontFamilyDropDown(
currentFontFamily: '',
currentFontFamily: currentFontFamily ?? '',
offset: const Offset(0, 12),
popoverController: popoverController,
onOpen: () => keepEditorFocusNotifier.increase(),
@ -35,8 +52,11 @@ final customizeFontToolbarItem = ToolbarItem(
Log.error('Failed to set font family: $e');
}
},
onResetFont: () async => editorState
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}),
onResetFont: () async {
popoverController.close();
await editorState
.formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null});
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FlowyTooltip(
@ -52,3 +72,227 @@ final customizeFontToolbarItem = ToolbarItem(
);
},
);
class ThemeFontFamilySetting extends StatefulWidget {
const ThemeFontFamilySetting({
super.key,
required this.currentFontFamily,
});
final String currentFontFamily;
static Key textFieldKey = const Key('FontFamilyTextField');
static Key resetButtonkey = const Key('FontFamilyResetButton');
static Key popoverKey = const Key('FontFamilyPopover');
@override
State<ThemeFontFamilySetting> createState() => _ThemeFontFamilySettingState();
}
class _ThemeFontFamilySettingState extends State<ThemeFontFamilySetting> {
@override
Widget build(BuildContext context) {
return SettingListTile(
label: LocaleKeys.settings_appearance_fontFamily_label.tr(),
resetButtonKey: ThemeFontFamilySetting.resetButtonkey,
onResetRequested: () {
context.read<AppearanceSettingsCubit>().resetFontFamily();
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(DefaultAppearanceSettings.kDefaultFontFamily);
},
trailing: [
FontFamilyDropDown(currentFontFamily: widget.currentFontFamily),
],
);
}
}
class FontFamilyDropDown extends StatefulWidget {
const FontFamilyDropDown({
super.key,
required this.currentFontFamily,
this.onOpen,
this.onClose,
this.onFontFamilyChanged,
this.child,
this.popoverController,
this.offset,
this.showResetButton = false,
this.onResetFont,
});
final String currentFontFamily;
final VoidCallback? onOpen;
final VoidCallback? onClose;
final void Function(String fontFamily)? onFontFamilyChanged;
final Widget? child;
final PopoverController? popoverController;
final Offset? offset;
final bool showResetButton;
final VoidCallback? onResetFont;
@override
State<FontFamilyDropDown> createState() => _FontFamilyDropDownState();
}
class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
final List<String> availableFonts = [
defaultFontFamily,
...GoogleFonts.asMap().keys,
];
final ValueNotifier<String> query = ValueNotifier('');
@override
void dispose() {
query.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final currentValue = widget.currentFontFamily.fontFamilyDisplayName;
return SettingValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController,
currentValue: currentValue,
onClose: () {
query.value = '';
widget.onClose?.call();
},
offset: widget.offset,
child: widget.child,
popupBuilder: (_) {
widget.onOpen?.call();
return CustomScrollView(
shrinkWrap: true,
slivers: [
if (widget.showResetButton)
SliverPersistentHeader(
delegate: _ResetFontButton(onPressed: widget.onResetFont),
pinned: true,
),
SliverPadding(
padding: const EdgeInsets.only(right: 8),
sliver: SliverToBoxAdapter(
child: FlowyTextField(
key: ThemeFontFamilySetting.textFieldKey,
hintText:
LocaleKeys.settings_appearance_fontFamily_search.tr(),
autoFocus: false,
debounceDuration: const Duration(milliseconds: 300),
onChanged: (value) {
query.value = value;
},
),
),
),
const SliverToBoxAdapter(
child: SizedBox(height: 4),
),
ValueListenableBuilder(
valueListenable: query,
builder: (context, value, child) {
var displayed = availableFonts;
if (value.isNotEmpty) {
displayed = availableFonts
.where(
(font) => font
.toLowerCase()
.contains(value.toLowerCase().toString()),
)
.sorted((a, b) => levenshtein(a, b))
.toList();
}
return SliverFixedExtentList.builder(
itemBuilder: (context, index) => _fontFamilyItemButton(
context,
getGoogleFontSafely(displayed[index]),
),
itemCount: displayed.length,
itemExtent: 32,
);
},
),
],
);
},
);
}
Widget _fontFamilyItemButton(
BuildContext context,
TextStyle style,
) {
final buttonFontFamily =
style.fontFamily?.parseFontFamilyName() ?? defaultFontFamily;
return Tooltip(
message: buttonFontFamily,
waitDuration: const Duration(milliseconds: 150),
child: SizedBox(
key: ValueKey(buttonFontFamily),
height: 32,
child: FlowyButton(
onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium(
buttonFontFamily.fontFamilyDisplayName,
fontFamily: buttonFontFamily,
),
rightIcon:
buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
? const FlowySvg(FlowySvgs.check_s)
: null,
onTap: () {
if (widget.onFontFamilyChanged != null) {
widget.onFontFamilyChanged!(buttonFontFamily);
} else {
if (widget.currentFontFamily.parseFontFamilyName() !=
buttonFontFamily) {
context
.read<AppearanceSettingsCubit>()
.setFontFamily(buttonFontFamily);
context
.read<DocumentAppearanceCubit>()
.syncFontFamily(buttonFontFamily);
}
}
PopoverContainer.of(context).close();
},
),
),
);
}
}
class _ResetFontButton extends SliverPersistentHeaderDelegate {
_ResetFontButton({this.onPressed});
final VoidCallback? onPressed;
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return Padding(
padding: const EdgeInsets.only(right: 8, bottom: 8.0),
child: FlowyTextButton(
LocaleKeys.document_toolbar_resetToDefaultFont.tr(),
fontColor: AFThemeExtension.of(context).textColor,
fontHoverColor: Theme.of(context).colorScheme.onSurface,
fontSize: 12,
onPressed: onPressed,
),
);
}
@override
double get maxExtent => 35;
@override
double get minExtent => 35;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}

View File

@ -1,5 +1,8 @@
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
@ -14,8 +17,6 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
@ -48,9 +49,9 @@ class EditorStyleCustomizer {
return EditorStyle.desktop(
padding: padding,
cursorColor: appearance.cursorColor ??
DefaultAppearanceSettings.getDefaultDocumentCursorColor(context),
DefaultAppearanceSettings.getDefaultCursorColor(context),
selectionColor: appearance.selectionColor ??
DefaultAppearanceSettings.getDefaultDocumentSelectionColor(context),
DefaultAppearanceSettings.getDefaultSelectionColor(context),
defaultTextDirection: appearance.defaultTextDirection,
textStyleConfiguration: TextStyleConfiguration(
text: baseTextStyle(fontFamily).copyWith(

View File

@ -16,7 +16,10 @@ import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flowy_infra/notifier.dart';
typedef DidUserWorkspaceUpdateCallback = void Function(
typedef DidUpdateUserWorkspaceCallback = void Function(
UserWorkspacePB workspace,
);
typedef DidUpdateUserWorkspacesCallback = void Function(
RepeatedUserWorkspacePB workspaces,
);
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
@ -31,11 +34,19 @@ class UserListener {
UserNotificationParser? _userParser;
StreamSubscription<SubscribeObject>? _subscription;
PublishNotifier<UserProfileNotifyValue>? _profileNotifier = PublishNotifier();
DidUserWorkspaceUpdateCallback? didUpdateUserWorkspaces;
/// Update notification about _all_ of the users workspaces
///
DidUpdateUserWorkspacesCallback? didUpdateUserWorkspaces;
/// Update notification about _one_ workspace
///
DidUpdateUserWorkspaceCallback? didUpdateUserWorkspace;
void start({
void Function(UserProfileNotifyValue)? onProfileUpdated,
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
void Function(UserWorkspacePB)? didUpdateUserWorkspace,
}) {
if (onProfileUpdated != null) {
_profileNotifier?.addPublishListener(onProfileUpdated);
@ -45,6 +56,10 @@ class UserListener {
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
}
if (didUpdateUserWorkspace != null) {
this.didUpdateUserWorkspace = didUpdateUserWorkspace;
}
_userParser = UserNotificationParser(
id: _userProfile.id.toString(),
callback: _userNotificationCallback,
@ -81,6 +96,11 @@ class UserListener {
},
);
break;
case user.UserNotification.DidUpdateUserWorkspace:
result.map(
(r) => didUpdateUserWorkspace?.call(UserWorkspacePB.fromBuffer(r)),
);
break;
default:
break;
}

View File

@ -10,7 +10,6 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/presentation/router.dart';
import 'package:appflowy/user/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:appflowy_popover/appflowy_popover.dart';
@ -36,9 +35,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: const _SkipLoginMoveWindow(),
body: Center(
child: _renderBody(context),
),
body: Center(child: _renderBody(context)),
);
}
@ -73,9 +70,7 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
SizedBox(
width: size.width * 0.7,
child: FolderWidget(
createFolderCallback: () async {
_didCustomizeFolder = true;
},
createFolderCallback: () async => _didCustomizeFolder = true,
),
),
const Spacer(),
@ -88,24 +83,16 @@ class _SkipLogInScreenState extends State<SkipLogInScreen> {
Future<void> _autoRegister(BuildContext context) async {
final result = await getIt<AuthService>().signUpAsGuest();
result.fold(
(user) {
getIt<AuthRouter>().goHomeScreen(context, user);
},
(error) {
Log.error(error);
},
(user) => getIt<AuthRouter>().goHomeScreen(context, user),
(error) => Log.error(error),
);
}
Future<void> _relaunchAppAndAutoRegister() async {
await runAppFlowy(isAnon: true);
}
Future<void> _relaunchAppAndAutoRegister() async => runAppFlowy(isAnon: true);
}
class SkipLoginPageFooter extends StatelessWidget {
const SkipLoginPageFooter({
super.key,
});
const SkipLoginPageFooter({super.key});
@override
Widget build(BuildContext context) {
@ -135,9 +122,7 @@ class SkipLoginPageFooter extends StatelessWidget {
}
class SubscribeButtons extends StatelessWidget {
const SubscribeButtons({
super.key,
});
const SubscribeButtons({super.key});
@override
Widget build(BuildContext context) {
@ -168,10 +153,7 @@ class SubscribeButtons extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
FlowyText.regular(
LocaleKeys.and.tr(),
fontSize: FontSizes.s12,
),
FlowyText.regular(LocaleKeys.and.tr(), fontSize: FontSizes.s12),
FlowyTextButton(
LocaleKeys.subscribeNewsletterText.tr(),
padding: const EdgeInsets.symmetric(horizontal: 4.0),
@ -190,9 +172,7 @@ class SubscribeButtons extends StatelessWidget {
}
class LanguageSelectorOnWelcomePage extends StatelessWidget {
const LanguageSelectorOnWelcomePage({
super.key,
});
const LanguageSelectorOnWelcomePage({super.key});
@override
Widget build(BuildContext context) {
@ -205,24 +185,16 @@ class LanguageSelectorOnWelcomePage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
const FlowySvg(
FlowySvgs.ethernet_m,
size: Size.square(20),
),
const FlowySvg(FlowySvgs.ethernet_m, size: Size.square(20)),
const HSpace(4),
Builder(
builder: (context) {
final currentLocale =
context.watch<AppearanceSettingsCubit>().state.locale;
return FlowyText(
languageFromLocale(currentLocale),
);
return FlowyText(languageFromLocale(currentLocale));
},
),
const FlowySvg(
FlowySvgs.drop_menu_hide_m,
size: Size.square(20),
),
const FlowySvg(FlowySvgs.drop_menu_hide_m, size: Size.square(20)),
],
),
),
@ -231,15 +203,68 @@ class LanguageSelectorOnWelcomePage extends StatelessWidget {
if (easyLocalization == null) {
return const SizedBox.shrink();
}
final allLocales = easyLocalization.supportedLocales;
return LanguageItemsListView(
allLocales: allLocales,
allLocales: easyLocalization.supportedLocales,
);
},
);
}
}
class LanguageItemsListView extends StatelessWidget {
const LanguageItemsListView({super.key, required this.allLocales});
final List<Locale> allLocales;
@override
Widget build(BuildContext context) {
// get current locale from cubit
final state = context.watch<AppearanceSettingsCubit>().state;
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: ListView.builder(
itemCount: allLocales.length,
itemBuilder: (context, index) {
final locale = allLocales[index];
return LanguageItem(locale: locale, currentLocale: state.locale);
},
),
);
}
}
class LanguageItem extends StatelessWidget {
const LanguageItem({
super.key,
required this.locale,
required this.currentLocale,
});
final Locale locale;
final Locale currentLocale;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 32,
child: FlowyButton(
text: FlowyText.medium(
languageFromLocale(locale),
),
rightIcon:
currentLocale == locale ? const FlowySvg(FlowySvgs.check_s) : null,
onTap: () {
if (currentLocale != locale) {
context.read<AppearanceSettingsCubit>().setLocale(context, locale);
}
PopoverContainer.of(context).close();
},
),
);
}
}
class GoButton extends StatelessWidget {
const GoButton({super.key, required this.onPressed});
@ -248,10 +273,7 @@ class GoButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => AnonUserBloc()
..add(
const AnonUserEvent.initial(),
),
create: (context) => AnonUserBloc()..add(const AnonUserEvent.initial()),
child: BlocListener<AnonUserBloc, AnonUserState>(
listener: (context, state) async {
if (state.openedAnonUser != null) {
@ -265,7 +287,6 @@ class GoButton extends StatelessWidget {
: LocaleKeys.signIn_continueAnonymousUser.tr();
final textWidget = Row(
// mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: FlowyText.medium(
@ -274,22 +295,6 @@ class GoButton extends StatelessWidget {
fontSize: 14,
),
),
// Tooltip(
// message: LocaleKeys.settings_menu_configServerGuide.tr(),
// child: Container(
// width: 30.0,
// decoration: const BoxDecoration(
// shape: BoxShape.circle,
// ),
// child: Center(
// child: Icon(
// Icons.help,
// color: Colors.white,
// weight: 2,
// ),
// ),
// ),
// ),
],
);
@ -325,15 +330,8 @@ class _SkipLoginMoveWindow extends StatelessWidget
const _SkipLoginMoveWindow();
@override
Widget build(BuildContext context) {
return const Row(
children: [
Expanded(
child: MoveWindowDetector(),
),
],
);
}
Widget build(BuildContext context) =>
const Row(children: [Expanded(child: MoveWindowDetector())]);
@override
Size get preferredSize => const Size.fromHeight(55.0);

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
/// A class for the default appearance settings for the app
class DefaultAppearanceSettings {
@ -9,11 +10,11 @@ class DefaultAppearanceSettings {
static const kDefaultThemeName = "Default";
static const kDefaultTheme = BuiltInTheme.defaultTheme;
static Color getDefaultDocumentCursorColor(BuildContext context) {
static Color getDefaultCursorColor(BuildContext context) {
return Theme.of(context).colorScheme.primary;
}
static Color getDefaultDocumentSelectionColor(BuildContext context) {
static Color getDefaultSelectionColor(BuildContext context) {
return Theme.of(context).colorScheme.primary.withOpacity(0.2);
}
}

View File

@ -47,11 +47,13 @@ class CachedRecentService {
Future<FlowyResult<void, FlowyError>> updateRecentViews(
List<String> viewIds,
bool addInRecent,
) async {
return FolderEventUpdateRecentViews(
UpdateRecentViewPayloadPB(viewIds: viewIds, addInRecent: addInRecent),
).send();
}
) async =>
FolderEventUpdateRecentViews(
UpdateRecentViewPayloadPB(
viewIds: viewIds,
addInRecent: addInRecent,
),
).send();
Future<FlowyResult<RepeatedViewPB, FlowyError>> _readRecentViews() =>
FolderEventReadRecentViews().send();

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/shared/google_fonts_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
// the default font family is empty, so we can use the default font family of the platform
// the system will choose the default font family of the platform

View File

@ -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(),
};

View File

@ -11,11 +11,9 @@ part 'settings_dialog_bloc.freezed.dart';
enum SettingsPage {
// NEW
account,
workspace,
// OLD
appearance,
language,
files,
// user,
notifications,
cloud,
shortcuts,

View File

@ -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();
}

View File

@ -1,3 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/user/application/user_listener.dart';
@ -10,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
@ -27,11 +28,18 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
(event, emit) async {
await event.when(
initial: () async {
_listener
..didUpdateUserWorkspaces = (workspaces) {
add(UserWorkspaceEvent.updateWorkspaces(workspaces));
}
..start();
_listener.start(
didUpdateUserWorkspaces: (workspaces) =>
add(UserWorkspaceEvent.updateWorkspaces(workspaces)),
didUpdateUserWorkspace: (workspace) {
// If currentWorkspace is updated, eg. Icon or Name, we should notify
// the UI to render the updated information.
final currentWorkspace = state.currentWorkspace;
if (currentWorkspace?.workspaceId == workspace.workspaceId) {
add(UserWorkspaceEvent.updateCurrentWorkspace(workspace));
}
},
);
final result = await _fetchWorkspaces();
final currentWorkspace = result.$1;
@ -337,6 +345,25 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
),
);
},
updateCurrentWorkspace: (workspace) async {
final workspaces = [...state.workspaces];
final index = workspaces
.indexWhere((e) => e.workspaceId == workspace.workspaceId);
if (index != -1) {
workspaces[index] = workspace;
}
emit(
state.copyWith(
currentWorkspace: workspace,
workspaces: workspaces
..sort(
(a, b) =>
a.createdAtTimestamp.compareTo(b.createdAtTimestamp),
),
),
);
},
);
},
);
@ -413,6 +440,9 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent {
const factory UserWorkspaceEvent.updateWorkspaces(
RepeatedUserWorkspacePB workspaces,
) = UpdateWorkspaces;
const factory UserWorkspaceEvent.updateCurrentWorkspace(
UserWorkspacePB workspace,
) = UpdateCurrentWorkspace;
}
enum UserWorkspaceActionType {

View File

@ -88,40 +88,38 @@ class DesktopHomeScreen extends StatelessWidget {
FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: HomeHotKeys(
userProfile: userProfile,
child: Scaffold(
floatingActionButton: enableMemoryLeakDetect
? const FloatingActionButton(
onPressed: dumpMemoryLeak,
child: Icon(Icons.memory),
)
: null,
body: BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.latestView != c.latestView,
listener: (context, state) {
final view = state.latestView;
if (view != null) {
// Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
final currentPageManager =
context.read<TabsBloc>().state.currentPageManager;
child: Scaffold(
floatingActionButton: enableMemoryLeakDetect
? const FloatingActionButton(
onPressed: dumpMemoryLeak,
child: Icon(Icons.memory),
)
: null,
body: BlocListener<HomeBloc, HomeState>(
listenWhen: (p, c) => p.latestView != c.latestView,
listener: (context, state) {
final view = state.latestView;
if (view != null) {
// Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
final currentPageManager =
context.read<TabsBloc>().state.currentPageManager;
if (currentPageManager.plugin.pluginType ==
PluginType.blank) {
getIt<TabsBloc>().add(
TabsEvent.openPlugin(plugin: view.plugin()),
);
}
if (currentPageManager.plugin.pluginType ==
PluginType.blank) {
getIt<TabsBloc>().add(
TabsEvent.openPlugin(plugin: view.plugin()),
);
}
},
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) => BlocProvider(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
),
}
},
child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) => BlocProvider(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(const UserWorkspaceEvent.initial()),
child: HomeHotKeys(
userProfile: userProfile,
child: FlowyContainer(
Theme.of(context).colorScheme.surface,
child: _buildBody(context, userProfile, workspaceSetting),

View File

@ -136,9 +136,9 @@ class HomeSideBar extends StatelessWidget {
workspaceSetting.workspaceId,
),
);
context.read<FavoriteBloc>().add(
const FavoriteEvent.fetchFavorites(),
);
context
.read<FavoriteBloc>()
.add(const FavoriteEvent.fetchFavorites());
}
},
),

View File

@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy_backend/log.dart';
@ -65,9 +66,14 @@ class UserSettingButton extends StatelessWidget {
void showSettingsDialog(BuildContext context, UserProfilePB userProfile) =>
showDialog(
context: context,
builder: (dialogContext) => BlocProvider<DocumentAppearanceCubit>.value(
builder: (dialogContext) => MultiBlocProvider(
key: _settingsDialogKey,
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
providers: [
BlocProvider<DocumentAppearanceCubit>.value(
value: BlocProvider.of<DocumentAppearanceCubit>(dialogContext),
),
BlocProvider.value(value: context.read<UserWorkspaceBloc>()),
],
child: SettingsDialog(
userProfile,
didLogout: () async {

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
@ -14,14 +16,10 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarWorkspace extends StatefulWidget {
const SidebarWorkspace({
super.key,
required this.userProfile,
});
const SidebarWorkspace({super.key, required this.userProfile});
final UserProfilePB userProfile;
@ -197,6 +195,12 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget {
workspace: currentWorkspace,
iconSize: 20,
enableEdit: false,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId,
result.emoji,
),
),
),
),
const HSpace(6),

View File

@ -1,25 +1,26 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class WorkspaceIcon extends StatefulWidget {
const WorkspaceIcon({
super.key,
required this.workspace,
required this.enableEdit,
required this.iconSize,
required this.workspace,
required this.onSelected,
});
final UserWorkspacePB workspace;
final double iconSize;
final bool enableEdit;
final void Function(EmojiPickerResult) onSelected;
@override
State<WorkspaceIcon> createState() => _WorkspaceIconState();
@ -45,7 +46,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
height: max(widget.iconSize, 26),
decoration: BoxDecoration(
color: ColorGenerator(widget.workspace.name).toColor(),
borderRadius: BorderRadius.circular(4),
borderRadius: BorderRadius.circular(8),
),
child: FlowyText(
widget.workspace.name.isEmpty
@ -55,6 +56,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
color: Colors.black,
),
);
if (widget.enableEdit) {
child = AppFlowyPopover(
offset: const Offset(0, 8),
@ -62,19 +64,12 @@ class _WorkspaceIconState extends State<WorkspaceIcon> {
direction: PopoverDirection.bottomWithLeftAligned,
constraints: BoxConstraints.loose(const Size(360, 380)),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (BuildContext popoverContext) {
return FlowyIconPicker(
onSelected: (result) {
context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
widget.workspace.workspaceId,
result.emoji,
),
);
controller.close();
},
);
},
popupBuilder: (_) => FlowyIconPicker(
onSelected: (result) {
widget.onSelected(result);
controller.close();
},
),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: child,

View File

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
@ -11,7 +13,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@visibleForTesting
@ -152,6 +153,12 @@ class WorkspaceMenuItem extends StatelessWidget {
workspace: workspace,
iconSize: 26,
enableEdit: true,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
),
),
),
);

View File

@ -10,8 +10,6 @@ import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
@ -58,11 +56,9 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_accountPage_title.tr(),
description: LocaleKeys.settings_accountPage_description.tr(),
children: [
SettingsHeader(
title: LocaleKeys.settings_accountPage_title.tr(),
description: LocaleKeys.settings_accountPage_description.tr(),
),
SettingsCategory(
title: LocaleKeys.settings_accountPage_general_title.tr(),
children: [
@ -140,7 +136,6 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
// ),
// ],
// ),
const SettingsCategorySpacer(),
SettingsCategory(
title: LocaleKeys.settings_accountPage_keys_title.tr(),
children: [
@ -174,7 +169,6 @@ class _SettingsAccountViewState extends State<SettingsAccountView> {
),
],
),
const SettingsCategorySpacer(),
SettingsCategory(
title: LocaleKeys.settings_accountPage_login_title.tr(),
children: [
@ -409,10 +403,10 @@ class _UserProfileSettingState extends State<UserProfileSetting> {
width: 360,
margin: const EdgeInsets.symmetric(horizontal: 12),
child: FlowyIconPicker(
onSelected: (result) {
context.read<SettingsUserViewBloc>().add(
SettingsUserEvent.updateUserIcon(iconUrl: result.emoji),
);
onSelected: (r) {
context
.read<SettingsUserViewBloc>()
.add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji));
Navigator.of(dialogContext).pop();
},
),

View File

@ -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,
),
],
);
}
}

View File

@ -3,12 +3,11 @@ import 'package:flutter/material.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
@ -78,10 +77,8 @@ class SettingsDialog extends StatelessWidget {
didLogout: didLogout,
didLogin: dismissDialog,
);
case SettingsPage.appearance:
return const SettingsAppearanceView();
case SettingsPage.language:
return const SettingsLanguageView();
case SettingsPage.workspace:
return SettingsWorkspaceView(userProfile: user);
case SettingsPage.files:
return const SettingsFileSystemView();
case SettingsPage.notifications:

View File

@ -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(),
],
),
);
}

View File

@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
class DocumentColorSettingButton extends StatelessWidget {
class DocumentColorSettingButton extends StatefulWidget {
const DocumentColorSettingButton({
super.key,
required this.currentColor,
@ -27,41 +28,53 @@ class DocumentColorSettingButton extends StatelessWidget {
final void Function(Color selectedColorOnDialog) onApply;
@override
State<DocumentColorSettingButton> createState() =>
_DocumentColorSettingButtonState();
}
class _DocumentColorSettingButtonState
extends State<DocumentColorSettingButton> {
late Color newColor = widget.currentColor;
@override
Widget build(BuildContext context) {
return FlowyButton(
margin: const EdgeInsets.all(8),
text: previewWidgetBuilder.call(currentColor),
text: widget.previewWidgetBuilder.call(widget.currentColor),
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
expandText: false,
onTap: () => Dialogs.show(
context,
child: _DocumentColorSettingDialog(
currentColor: currentColor,
previewWidgetBuilder: previewWidgetBuilder,
dialogTitle: dialogTitle,
onApply: onApply,
),
),
onTap: () => SettingsAlertDialog(
title: widget.dialogTitle,
confirm: () {
widget.onApply(newColor);
Navigator.of(context).pop();
},
children: [
_DocumentColorSettingDialog(
formKey: GlobalKey<FormState>(),
currentColor: widget.currentColor,
previewWidgetBuilder: widget.previewWidgetBuilder,
onChanged: (color) => newColor = color,
),
],
).show(context),
);
}
}
class _DocumentColorSettingDialog extends StatefulWidget {
const _DocumentColorSettingDialog({
required this.formKey,
required this.currentColor,
required this.previewWidgetBuilder,
required this.dialogTitle,
required this.onApply,
required this.onChanged,
});
final GlobalKey<FormState> formKey;
final Color currentColor;
final Widget Function(Color?) previewWidgetBuilder;
final String dialogTitle;
final void Function(Color selectedColorOnDialog) onApply;
final void Function(Color selectedColor) onChanged;
@override
State<_DocumentColorSettingDialog> createState() =>
@ -76,16 +89,16 @@ class DocumentColorSettingDialogState
late String currentColorHexString;
late TextEditingController hexController;
late TextEditingController opacityController;
final _formKey = GlobalKey<FormState>(debugLabel: 'colorSettingForm');
void updateSelectedColor() {
if (_formKey.currentState!.validate()) {
if (widget.formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
widget.onChanged(selectedColorOnDialog!);
});
}
}
@ -112,74 +125,43 @@ class DocumentColorSettingDialogState
@override
Widget build(BuildContext context) {
return FlowyDialog(
constraints: const BoxConstraints(maxWidth: 360, maxHeight: 320),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const Spacer(),
FlowyText(widget.dialogTitle),
const VSpace(8),
SizedBox(
width: 100,
height: 40,
child: Center(
child: widget.previewWidgetBuilder(
selectedColorOnDialog,
),
),
return Column(
children: [
SizedBox(
width: 100,
height: 40,
child: Center(
child: widget.previewWidgetBuilder(
selectedColorOnDialog,
),
const VSpace(8),
SizedBox(
height: 160,
child: Form(
key: _formKey,
child: Column(
children: [
_ColorSettingTextField(
controller: hexController,
labelText: LocaleKeys.editor_hexValue.tr(),
hintText: '6fc9e7',
onFieldSubmitted: (_) => updateSelectedColor(),
validator: (hexValue) => validateHexValue(
hexValue,
opacityController.text,
),
),
const VSpace(8),
_ColorSettingTextField(
controller: opacityController,
labelText: LocaleKeys.editor_opacity.tr(),
hintText: '50',
onFieldSubmitted: (_) => updateSelectedColor(),
validator: (value) => validateOpacityValue(value),
),
],
),
),
),
const VSpace(8),
RoundedTextButton(
title: LocaleKeys.settings_appearance_documentSettings_apply.tr(),
width: 100,
height: 30,
onPressed: () {
if (_formKey.currentState!.validate()) {
if (selectedColorOnDialog != null &&
selectedColorOnDialog != widget.currentColor) {
widget.onApply.call(selectedColorOnDialog!);
}
} else {
// error message will be shown below the text field
return;
}
Navigator.of(context).pop();
},
),
],
),
),
),
const VSpace(8),
Form(
key: widget.formKey,
child: Column(
children: [
_ColorSettingTextField(
controller: hexController,
labelText: LocaleKeys.editor_hexValue.tr(),
hintText: '6fc9e7',
onChanged: (_) => updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(),
validator: (v) => validateHexValue(v, opacityController.text),
),
const VSpace(8),
_ColorSettingTextField(
controller: opacityController,
labelText: LocaleKeys.editor_opacity.tr(),
hintText: '50',
onChanged: (_) => updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(),
validator: (value) => validateOpacityValue(value),
),
],
),
),
],
);
}
}
@ -190,14 +172,15 @@ class _ColorSettingTextField extends StatelessWidget {
required this.labelText,
required this.hintText,
required this.onFieldSubmitted,
required this.validator,
this.onChanged,
this.validator,
});
final TextEditingController controller;
final String labelText;
final String hintText;
final void Function(String) onFieldSubmitted;
final void Function(String)? onChanged;
final String? Function(String?)? validator;
@override
@ -209,17 +192,14 @@ class _ColorSettingTextField extends StatelessWidget {
labelText: labelText,
hintText: hintText,
border: OutlineInputBorder(
borderSide: BorderSide(
color: style.colorScheme.outline,
),
borderSide: BorderSide(color: style.colorScheme.outline),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: style.colorScheme.outline,
),
borderSide: BorderSide(color: style.colorScheme.outline),
),
),
style: style.textTheme.bodyMedium,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
validator: validator,
autovalidateMode: AutovalidateMode.onUserInteraction,
@ -227,10 +207,7 @@ class _ColorSettingTextField extends StatelessWidget {
}
}
String? validateHexValue(
String? hexValue,
String opacityValue,
) {
String? validateHexValue(String? hexValue, String opacityValue) {
if (hexValue == null || hexValue.isEmpty) {
return LocaleKeys.settings_appearance_documentSettings_hexEmptyError.tr();
}

View File

@ -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!),
],
],
),
),
),
),
);
}
}

View File

@ -1,12 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class FlowySettingListTile extends StatelessWidget {
const FlowySettingListTile({
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class SettingListTile extends StatelessWidget {
const SettingListTile({
super.key,
this.resetTooltipText,
this.resetButtonKey,
@ -67,54 +67,3 @@ class FlowySettingListTile extends StatelessWidget {
);
}
}
class FlowySettingValueDropDown extends StatefulWidget {
const FlowySettingValueDropDown({
super.key,
required this.currentValue,
required this.popupBuilder,
this.popoverKey,
this.onClose,
this.child,
this.popoverController,
this.offset,
});
final String currentValue;
final Key? popoverKey;
final Widget Function(BuildContext) popupBuilder;
final void Function()? onClose;
final Widget? child;
final PopoverController? popoverController;
final Offset? offset;
@override
State<FlowySettingValueDropDown> createState() =>
_FlowySettingValueDropDownState();
}
class _FlowySettingValueDropDownState extends State<FlowySettingValueDropDown> {
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
key: widget.popoverKey,
controller: widget.popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: widget.popupBuilder,
constraints: const BoxConstraints(
minWidth: 80,
maxWidth: 160,
maxHeight: 400,
),
offset: widget.offset,
onClose: widget.onClose,
child: widget.child ??
FlowyTextButton(
widget.currentValue,
fontColor: Theme.of(context).colorScheme.onBackground,
fillColor: Colors.transparent,
onPressed: () {},
),
);
}
}

View File

@ -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: () {},
),
);
}
}

View File

@ -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,
),
],
],
);
}
}

View File

@ -1,11 +1,19 @@
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class SettingsBody extends StatelessWidget {
const SettingsBody({
super.key,
required this.title,
this.description,
required this.children,
});
final String title;
final String? description;
final List<Widget> children;
@override
@ -14,8 +22,18 @@ class SettingsBody extends StatelessWidget {
physics: const ClampingScrollPhysics(),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
children: [
SettingsHeader(title: title, description: description),
Flexible(
child: SeparatedColumn(
separatorBuilder: () => const SettingsCategorySpacer(),
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
],
),
);
}

View File

@ -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();
}
}

View File

@ -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!,
),
],
],
);
}
}

View File

@ -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(),
);
}
}

View File

@ -3,8 +3,6 @@ import 'package:flutter/material.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
class FeatureFlagsPage extends StatelessWidget {
@ -15,15 +13,14 @@ class FeatureFlagsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsBody(
title: 'Feature flags',
children: [
const SettingsHeader(title: 'Feature flags'),
SeparatedColumn(
children: FeatureFlag.data.entries
.where((e) => e.key != FeatureFlag.unknown)
.map((e) => _FeatureFlagItem(featureFlag: e.key))
.toList(),
),
const SettingsCategorySpacer(),
FlowyTextButton(
'Restart the app to apply changes',
fontSize: 16.0,

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
@ -16,7 +17,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/rounded_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
@ -34,11 +34,8 @@ class WorkspaceMembersPage extends StatelessWidget {
listener: _showResultDialog,
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_appearance_members_title.tr(),
children: [
// title
SettingsHeader(
title: LocaleKeys.settings_appearance_members_title.tr(),
),
if (state.myRole.canInvite) const _InviteMember(),
if (state.myRole.canInvite && state.members.isNotEmpty)
const SettingsCategorySpacer(),

View File

@ -9,7 +9,6 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -41,10 +40,8 @@ class SettingCloud extends StatelessWidget {
child: BlocBuilder<CloudSettingBloc, CloudSettingState>(
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_menu_cloudSettings.tr(),
children: [
SettingsHeader(
title: LocaleKeys.settings_menu_cloudSettings.tr(),
),
if (Env.enableCustomCloud)
Row(
children: [
@ -55,17 +52,12 @@ class SettingCloud extends StatelessWidget {
),
CloudTypeSwitcher(
cloudType: state.cloudType,
onSelected: (newCloudType) {
context.read<CloudSettingBloc>().add(
CloudSettingEvent.updateCloudType(
newCloudType,
),
);
},
onSelected: (type) => context
.read<CloudSettingBloc>()
.add(CloudSettingEvent.updateCloudType(type)),
),
],
),
const VSpace(8),
_viewFromCloudType(state.cloudType),
],
);
@ -73,9 +65,7 @@ class SettingCloud extends StatelessWidget {
),
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
return const Center(child: CircularProgressIndicator());
}
},
);

View File

@ -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();
},
),
);
}
}

View File

@ -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)),
),
],
),
);
}
}

View File

@ -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;
},
);
},
),
),
],
);
}
}

View File

@ -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 "";
}
}
}

View File

@ -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);
},
),
],
);
}
}

View File

@ -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,
),
],
);
}
}

View File

@ -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,
),
],
);
}
}

View File

@ -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;
}

View File

@ -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';

View File

@ -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 "";
}
}
}

View File

@ -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(),
],
);
},
),
);
}
}

View File

@ -6,7 +6,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/strin
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart';
import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -22,10 +21,8 @@ class SettingsShortcutsView extends StatelessWidget {
create: (_) =>
ShortcutsCubit(SettingsShortcutService())..fetchShortcuts(),
child: SettingsBody(
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
children: [
SettingsHeader(
title: LocaleKeys.settings_shortcuts_shortcutsLabel.tr(),
),
BlocBuilder<ShortcutsCubit, ShortcutsState>(
builder: (_, state) => switch (state.status) {
ShortcutsStatus.initial ||
@ -110,8 +107,8 @@ class ShortcutsListTile extends StatelessWidget {
),
FlowyTextButton(
shortcutEvent.command,
fillColor: Colors.transparent,
fontColor: AFThemeExtension.of(context).textColor,
fillColor: Colors.transparent,
onPressed: () => showKeyListenerDialog(context),
),
],

View File

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/files/setting_file_import_appflowy_data_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_export_file_widget.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_file_cache_widget.dart';
@ -17,16 +16,16 @@ class SettingsFileSystemView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsBody(
children: [
SettingsHeader(title: LocaleKeys.settings_menu_files.tr()),
const SettingsFileLocationCustomizer(),
const SettingsCategorySpacer(),
title: LocaleKeys.settings_menu_files.tr(),
children: const [
SettingsFileLocationCustomizer(),
SettingsCategorySpacer(),
if (kDebugMode) ...[
const SettingsExportFileWidget(),
SettingsExportFileWidget(),
],
const ImportAppFlowyData(),
const SettingsCategorySpacer(),
const SettingsFileCacheWidget(),
ImportAppFlowyData(),
SettingsCategorySpacer(),
SettingsFileCacheWidget(),
],
);
}

View File

@ -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();
},
),
);
}
}

View File

@ -55,19 +55,22 @@ class SettingsMenu extends StatelessWidget {
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.appearance,
page: SettingsPage.workspace,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_appearance.tr(),
icon: const Icon(Icons.brightness_4),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.language,
selectedPage: currentPage,
label: LocaleKeys.settings_menu_language.tr(),
icon: const Icon(Icons.translate),
label: LocaleKeys.settings_workspacePage_menuLabel.tr(),
icon: const FlowySvg(FlowySvgs.settings_workplace_m),
changeSelectedPage: changeSelectedPage,
),
if (FeatureFlag.membersSettings.isOn &&
userProfile.authenticator ==
AuthenticatorPB.AppFlowyCloud)
SettingsMenuElement(
page: SettingsPage.member,
selectedPage: currentPage,
label: LocaleKeys.settings_appearance_members_label.tr(),
icon: const Icon(Icons.people),
changeSelectedPage: changeSelectedPage,
),
SettingsMenuElement(
page: SettingsPage.files,
selectedPage: currentPage,
@ -96,16 +99,6 @@ class SettingsMenu extends StatelessWidget {
icon: const Icon(Icons.cut),
changeSelectedPage: changeSelectedPage,
),
if (FeatureFlag.membersSettings.isOn &&
userProfile.authenticator ==
AuthenticatorPB.AppFlowyCloud)
SettingsMenuElement(
page: SettingsPage.member,
selectedPage: currentPage,
label: LocaleKeys.settings_appearance_members_label.tr(),
icon: const Icon(Icons.people),
changeSelectedPage: changeSelectedPage,
),
if (kDebugMode)
SettingsMenuElement(
// no need to translate this page

View File

@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart';
import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/theme_setting_entry_template.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -16,9 +15,9 @@ class SettingsNotificationsView extends StatelessWidget {
return BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_menu_notifications.tr(),
children: [
SettingsHeader(title: LocaleKeys.settings_menu_notifications.tr()),
FlowySettingListTile(
SettingListTile(
label: LocaleKeys.settings_notifications_enableNotifications_label
.tr(),
hint: LocaleKeys.settings_notifications_enableNotifications_hint

View File

@ -240,6 +240,7 @@ flutter:
# To add assets to your application, add an assets section, like this:
assets:
- assets/images/
- assets/images/appearance/
- assets/images/built_in_cover_images/
- assets/flowy_icons/
- assets/flowy_icons/16x/

View File

@ -1,4 +1,4 @@
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/levenshtein.dart';
import 'package:appflowy/util/levenshtein.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {

View File

@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance/font_family_setting.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

View File

@ -9,6 +9,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
@ -92,7 +97,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $<CONFIG>
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS

View 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_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

View 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

View 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

View File

@ -347,6 +347,79 @@
"logoutLabel": "Log out"
}
},
"workspacePage": {
"menuLabel": "Workspace",
"title": "Workspace",
"description": "Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.",
"workspaceName": {
"title": "Workspace name",
"savedMessage": "Saved workspace name"
},
"workspaceIcon": {
"title": "Workspace icon",
"description": "Customize your workspace appearance, theme, font, text layout, date, time, and language."
},
"appearance": {
"title": "Appearance",
"description": "Customize your workspace appearance, theme, font, text layout, date, time, and language.",
"options": {
"system": "Auto",
"light": "Light",
"dark": "Dark"
}
},
"theme": {
"title": "Theme",
"description": "Select a preset theme, or upload your own custom theme."
},
"workspaceFont": {
"title": "Workspace font"
},
"textDirection": {
"title": "Text direction",
"leftToRight": "Left to right",
"rightToLeft": "Right to left",
"auto": "Auto",
"enableRTLItems": "Enable RTL toolbar items"
},
"layoutDirection": {
"title": "Layout direction",
"leftToRight": "Left to right",
"rightToLeft": "Right to left"
},
"dateTime": {
"title": "Date & time",
"example": "{} at {} ({})",
"24HourTime": "24-hour time",
"dateFormat": {
"label": "Date format",
"local": "Local",
"us": "US",
"iso": "ISO",
"friendly": "Friendly",
"dmy": "D/M/Y"
}
},
"language": {
"title": "Language"
},
"deleteWorkspacePrompt": {
"title": "Delete workspace",
"content": "Are you sure you want to delete this workspace? This action cannot be undone."
},
"leaveWorkspacePrompt": {
"title": "Leave workspace",
"content": "Are you sure you want to leave this workspace? You will lose access to all pages and data within it."
},
"manageWorkspace": {
"title": "Manage workspace",
"leaveWorkspace": "Leave workspace",
"deleteWorkspace": "Delete workspace"
}
},
"common": {
"reset": "Reset"
},
"menu": {
"appearance": "Appearance",
"language": "Language",

View File

@ -13,6 +13,7 @@ pub(crate) enum UserNotification {
DidUpdateUserProfile = 2,
DidUpdateUserWorkspaces = 3,
DidUpdateCloudConfig = 4,
DidUpdateUserWorkspace = 5,
}
impl std::convert::From<UserNotification> for i32 {

View File

@ -39,11 +39,13 @@ use crate::services::collab_interact::{CollabInteract, DefaultCollabInteract};
use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset};
use crate::user_manager::manager_user_encryption::validate_encryption_sign;
use crate::user_manager::manager_user_workspace::save_user_workspaces;
use crate::user_manager::manager_user_workspace::save_all_user_workspaces;
use crate::user_manager::user_login_state::UserAuthProcess;
use crate::{errors::FlowyError, notification::*};
use flowy_user_pub::session::Session;
use super::manager_user_workspace::save_user_workspace;
pub struct UserManager {
pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>,
pub(crate) store_preferences: Arc<StorePreferences>,
@ -708,7 +710,7 @@ impl UserManager {
self.set_anon_user(session.clone());
}
save_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?;
save_all_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?;
info!(
"Save new user profile to disk, authenticator: {:?}",
authenticator
@ -779,13 +781,13 @@ impl UserManager {
}
// Save the old user workspace setting.
save_user_workspaces(
save_user_workspace(
old_user.session.user_id,
self
.authenticate_user
.database
.get_connection(old_user.session.user_id)?,
&[old_user.session.user_workspace.clone()],
&old_user.session.user_workspace.clone(),
)?;
Ok(())
}

View File

@ -14,7 +14,7 @@ use flowy_user_pub::entities::{
};
use lib_dispatch::prelude::af_spawn;
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB};
use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB, UserWorkspacePB};
use crate::migrations::AnonUser;
use crate::notification::{send_notification, UserNotification};
use crate::services::data_import::{
@ -239,7 +239,14 @@ impl UserManager {
user_workspace.icon = new_workspace_icon.to_string();
}
save_user_workspaces(uid, conn, &[user_workspace])
let _ = save_user_workspace(uid, conn, &user_workspace);
let payload: UserWorkspacePB = user_workspace.clone().into();
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace)
.payload(payload)
.send();
Ok(())
}
#[instrument(level = "info", skip(self), err)]
@ -371,7 +378,7 @@ impl UserManager {
af_spawn(async move {
if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await {
if let Ok(conn) = pool.get() {
let _ = save_user_workspaces(uid, conn, &new_user_workspaces);
let _ = save_all_user_workspaces(uid, conn, &new_user_workspaces);
let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces);
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces)
.payload(repeated_workspace_pbs)
@ -403,7 +410,48 @@ impl UserManager {
}
}
pub fn save_user_workspaces(
/// This method is used to save one user workspace to the SQLite database
///
/// If the workspace is already persisted in the database, it will be overridden.
///
/// Consider using [save_all_user_workspaces] if you need to override all workspaces of the user.
///
pub fn save_user_workspace(
uid: i64,
mut conn: DBConnection,
user_workspace: &UserWorkspace,
) -> FlowyResult<()> {
conn.immediate_transaction(|conn| {
let user_workspace = UserWorkspaceTable::try_from((uid, user_workspace))?;
let affected_rows = diesel::update(
user_workspace_table::dsl::user_workspace_table
.filter(user_workspace_table::id.eq(&user_workspace.id)),
)
.set((
user_workspace_table::name.eq(&user_workspace.name),
user_workspace_table::created_at.eq(&user_workspace.created_at),
user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id),
user_workspace_table::icon.eq(&user_workspace.icon),
))
.execute(conn)?;
if affected_rows == 0 {
diesel::insert_into(user_workspace_table::table)
.values(user_workspace)
.execute(conn)?;
}
Ok::<(), FlowyError>(())
})
}
/// This method is used to save the user workspaces (plural) to the SQLite database
///
/// The workspaces provided in [user_workspaces] will override the existing workspaces in the database.
///
/// Consider using [save_user_workspace] if you only need to save a single workspace.
///
pub fn save_all_user_workspaces(
uid: i64,
mut conn: DBConnection,
user_workspaces: &[UserWorkspace],