diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart new file mode 100644 index 0000000000..646a4eb565 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart @@ -0,0 +1,32 @@ +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MoreViewActions', () { + testWidgets('can duplicate and delete from menu', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.pumpAndSettle(); + + final pageFinder = find.byType(ViewItem); + expect(pageFinder, findsNWidgets(1)); + + // Duplicate + await tester.openMoreViewActions(); + await tester.duplicateByMoreViewActions(); + + expect(pageFinder, findsNWidgets(2)); + + // Delete + await tester.openMoreViewActions(); + await tester.deleteByMoreViewActions(); + + expect(pageFinder, findsNWidgets(1)); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart index 42462c2658..239e7e09a8 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart @@ -6,6 +6,9 @@ import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test; import 'document_create_and_delete_test.dart' as document_create_and_delete_test; import 'document_option_action_test.dart' as document_option_action_test; +import 'document_inline_page_reference_test.dart' + as document_inline_page_reference_test; +import 'document_more_actions_test.dart' as document_more_actions_test; import 'document_text_direction_test.dart' as document_text_direction_test; import 'document_with_cover_image_test.dart' as document_with_cover_image_test; import 'document_with_database_test.dart' as document_with_database_test; @@ -16,8 +19,6 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_test; import 'document_with_outline_block_test.dart' as document_with_outline_block; import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test; import 'edit_document_test.dart' as document_edit_test; -import 'document_inline_page_reference_test.dart' - as document_inline_page_reference_test; void startTesting() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -38,4 +39,5 @@ void startTesting() { document_option_action_test.main(); document_with_image_block_test.main(); document_inline_page_reference_test.main(); + document_more_actions_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 08d6fd0ec6..a2a6318c3d 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,5 +1,10 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -22,15 +27,13 @@ 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/widgets/more_view_actions/more_view_actions.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.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'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -564,6 +567,44 @@ extension CommonOperations on WidgetTester { ); await tapButton(button); } + + Future openMoreViewActions() async { + final button = find.byType(MoreViewActions); + await tap(button); + await pumpAndSettle(); + } + + /// Presses on the Duplicate ViewAction in the [MoreViewActions] popup. + /// + /// [openMoreViewActions] must be called beforehand! + /// + Future duplicateByMoreViewActions() async { + final button = find.descendant( + of: find.byType(ListView), + matching: find.byWidgetPredicate( + (widget) => + widget is ViewAction && widget.type == ViewActionType.duplicate, + ), + ); + await tap(button); + await pump(); + } + + /// Presses on the Delete ViewAction in the [MoreViewActions] popup. + /// + /// [openMoreViewActions] must be called beforehand! + /// + Future deleteByMoreViewActions() async { + final button = find.descendant( + of: find.byType(ListView), + matching: find.byWidgetPredicate( + (widget) => + widget is ViewAction && widget.type == ViewActionType.delete, + ), + ); + await tap(button); + await pump(); + } } extension SettingsFinder on CommonFinders { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart index 64dd62729c..390f0824de 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart @@ -29,9 +29,7 @@ class FontPickerScreen extends StatelessWidget { } class LanguagePickerPage extends StatefulWidget { - const LanguagePickerPage({ - super.key, - }); + const LanguagePickerPage({super.key}); @override State createState() => _LanguagePickerPageState(); @@ -43,7 +41,6 @@ class _LanguagePickerPageState extends State { @override void initState() { super.initState(); - availableFonts = _availableFonts; } @@ -90,7 +87,6 @@ class _FontSelectorState extends State { @override void initState() { super.initState(); - availableFonts = _availableFonts; } diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index 1297bccc37..86c3b1e625 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; @@ -83,7 +84,9 @@ class _FlowyEmojiPickerState extends State { }, itemBuilder: (context, emojiId, emoji, callback) { return FlowyIconButton( - iconPadding: const EdgeInsets.all(2.0), + iconPadding: PlatformExtension.isWindows + ? const EdgeInsets.only(bottom: 2.0) + : const EdgeInsets.all(2), icon: FlowyText( emoji, fontSize: 28.0, diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index b3e7d6fbe0..81a081b4e3 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -113,7 +113,7 @@ typedef WorkspaceSettingNotifyValue class UserWorkspaceListener { UserWorkspaceListener(); - PublishNotifier? _settingChangedNotifier = + final PublishNotifier _settingChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; @@ -122,7 +122,7 @@ class UserWorkspaceListener { void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, }) { if (onSettingUpdated != null) { - _settingChangedNotifier?.addPublishListener(onSettingUpdated); + _settingChangedNotifier.addPublishListener(onSettingUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -140,13 +140,11 @@ class UserWorkspaceListener { switch (ty) { case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _settingChangedNotifier?.value = + (payload) => _settingChangedNotifier.value = FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)), - (error) => - _settingChangedNotifier?.value = FlowyResult.failure(error), + (error) => _settingChangedNotifier.value = FlowyResult.failure(error), ); break; - default: break; } @@ -154,8 +152,6 @@ class UserWorkspaceListener { Future stop() async { await _listener?.stop(); - - _settingChangedNotifier?.dispose(); - _settingChangedNotifier = null; + _settingChangedNotifier.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/util/theme_extension.dart b/frontend/appflowy_flutter/lib/util/theme_extension.dart new file mode 100644 index 0000000000..c7b56699d3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/theme_extension.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +extension IsLightMode on ThemeData { + bool get isLightMode => brightness == Brightness.light; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart index 5da3caa5b9..0dfa2807b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart @@ -8,6 +8,6 @@ extension TimeFormatter on UserTimeFormatPB { } final _toFormat = { - UserTimeFormatPB.TwelveHour: DateFormat.Hm(), - UserTimeFormatPB.TwentyFourHour: DateFormat.jm(), + UserTimeFormatPB.TwentyFourHour: DateFormat.Hm(), + UserTimeFormatPB.TwelveHour: DateFormat.jm(), }; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index 5c02ea6b11..d7980e031a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -56,7 +56,13 @@ class WorkspaceSettingsBloc ?.role ?? AFRolePB.Guest; - emit(state.copyWith(members: members, myRole: role)); + emit( + state.copyWith( + workspace: currentWorkspaceInList, + members: members, + myRole: role, + ), + ); } catch (e) { Log.error('Failed to get or create current workspace'); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index f3f78ee99b..101c12c3a6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -38,6 +38,7 @@ class _WorkspaceIconState extends State { child: EmojiText( emoji: widget.workspace.icon, fontSize: widget.iconSize, + lineHeight: 1, ), ) : Container( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index 9ee584e5dc..ebbb4f0e07 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; @@ -78,46 +79,46 @@ class _SettingsAccountViewState extends State { ], ), - // Enable when/if we need change email feature - // // Only show change email if the user is authenticated and not using local auth - // if (isAuthEnabled && - // state.userProfile.authenticator != AuthenticatorPB.Local) ...[ - // const SettingsCategorySpacer(), - // SettingsCategory( - // title: LocaleKeys.settings_accountPage_email_title.tr(), - // children: [ - // SingleSettingAction( - // label: state.userProfile.email, - // buttonLabel: LocaleKeys - // .settings_accountPage_email_actions_change - // .tr(), - // onPressed: () => SettingsAlertDialog( - // title: LocaleKeys - // .settings_accountPage_email_actions_change - // .tr(), - // confirmLabel: LocaleKeys.button_save.tr(), - // confirm: () { - // context.read().add( - // SettingsUserEvent.updateUserEmail( - // _emailController.text, - // ), - // ); - // Navigator.of(context).pop(); - // }, - // children: [ - // SettingsInputField( - // label: LocaleKeys.settings_accountPage_email_title - // .tr(), - // value: state.userProfile.email, - // hideActions: true, - // textController: _emailController, - // ), - // ], - // ).show(context), - // ), - // ], - // ), - // ], + // Only show email if the user is authenticated and not using local auth + if (isAuthEnabled && + state.userProfile.authenticator != AuthenticatorPB.Local) ...[ + SettingsCategory( + title: LocaleKeys.settings_accountPage_email_title.tr(), + children: [ + FlowyText.regular(state.userProfile.email), + // Enable when/if we need change email feature + // SingleSettingAction( + // label: state.userProfile.email, + // buttonLabel: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // onPressed: () => SettingsAlertDialog( + // title: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // confirmLabel: LocaleKeys.button_save.tr(), + // confirm: () { + // context.read().add( + // SettingsUserEvent.updateUserEmail( + // _emailController.text, + // ), + // ); + // Navigator.of(context).pop(); + // }, + // children: [ + // SettingsInputField( + // label: LocaleKeys.settings_accountPage_email_title + // .tr(), + // value: state.userProfile.email, + // hideActions: true, + // textController: _emailController, + // ), + // ], + // ).show(context), + // ), + ], + ), + ], /// Enable when we have change password feature and 2FA // const SettingsCategorySpacer(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index 17d76b4fe1..4ad5d00e1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -9,6 +9,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/rust_sdk.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; @@ -55,6 +56,9 @@ class SettingsManageDataView extends StatelessWidget { actions: [ if (state.mapOrNull(didReceivedPath: (_) => true) == true) SettingAction( + tooltip: LocaleKeys + .settings_manageDataPage_dataStorage_actions_resetTooltip + .tr(), icon: const FlowySvg(FlowySvgs.restore_s), label: LocaleKeys.settings_common_reset.tr(), onPressed: () => SettingsAlertDialog( @@ -375,6 +379,8 @@ class _CurrentPathState extends State<_CurrentPath> { @override Widget build(BuildContext context) { + final isLM = Theme.of(context).isLightMode; + return Column( children: [ Row( @@ -392,7 +398,9 @@ class _CurrentPathState extends State<_CurrentPath> { maxLines: 2, overflow: TextOverflow.ellipsis, decoration: isHovering ? TextDecoration.underline : null, - color: const Color(0xFF005483), + color: isLM + ? const Color(0xFF005483) + : Theme.of(context).colorScheme.primary, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index c1d167844a..12a932aabc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -18,12 +19,12 @@ import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu 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_input_field.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'; @@ -41,39 +42,22 @@ 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:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; -class SettingsWorkspaceView extends StatefulWidget { +class SettingsWorkspaceView extends StatelessWidget { const SettingsWorkspaceView({super.key, required this.userProfile}); final UserProfilePB userProfile; - @override - State createState() => _SettingsWorkspaceViewState(); -} - -class _SettingsWorkspaceViewState extends State { - final TextEditingController _workspaceNameController = - TextEditingController(); - - @override - void dispose() { - _workspaceNameController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocProvider( create: (context) => WorkspaceSettingsBloc() - ..add(WorkspaceSettingsEvent.initial(userProfile: widget.userProfile)), + ..add(WorkspaceSettingsEvent.initial(userProfile: userProfile)), child: BlocConsumer( listener: (context, state) { - if ((state.workspace?.name ?? '') != _workspaceNameController.text) { - _workspaceNameController.text = state.workspace?.name ?? ''; - } - if (state.deleteWorkspace) { context.read().add( UserWorkspaceEvent.deleteWorkspace( @@ -97,44 +81,11 @@ class _SettingsWorkspaceViewState extends State { 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) ...[ + if (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, - ), - ), - ), - ], - ), - ], + children: const [_WorkspaceNameSetting()], ), SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceIcon_title @@ -143,7 +94,10 @@ class _SettingsWorkspaceViewState extends State { .settings_workspacePage_workspaceIcon_description .tr(), children: [ - _WorkspaceIconSetting(workspace: state.workspace!), + _WorkspaceIconSetting( + enableEdit: state.myRole.isOwner, + workspace: state.workspace, + ), ], ), ], @@ -195,9 +149,7 @@ class _SettingsWorkspaceViewState extends State { title: LocaleKeys.settings_workspacePage_language_title.tr(), children: const [LanguageDropdown()], ), - if (state.workspace != null && - widget.userProfile.authenticator != - AuthenticatorPB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), @@ -244,17 +196,115 @@ class _SettingsWorkspaceViewState extends State { ), ); } +} - void _saveWorkspaceName( - BuildContext context, { - required String current, +class _WorkspaceNameSetting extends StatefulWidget { + const _WorkspaceNameSetting(); + + @override + State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState(); +} + +class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> { + final TextEditingController workspaceNameController = TextEditingController(); + late final FocusNode focusNode; + bool isEditing = false; + + @override + void initState() { + super.initState(); + focusNode = FocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape && + isEditing && + mounted) { + setState(() => isEditing = false); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + )..addListener(() { + if (!focusNode.hasFocus && isEditing && mounted) { + _saveWorkspaceName(name: workspaceNameController.text); + setState(() => isEditing = false); + } + }); + } + + @override + void dispose() { + focusNode.dispose(); + workspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (_, state) { + final newName = state.workspace?.name; + if (newName != null && newName != workspaceNameController.text) { + workspaceNameController.text = newName; + } + }, + builder: (_, state) { + if (isEditing) { + return Flexible( + child: SettingsInputField( + textController: workspaceNameController, + value: workspaceNameController.text, + focusNode: focusNode..requestFocus(), + onCancel: () => setState(() => isEditing = false), + onSave: (_) { + _saveWorkspaceName(name: workspaceNameController.text); + setState(() => isEditing = false); + }, + ), + ); + } + + return Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.5), + child: FlowyText.regular( + workspaceNameController.text, + fontSize: 14, + ), + ), + if (state.myRole.isOwner) ...[ + const HSpace(4), + FlowyTooltip( + message: LocaleKeys + .settings_workspacePage_workspaceName_editTooltip + .tr(), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => isEditing = true), + child: const FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: EdgeInsets.all(4), + child: FlowySvg(FlowySvgs.edit_s), + ), + ), + ), + ), + ], + ], + ); + }, + ); + } + + void _saveWorkspaceName({ required String name, }) { - if (name.isNotEmpty && name != current) { + if (name.isNotEmpty) { context.read().add( - WorkspaceSettingsEvent.updateWorkspaceName( - _workspaceNameController.text, - ), + WorkspaceSettingsEvent.updateWorkspaceName(name), ); if (context.mounted) { @@ -300,12 +350,21 @@ class LanguageDropdown extends StatelessWidget { } class _WorkspaceIconSetting extends StatelessWidget { - const _WorkspaceIconSetting({required this.workspace}); + const _WorkspaceIconSetting({required this.enableEdit, this.workspace}); - final UserWorkspacePB workspace; + final bool enableEdit; + final UserWorkspacePB? workspace; @override Widget build(BuildContext context) { + if (workspace == null) { + return const SizedBox( + height: 64, + width: 64, + child: CircularProgressIndicator(), + ); + } + return Container( height: 64, width: 64, @@ -316,9 +375,9 @@ class _WorkspaceIconSetting extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(1), child: WorkspaceIcon( - workspace: workspace, - iconSize: workspace.icon.isNotEmpty == true ? 46 : 20, - enableEdit: true, + workspace: workspace!, + iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20, + enableEdit: enableEdit, onSelected: (r) => context .read() .add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)), @@ -508,6 +567,7 @@ class _DateTimeFormatLabel extends StatelessWidget { now.timeZoneName, ], ), + maxLines: 2, fontSize: 16, color: AFThemeExtension.of(context).secondaryTextColor, ); @@ -712,6 +772,9 @@ class AppearanceSelector extends StatelessWidget { ), ), ), + child: t != themeMode + ? null + : const _SelectedModeIndicator(), ), const VSpace(6), FlowyText.regular(getLabel(t), textAlign: TextAlign.center), @@ -735,6 +798,38 @@ class AppearanceSelector extends StatelessWidget { }; } +class _SelectedModeIndicator extends StatelessWidget { + const _SelectedModeIndicator(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 4, + left: 4, + child: Material( + shape: const CircleBorder(), + elevation: 2, + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + height: 16, + width: 16, + child: const FlowySvg( + FlowySvgs.settings_selected_theme_m, + size: Size.square(16), + blendMode: BlendMode.dstIn, + ), + ), + ), + ), + ], + ); + } +} + class _FontSelectorDropdown extends StatelessWidget { const _FontSelectorDropdown(); @@ -777,6 +872,7 @@ class _FontSelectorDropdown extends StatelessWidget { selectedValue: appearance.font, value: font, label: font.fontFamilyDisplayName, + fontFamily: font, ), ) .toList(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index ef4c374239..c234f538b0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -1,3 +1,5 @@ +import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -10,7 +12,12 @@ DropdownMenuEntry buildDropdownMenuEntry( T? selectedValue, Widget? leadingWidget, Widget? trailingWidget, + String? fontFamily, }) { + final fontFamilyUsed = fontFamily != null + ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily + : defaultFontFamily; + return DropdownMenuEntry( style: ButtonStyle( foregroundColor: @@ -26,7 +33,12 @@ DropdownMenuEntry buildDropdownMenuEntry( leadingIcon: leadingWidget, labelWidget: Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: FlowyText.medium(label, fontSize: 14, textAlign: TextAlign.start), + child: FlowyText.medium( + label, + fontSize: 14, + textAlign: TextAlign.start, + fontFamily: fontFamilyUsed, + ), ), trailingIcon: Row( children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 67c0dc4cf9..8b28289670 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -1,9 +1,13 @@ +import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; 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'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsDropdown extends StatefulWidget { const SettingsDropdown({ @@ -37,6 +41,10 @@ class _SettingsDropdownState extends State> { @override Widget build(BuildContext context) { + final fontFamily = context.read().state.font; + final fontFamilyUsed = + getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily; + return Row( children: [ Expanded( @@ -45,6 +53,10 @@ class _SettingsDropdownState extends State> { expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, + textStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontFamily: fontFamilyUsed), menuStyle: MenuStyle( maximumSize: const MaterialStatePropertyAll(Size(double.infinity, 250)), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index e104879752..6b9e55fc7d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -5,7 +5,6 @@ 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/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -37,8 +36,6 @@ class WorkspaceMembersPage extends StatelessWidget { title: LocaleKeys.settings_appearance_members_title.tr(), children: [ if (state.myRole.canInvite) const _InviteMember(), - if (state.myRole.canInvite && state.members.isNotEmpty) - const SettingsCategorySpacer(), if (state.members.isNotEmpty) _MemberList( members: state.members, diff --git a/frontend/resources/flowy_icons/24x/settings_selected_theme.svg b/frontend/resources/flowy_icons/24x/settings_selected_theme.svg new file mode 100644 index 0000000000..d6c6b6d809 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_selected_theme.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ca0a190367..0ff64fedb1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -353,7 +353,8 @@ "description": "Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.", "workspaceName": { "title": "Workspace name", - "savedMessage": "Saved workspace name" + "savedMessage": "Saved workspace name", + "editTooltip": "Edit workspace name" }, "workspaceIcon": { "title": "Workspace icon", @@ -429,7 +430,8 @@ "open": "Open folder", "openTooltip": "Open current data folder location", "copy": "Copy path", - "copiedHint": "Link copied!" + "copiedHint": "Path copied!", + "resetTooltip": "Reset to default location" }, "resetDialog": { "title": "Are you sure?", @@ -1673,4 +1675,4 @@ "betaTooltip": "We currently only support searching for pages", "fromTrashHint": "From trash" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index c57450a5d6..1ddcebcafd 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -12,7 +12,7 @@ pub enum FolderNotification { Unknown = 0, /// Trigger after creating a workspace DidCreateWorkspace = 1, - // /// Trigger after updating a workspace + /// Trigger after updating a workspace DidUpdateWorkspace = 2, DidUpdateWorkspaceViews = 3,