Преглед изворни кода

fix: open latest view after launch

appflowy пре 3 година
родитељ
комит
c8945133dc

+ 5 - 2
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -14,6 +14,7 @@ import 'package:app_flowy/workspace/application/menu/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';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
@@ -50,6 +51,8 @@ void _resolveUserDeps(GetIt getIt) {
 }
 
 void _resolveHomeDeps(GetIt getIt) {
+  getIt.registerSingleton(MenuSharedState());
+
   getIt.registerFactoryParam<UserListener, UserProfile, void>(
     (user, _) => UserListener(user: user),
   );
@@ -113,8 +116,8 @@ void _resolveFolderDeps(GetIt getIt) {
   getIt.registerFactoryParam<AppBloc, App, void>(
     (app, _) => AppBloc(
       app: app,
-      service: AppService(),
-      listener: AppListener(appId: app.id),
+      appService: AppService(),
+      appListener: AppListener(appId: app.id),
     ),
   );
 

+ 114 - 53
frontend/app_flowy/lib/workspace/application/app/app_bloc.dart

@@ -1,10 +1,14 @@
 import 'package:app_flowy/plugin/plugin.dart';
+import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/app/app_listener.dart';
 import 'package:app_flowy/workspace/application/app/app_service.dart';
+import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
+import 'package:expandable/expandable.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:dartz/dartz.dart';
@@ -13,71 +17,83 @@ part 'app_bloc.freezed.dart';
 
 class AppBloc extends Bloc<AppEvent, AppState> {
   final App app;
-  final AppService service;
-  final AppListener listener;
+  final AppService appService;
+  final AppListener appListener;
 
-  AppBloc({required this.app, required this.service, required this.listener}) : super(AppState.initial(app)) {
+  AppBloc({required this.app, required this.appService, required this.appListener}) : super(AppState.initial(app)) {
     on<AppEvent>((event, emit) async {
       await event.map(initial: (e) async {
-        listener.start(
-          viewsChanged: _handleViewsChanged,
-          appUpdated: (app) => add(AppEvent.appDidUpdate(app)),
-        );
-        await _fetchViews(emit);
+        _startListening();
+        await _loadViews(emit);
       }, createView: (CreateView value) async {
-        final viewOrFailed = await service.createView(
-          appId: app.id,
-          name: value.name,
-          desc: value.desc,
-          dataType: value.dataType,
-          pluginType: value.pluginType,
-        );
-        viewOrFailed.fold(
-          (view) => emit(state.copyWith(
-            latestCreatedView: view,
-            successOrFailure: left(unit),
-          )),
-          (error) {
-            Log.error(error);
-            emit(state.copyWith(successOrFailure: right(error)));
-          },
-        );
-      }, didReceiveViews: (e) async {
-        await handleDidReceiveViews(e.views, emit);
+        await _createView(value, emit);
+      }, didReceiveViewUpdated: (e) async {
+        await _didReceiveViewUpdated(e.views, emit);
       }, delete: (e) async {
-        final result = await service.delete(appId: app.id);
-        result.fold(
-          (unit) => emit(state.copyWith(successOrFailure: left(unit))),
-          (error) => emit(state.copyWith(successOrFailure: right(error))),
-        );
+        await _deleteView(emit);
       }, rename: (e) async {
-        final result = await service.updateApp(appId: app.id, name: e.newName);
-        result.fold(
-          (l) => emit(state.copyWith(successOrFailure: left(unit))),
-          (error) => emit(state.copyWith(successOrFailure: right(error))),
-        );
+        await _renameView(e, emit);
       }, appDidUpdate: (e) async {
         emit(state.copyWith(app: e.app));
       });
     });
   }
 
-  @override
-  Future<void> close() async {
-    await listener.close();
-    return super.close();
+  void _startListening() {
+    appListener.start(
+      viewsChanged: (result) {
+        result.fold(
+          (views) => add(AppEvent.didReceiveViewUpdated(views)),
+          (error) => Log.error(error),
+        );
+      },
+      appUpdated: (app) => add(AppEvent.appDidUpdate(app)),
+    );
+  }
+
+  Future<void> _renameView(Rename e, Emitter<AppState> emit) async {
+    final result = await appService.updateApp(appId: app.id, name: e.newName);
+    result.fold(
+      (l) => emit(state.copyWith(successOrFailure: left(unit))),
+      (error) => emit(state.copyWith(successOrFailure: right(error))),
+    );
   }
 
-  void _handleViewsChanged(Either<List<View>, FlowyError> result) {
+  Future<void> _deleteView(Emitter<AppState> emit) async {
+    final result = await appService.delete(appId: app.id);
     result.fold(
-      (views) => add(AppEvent.didReceiveViews(views)),
+      (unit) => emit(state.copyWith(successOrFailure: left(unit))),
+      (error) => emit(state.copyWith(successOrFailure: right(error))),
+    );
+  }
+
+  Future<void> _createView(CreateView value, Emitter<AppState> emit) async {
+    final viewOrFailed = await appService.createView(
+      appId: app.id,
+      name: value.name,
+      desc: value.desc,
+      dataType: value.dataType,
+      pluginType: value.pluginType,
+    );
+    viewOrFailed.fold(
+      (view) => emit(state.copyWith(
+        latestCreatedView: view,
+        successOrFailure: left(unit),
+      )),
       (error) {
         Log.error(error);
+        emit(state.copyWith(successOrFailure: right(error)));
       },
     );
   }
 
-  Future<void> handleDidReceiveViews(List<View> views, Emitter<AppState> emit) async {
+  @override
+  Future<void> close() async {
+    await appListener.close();
+    return super.close();
+  }
+
+  Future<void> _didReceiveViewUpdated(List<View> views, Emitter<AppState> emit) async {
     final latestCreatedView = state.latestCreatedView;
     AppState newState = state.copyWith(views: views);
     if (latestCreatedView != null) {
@@ -90,10 +106,10 @@ class AppBloc extends Bloc<AppEvent, AppState> {
     emit(newState);
   }
 
-  Future<void> _fetchViews(Emitter<AppState> emit) async {
-    final viewsOrFailed = await service.getViews(appId: app.id);
+  Future<void> _loadViews(Emitter<AppState> emit) async {
+    final viewsOrFailed = await appService.getViews(appId: app.id);
     viewsOrFailed.fold(
-      (apps) => emit(state.copyWith(views: apps)),
+      (views) => emit(state.copyWith(views: views)),
       (error) {
         Log.error(error);
         emit(state.copyWith(successOrFailure: right(error)));
@@ -113,7 +129,7 @@ class AppEvent with _$AppEvent {
   ) = CreateView;
   const factory AppEvent.delete() = Delete;
   const factory AppEvent.rename(String newName) = Rename;
-  const factory AppEvent.didReceiveViews(List<View> views) = ReceiveViews;
+  const factory AppEvent.didReceiveViewUpdated(List<View> views) = ReceiveViews;
   const factory AppEvent.appDidUpdate(App app) = AppDidUpdate;
 }
 
@@ -121,17 +137,62 @@ class AppEvent with _$AppEvent {
 class AppState with _$AppState {
   const factory AppState({
     required App app,
-    required bool isLoading,
-    required List<View>? views,
+    required List<View> views,
     View? latestCreatedView,
     required Either<Unit, FlowyError> successOrFailure,
   }) = _AppState;
 
   factory AppState.initial(App app) => AppState(
         app: app,
-        isLoading: false,
-        views: null,
-        latestCreatedView: null,
+        views: [],
         successOrFailure: left(unit),
       );
 }
+
+class AppViewDataNotifier extends ChangeNotifier {
+  List<View> _views = [];
+  View? _selectedView;
+  ExpandableController expandController = ExpandableController(initialExpanded: false);
+
+  AppViewDataNotifier() {
+    _setLatestView(getIt<MenuSharedState>().latestOpenView);
+    getIt<MenuSharedState>().addLatestViewListener((view) {
+      _setLatestView(view);
+    });
+  }
+
+  void _setLatestView(View? view) {
+    view?.freeze();
+    _selectedView = view;
+    _expandIfNeed();
+  }
+
+  View? get selectedView => _selectedView;
+
+  set views(List<View> views) {
+    if (_views != views) {
+      _views = views;
+      _expandIfNeed();
+      notifyListeners();
+    }
+  }
+
+  void _expandIfNeed() {
+    if (_selectedView == null) {
+      return;
+    }
+
+    if (!_views.contains(_selectedView!)) {
+      return;
+    }
+
+    if (expandController.expanded == false) {
+      // Workaround: Delay 150 milliseconds to make the smooth animation while expanding
+      Future.delayed(const Duration(milliseconds: 150), () {
+        expandController.expanded = true;
+      });
+    }
+  }
+
+  UnmodifiableListView<View> get views => UnmodifiableListView(_views);
+}

+ 3 - 0
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -121,6 +121,9 @@ class _HomeScreenState extends State<HomeScreen> {
       collapsedNotifier: getIt<HomeStackManager>().collapsedNotifier,
     );
 
+    final latestView = widget.workspaceSetting.hasLatestView() ? widget.workspaceSetting.latestView : null;
+    getIt<MenuSharedState>().latestOpenView = latestView;
+
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
   }
 

+ 26 - 64
frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart

@@ -2,7 +2,6 @@ import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
 import 'package:expandable/expandable.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:app_flowy/startup/startup.dart';
@@ -19,11 +18,11 @@ class MenuApp extends StatefulWidget {
 }
 
 class _MenuAppState extends State<MenuApp> {
-  late AppDataNotifier notifier;
+  late AppViewDataNotifier notifier;
 
   @override
   void initState() {
-    notifier = AppDataNotifier();
+    notifier = AppViewDataNotifier();
     super.initState();
   }
 
@@ -39,30 +38,34 @@ class _MenuAppState extends State<MenuApp> {
           },
         ),
       ],
-      child: BlocSelector<AppBloc, AppState, AppDataNotifier>(
-        selector: (state) {
-          final menuSharedState = Provider.of<MenuSharedState>(context, listen: false);
-          if (state.latestCreatedView != null) {
-            menuSharedState.forcedOpenView.value = state.latestCreatedView!;
-          }
-
-          notifier.views = state.views;
-          notifier.selectedView = menuSharedState.selectedView.value;
-          return notifier;
-        },
-        builder: (context, notifier) => ChangeNotifierProvider.value(
-          value: notifier,
-          child: Consumer(
-            builder: (BuildContext context, AppDataNotifier notifier, Widget? child) {
-              return expandableWrapper(context, notifier);
-            },
+      child: MultiBlocListener(
+        listeners: [
+          BlocListener<AppBloc, AppState>(
+            listenWhen: (p, c) => p.latestCreatedView != c.latestCreatedView,
+            listener: (context, state) => getIt<MenuSharedState>().latestOpenView = state.latestCreatedView,
           ),
+          BlocListener<AppBloc, AppState>(
+            listenWhen: (p, c) => p.views != c.views,
+            listener: (context, state) => notifier.views = state.views,
+          ),
+        ],
+        child: BlocBuilder<AppBloc, AppState>(
+          builder: (context, state) {
+            return ChangeNotifierProvider.value(
+              value: notifier,
+              child: Consumer<AppViewDataNotifier>(
+                builder: (context, notifier, _) {
+                  return expandableWrapper(context, notifier);
+                },
+              ),
+            );
+          },
         ),
       ),
     );
   }
 
-  ExpandableNotifier expandableWrapper(BuildContext context, AppDataNotifier notifier) {
+  ExpandableNotifier expandableWrapper(BuildContext context, AppViewDataNotifier notifier) {
     return ExpandableNotifier(
       controller: notifier.expandController,
       child: ScrollOnExpand(
@@ -92,11 +95,11 @@ class _MenuAppState extends State<MenuApp> {
     );
   }
 
-  Widget _renderViewSection(AppDataNotifier notifier) {
+  Widget _renderViewSection(AppViewDataNotifier notifier) {
     return MultiProvider(
       providers: [ChangeNotifierProvider.value(value: notifier)],
       child: Consumer(
-        builder: (context, AppDataNotifier notifier, child) {
+        builder: (context, AppViewDataNotifier notifier, child) {
           return ViewSection(appData: notifier);
         },
       ),
@@ -119,44 +122,3 @@ class MenuAppSizes {
   static double scale = 1;
   static double get expandedPadding => iconSize * scale + headerPadding;
 }
-
-class AppDataNotifier extends ChangeNotifier {
-  List<View> _views = [];
-  View? _selectedView;
-  ExpandableController expandController = ExpandableController(initialExpanded: false);
-
-  AppDataNotifier();
-
-  set selectedView(View? view) {
-    _selectedView = view;
-
-    if (view != null && _views.isNotEmpty) {
-      final isExpanded = _views.contains(view);
-      if (expandController.expanded == false && expandController.expanded != isExpanded) {
-        // Workaround: Delay 150 milliseconds to make the smooth animation while expanding
-        Future.delayed(const Duration(milliseconds: 150), () {
-          expandController.expanded = isExpanded;
-        });
-      }
-    }
-  }
-
-  View? get selectedView => _selectedView;
-
-  set views(List<View>? views) {
-    if (views == null) {
-      if (_views.isNotEmpty) {
-        _views = List.empty(growable: false);
-        notifyListeners();
-      }
-      return;
-    }
-
-    if (_views != views) {
-      _views = views;
-      notifyListeners();
-    }
-  }
-
-  List<View> get views => _views;
-}

+ 19 - 22
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart

@@ -2,6 +2,7 @@ import 'dart:async';
 import 'dart:developer';
 
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/app/app_bloc.dart';
 import 'package:app_flowy/workspace/application/view/view_ext.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
@@ -13,13 +14,13 @@ import 'package:styled_widget/styled_widget.dart';
 import 'item.dart';
 
 class ViewSection extends StatelessWidget {
-  final AppDataNotifier appData;
+  final AppViewDataNotifier appData;
   const ViewSection({Key? key, required this.appData}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     // The ViewSectionNotifier will be updated after AppDataNotifier changed passed by parent widget
-    return ChangeNotifierProxyProvider<AppDataNotifier, ViewSectionNotifier>(
+    return ChangeNotifierProxyProvider<AppViewDataNotifier, ViewSectionNotifier>(
       create: (_) {
         return ViewSectionNotifier(
           context: context,
@@ -29,7 +30,7 @@ class ViewSection extends StatelessWidget {
       },
       update: (_, notifier, controller) => controller!..update(notifier),
       child: Consumer(builder: (context, ViewSectionNotifier notifier, child) {
-        return RenderSectionItems(views: notifier.views);
+        return _SectionItems(views: notifier.views);
       }),
     );
   }
@@ -63,16 +64,16 @@ class ViewSection extends StatelessWidget {
   // }
 }
 
-class RenderSectionItems extends StatefulWidget {
-  const RenderSectionItems({Key? key, required this.views}) : super(key: key);
+class _SectionItems extends StatefulWidget {
+  const _SectionItems({Key? key, required this.views}) : super(key: key);
 
   final List<View> views;
 
   @override
-  State<RenderSectionItems> createState() => _RenderSectionItemsState();
+  State<_SectionItems> createState() => _SectionItemsState();
 }
 
-class _RenderSectionItemsState extends State<RenderSectionItems> {
+class _SectionItemsState extends State<_SectionItems> {
   List<View> views = <View>[];
 
   /// Maps the hasmap value of the section items to their index in the reorderable list.
@@ -123,10 +124,7 @@ class _RenderSectionItemsState extends State<RenderSectionItems> {
                   (view) => ViewSectionItem(
                     view: view,
                     isSelected: _isViewSelected(context, view.id),
-                    onSelected: (view) {
-                      context.read<ViewSectionNotifier>().selectedView = view;
-                      Provider.of<MenuSharedState>(context, listen: false).selectedView.value = view;
-                    },
+                    onSelected: (view) => getIt<MenuSharedState>().latestOpenView = view,
                   ).padding(vertical: 4),
                 )
                 .toList()[index],
@@ -150,6 +148,7 @@ class ViewSectionNotifier with ChangeNotifier {
   List<View> _views;
   View? _selectedView;
   Timer? _notifyListenerOperation;
+  VoidCallback? _latestViewDidChangeFn;
 
   ViewSectionNotifier({
     required BuildContext context,
@@ -157,16 +156,10 @@ class ViewSectionNotifier with ChangeNotifier {
     View? initialSelectedView,
   })  : _views = views,
         _selectedView = initialSelectedView {
-    final menuSharedState = Provider.of<MenuSharedState>(context, listen: false);
-    // The forcedOpenView will be the view after creating the new view
-    menuSharedState.forcedOpenView.addPublishListener((forcedOpenView) {
-      selectedView = forcedOpenView;
-    });
-
-    menuSharedState.selectedView.addListener(() {
-      // Cancel the selected view of this section by setting the selectedView to null
-      // that will notify the listener to refresh the ViewSection UI
-      if (menuSharedState.selectedView.value != _selectedView) {
+    _latestViewDidChangeFn = getIt<MenuSharedState>().addLatestViewListener((latestOpenView) {
+      if (_views.contains(latestOpenView)) {
+        selectedView = latestOpenView;
+      } else {
         selectedView = null;
       }
     });
@@ -199,7 +192,7 @@ class ViewSectionNotifier with ChangeNotifier {
 
   View? get selectedView => _selectedView;
 
-  void update(AppDataNotifier notifier) {
+  void update(AppViewDataNotifier notifier) {
     views = notifier.views;
   }
 
@@ -216,6 +209,10 @@ class ViewSectionNotifier with ChangeNotifier {
   void dispose() {
     isDisposed = true;
     _notifyListenerOperation?.cancel();
+    if (_latestViewDidChangeFn != null) {
+      getIt<MenuSharedState>().removeLatestViewListener(_latestViewDidChangeFn!);
+      _latestViewDidChangeFn = null;
+    }
     super.dispose();
   }
 }

+ 39 - 31
frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart

@@ -88,30 +88,24 @@ class _HomeMenuState extends State<HomeMenu> {
     final theme = context.watch<AppTheme>();
     return Container(
       color: theme.bg1,
-      child: ChangeNotifierProvider(
-        create: (_) =>
-            MenuSharedState(view: widget.workspaceSetting.hasLatestView() ? widget.workspaceSetting.latestView : null),
-        child: Consumer(builder: (context, MenuSharedState sharedState, child) {
-          return Column(
-            mainAxisAlignment: MainAxisAlignment.start,
-            children: [
-              Expanded(
-                child: Column(
-                  mainAxisAlignment: MainAxisAlignment.start,
-                  children: [
-                    const MenuTopBar(),
-                    const VSpace(10),
-                    _renderApps(context),
-                  ],
-                ).padding(horizontal: Insets.l),
-              ),
-              const VSpace(20),
-              _renderTrash(context).padding(horizontal: Insets.l),
-              const VSpace(20),
-              _renderNewAppButton(context),
-            ],
-          );
-        }),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.start,
+        children: [
+          Expanded(
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.start,
+              children: [
+                const MenuTopBar(),
+                const VSpace(10),
+                _renderApps(context),
+              ],
+            ).padding(horizontal: Insets.l),
+          ),
+          const VSpace(20),
+          _renderTrash(context).padding(horizontal: Insets.l),
+          const VSpace(20),
+          _renderNewAppButton(context),
+        ],
       ),
     );
   }
@@ -201,18 +195,32 @@ class _HomeMenuState extends State<HomeMenu> {
   }
 }
 
-class MenuSharedState extends ChangeNotifier {
-  PublishNotifier<View> forcedOpenView = PublishNotifier();
-  ValueNotifier<View?> selectedView = ValueNotifier<View?>(null);
+class MenuSharedState {
+  final ValueNotifier<View?> _latestOpenView = ValueNotifier<View?>(null);
 
   MenuSharedState({View? view}) {
     if (view != null) {
-      selectedView.value = view;
+      _latestOpenView.value = view;
+    }
+  }
+
+  View? get latestOpenView => _latestOpenView.value;
+
+  set latestOpenView(View? view) {
+    _latestOpenView.value = view;
+  }
+
+  VoidCallback addLatestViewListener(void Function(View?) latestViewDidChange) {
+    onChanged() {
+      latestViewDidChange(_latestOpenView.value);
     }
 
-    forcedOpenView.addPublishListener((view) {
-      selectedView.value = view;
-    });
+    _latestOpenView.addListener(onChanged);
+    return onChanged;
+  }
+
+  void removeLatestViewListener(VoidCallback fn) {
+    _latestOpenView.removeListener(fn);
   }
 }
 

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/trash/menu.dart

@@ -21,7 +21,7 @@ class MenuTrash extends StatelessWidget {
       height: 26,
       child: InkWell(
         onTap: () {
-          Provider.of<MenuSharedState>(context, listen: false).selectedView.value = null;
+          getIt<MenuSharedState>().latestOpenView = null;
           getIt<HomeStackManager>().setPlugin(makePlugin(pluginType: DefaultPlugin.trash.type()));
         },
         child: _render(context),