diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 8b9971e99a..2dbb970611 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -141,6 +141,7 @@ "menu": { "appearance": "Appearance", "language": "Language", + "user": "User", "open": "Open Settings" }, "appearance": { diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 1864a2aa62..e0ab40eea7 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -5,10 +5,12 @@ import 'package:app_flowy/workspace/application/app/prelude.dart'; import 'package:app_flowy/workspace/application/doc/prelude.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; import 'package:app_flowy/workspace/application/trash/prelude.dart'; +import 'package:app_flowy/workspace/application/user/prelude.dart'; import 'package:app_flowy/workspace/application/workspace/prelude.dart'; import 'package:app_flowy/workspace/application/edit_pannel/edit_pannel_bloc.dart'; import 'package:app_flowy/workspace/application/view/prelude.dart'; import 'package:app_flowy/workspace/application/menu/prelude.dart'; +import 'package:app_flowy/workspace/application/settings/prelude.dart'; import 'package:app_flowy/user/application/prelude.dart'; import 'package:app_flowy/user/presentation/router.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; @@ -101,6 +103,16 @@ void _resolveFolderDeps(GetIt getIt) { (user, _) => MenuUserBloc(user), ); + //Settings + getIt.registerFactoryParam( + (user, _) => SettingsDialogBloc(user), + ); + + //User + getIt.registerFactoryParam( + (user, _) => SettingsUserViewBloc(user), + ); + // AppPB getIt.registerFactoryParam( (app, _) => AppBloc( diff --git a/frontend/app_flowy/lib/workspace/application/settings/prelude.dart b/frontend/app_flowy/lib/workspace/application/settings/prelude.dart new file mode 100644 index 0000000000..3917b54aaf --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/settings/prelude.dart @@ -0,0 +1 @@ +export 'settings_dialog_bloc.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart new file mode 100644 index 0000000000..3c40f767b1 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -0,0 +1,67 @@ +import 'package:app_flowy/user/application/user_listener.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +part 'settings_dialog_bloc.freezed.dart'; + +class SettingsDialogBloc extends Bloc { + final UserListener _userListener; + final UserProfilePB userProfile; + + SettingsDialogBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + super(SettingsDialogState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + setViewIndex: (int viewIndex) { + emit(state.copyWith(viewIndex: viewIndex)); + }, + ); + }); + } + + @override + Future close() async { + await _userListener.stop(); + super.close(); + } + + void _profileUpdated(Either userProfileOrFailed) { + userProfileOrFailed.fold( + (newUserProfile) => add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + } +} + +@freezed +class SettingsDialogEvent with _$SettingsDialogEvent { + const factory SettingsDialogEvent.initial() = _Initial; + const factory SettingsDialogEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; + const factory SettingsDialogEvent.setViewIndex(int index) = _SetViewIndex; +} + +@freezed +class SettingsDialogState with _$SettingsDialogState { + const factory SettingsDialogState({ + required UserProfilePB userProfile, + required Either successOrFailure, + required int viewIndex, + }) = _SettingsDialogState; + + factory SettingsDialogState.initial(UserProfilePB userProfile) => SettingsDialogState( + userProfile: userProfile, + successOrFailure: left(unit), + viewIndex: 0, + ); +} diff --git a/frontend/app_flowy/lib/workspace/application/user/prelude.dart b/frontend/app_flowy/lib/workspace/application/user/prelude.dart new file mode 100644 index 0000000000..f698497db9 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/user/prelude.dart @@ -0,0 +1 @@ +export 'settings_user_bloc.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart new file mode 100644 index 0000000000..7435778471 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart @@ -0,0 +1,79 @@ +import 'package:app_flowy/user/application/user_listener.dart'; +import 'package:app_flowy/user/application/user_service.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +part 'settings_user_bloc.freezed.dart'; + +class SettingsUserViewBloc extends Bloc { + final UserService _userService; + final UserListener _userListener; + final UserProfilePB userProfile; + + SettingsUserViewBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + _userService = UserService(userId: userProfile.id), + super(SettingsUserState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + await _initUser(); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + updateUserName: (String name) { + _userService.updateUserProfile(name: name).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + ); + }); + } + + @override + Future close() async { + await _userListener.stop(); + super.close(); + } + + Future _initUser() async { + final result = await _userService.initUser(); + result.fold((l) => null, (error) => Log.error(error)); + } + + void _profileUpdated(Either userProfileOrFailed) { + userProfileOrFailed.fold( + (newUserProfile) => add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + } +} + +@freezed +class SettingsUserEvent with _$SettingsUserEvent { + const factory SettingsUserEvent.initial() = _Initial; + const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; + const factory SettingsUserEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; +} + +@freezed +class SettingsUserState with _$SettingsUserState { + const factory SettingsUserState({ + required UserProfilePB userProfile, + required Either successOrFailure, + }) = _SettingsUserState; + + factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( + userProfile: userProfile, + successOrFailure: left(unit), + ); +} diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index 6c9ffdece2..d7b8dce4af 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -67,6 +67,7 @@ class MenuUser extends StatelessWidget { Widget _renderSettingsButton(BuildContext context) { final theme = context.watch(); + final userProfile = context.read().state.userProfile; return Tooltip( message: LocaleKeys.settings_menu_open.tr(), child: IconButton( @@ -74,7 +75,7 @@ class MenuUser extends StatelessWidget { showDialog( context: context, builder: (context) { - return const SettingsDialog(); + return SettingsDialog(userProfile); }, ); }, diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart index 8c7bb8f494..eaf09d770f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,70 +1,75 @@ +import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu.dart'; +import 'package:app_flowy/workspace/application/settings/settings_dialog_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -class SettingsDialog extends StatefulWidget { - const SettingsDialog({Key? key}) : super(key: key); +class SettingsDialog extends StatelessWidget { + final UserProfilePB user; + SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id)); - @override - State createState() => _SettingsDialogState(); -} - -class _SettingsDialogState extends State { - int _selectedViewIndex = 0; - - final List settingsViews = const [ - SettingsAppearanceView(), - SettingsLanguageView(), - ]; + Widget getSettingsView(int index, UserProfilePB user) { + final List settingsViews = [ + const SettingsAppearanceView(), + const SettingsLanguageView(), + SettingsUserView(user), + ]; + return settingsViews[index]; + } @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - title: Text( - LocaleKeys.settings_title.tr(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - content: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 600, - minWidth: 600, - maxWidth: 1000, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: SettingsMenu( - changeSelectedIndex: (index) { - setState(() { - _selectedViewIndex = index; - }); - }, - currentIndex: _selectedViewIndex, - ), - ), - const VerticalDivider(), - const SizedBox(width: 10), - Expanded( - child: settingsViews[_selectedViewIndex], - ) - ], - ), - ), - ), - ); + return BlocProvider( + create: (context) => getIt(param1: user)..add(const SettingsDialogEvent.initial()), + child: BlocBuilder( + builder: (context, state) => ChangeNotifierProvider.value( + value: Provider.of(context, listen: true), + child: AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + title: Text( + LocaleKeys.settings_title.tr(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 600, + minWidth: 600, + maxWidth: 1000, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: SettingsMenu( + changeSelectedIndex: (index) { + context.read().add(SettingsDialogEvent.setViewIndex(index)); + }, + currentIndex: context.read().state.viewIndex, + ), + ), + const VerticalDivider(), + const SizedBox(width: 10), + Expanded( + child: getSettingsView(context.read().state.viewIndex, + context.read().state.userProfile), + ) + ], + ), + ), + ), + ))); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart index 241c337705..a27d9861c4 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -34,6 +34,16 @@ class SettingsMenu extends StatelessWidget { icon: Icons.translate, changeSelectedIndex: changeSelectedIndex, ), + const SizedBox( + height: 10, + ), + SettingsMenuElement( + index: 2, + currentIndex: currentIndex, + label: LocaleKeys.settings_menu_user.tr(), + icon: Icons.account_box_outlined, + changeSelectedIndex: changeSelectedIndex, + ), ], ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart new file mode 100644 index 0000000000..f8f094d1b0 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -0,0 +1,50 @@ +import 'package:app_flowy/startup/startup.dart'; +import 'package:flutter/material.dart'; +import 'package:app_flowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; + +class SettingsUserView extends StatelessWidget { + final UserProfilePB user; + SettingsUserView(this.user, {Key? key}) : super(key: ValueKey(user.id)); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(param1: user)..add(const SettingsUserEvent.initial()), + child: BlocBuilder( + builder: (context, state) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_renderUserNameInput(context)], + ), + ), + ), + ); + } + + Widget _renderUserNameInput(BuildContext context) { + String name = context.read().state.userProfile.name; + return _UserNameInput(name); + } +} + +class _UserNameInput extends StatelessWidget { + final String name; + const _UserNameInput( + this.name, { + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextField( + controller: TextEditingController()..text = name, + decoration: const InputDecoration( + labelText: 'Name', + ), + onSubmitted: (val) { + context.read().add(SettingsUserEvent.updateUserName(val)); + }); + } +}