diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart index 080211a8b7..59324fa0a7 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/section.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/view/view_ext.dart'; @@ -7,6 +8,7 @@ import 'package:app_flowy/workspace/presentation/home/menu/menu.dart'; import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:reorderables/reorderables.dart'; import 'package:styled_widget/styled_widget.dart'; import 'item.dart'; @@ -27,29 +29,111 @@ class ViewSection extends StatelessWidget { }, update: (_, notifier, controller) => controller!..update(notifier), child: Consumer(builder: (context, ViewSectionNotifier notifier, child) { - return _renderSectionItems(context, notifier.views); + return RenderSectionItems(views: notifier.views); }), ); } - Widget _renderSectionItems(BuildContext context, List views) { - List viewWidgets = []; - if (views.isNotEmpty) { - viewWidgets = views - .map( - (view) => ViewSectionItem( - view: view, - isSelected: _isViewSelected(context, view.id), - onSelected: (view) { - context.read().selectedView = view; - Provider.of(context, listen: false).selectedView.value = view; - }, - ).padding(vertical: 4), - ) - .toList(growable: false); + // Widget _renderSectionItems(BuildContext context, List views) { + // List viewWidgets = []; + // if (views.isNotEmpty) { + // viewWidgets = views + // .map( + // (view) => ViewSectionItem( + // view: view, + // isSelected: _isViewSelected(context, view.id), + // onSelected: (view) { + // context.read().selectedView = view; + // Provider.of(context, listen: false).selectedView.value = view; + // }, + // ).padding(vertical: 4), + // ) + // .toList(growable: false); + // } + + // return Column(children: viewWidgets); + // } + + // bool _isViewSelected(BuildContext context, String viewId) { + // final view = context.read().selectedView; + // if (view == null) { + // return false; + // } + // return view.id == viewId; + // } +} + +class RenderSectionItems extends StatefulWidget { + const RenderSectionItems({Key? key, required this.views}) : super(key: key); + + final List views; + + @override + State createState() => _RenderSectionItemsState(); +} + +class _RenderSectionItemsState extends State { + List views = []; + + /// Maps the hasmap value of the section items to their index in the reorderable list. + //TODO @gaganyadav80: Retain this map to persist the order of the items. + final Map _sectionItemIndex = {}; + + void _initItemList() { + views.addAll(widget.views); + + for (int i = 0; i < views.length; i++) { + if (_sectionItemIndex[views[i].id] == null) { + _sectionItemIndex[views[i].id] = i; + } + } + } + + @override + void initState() { + super.initState(); + _initItemList(); + } + + @override + Widget build(BuildContext context) { + if (views.isEmpty) { + _initItemList(); } - return Column(children: viewWidgets); + log("BUILD: Section items: ${views.length}"); + return ReorderableColumn( + needsLongPressDraggable: false, + onReorder: (oldIndex, index) { + setState(() { + // int index = newIndex > oldIndex ? newIndex - 1 : newIndex; + View section = views.removeAt(oldIndex); + views.insert(index, section); + + _sectionItemIndex[section.id] = index; + }); + }, + children: List.generate( + views.length, + (index) { + return Container( + key: ValueKey(views[index].id), + child: views + .map( + (view) => ViewSectionItem( + view: view, + isSelected: _isViewSelected(context, view.id), + onSelected: (view) { + context.read().selectedView = view; + Provider.of(context, listen: false).selectedView.value = view; + }, + ).padding(vertical: 4), + ) + .toList()[index], + ); + }, + ), + ); } bool _isViewSelected(BuildContext context, String viewId) { diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart index 395a85bf45..17d7326f0a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -27,7 +27,7 @@ import 'app/menu_app.dart'; import 'app/create_button.dart'; import 'menu_user.dart'; -class HomeMenu extends StatelessWidget { +class HomeMenu extends StatefulWidget { final PublishNotifier _collapsedNotifier; final UserProfile user; final CurrentWorkspaceSetting workspaceSetting; @@ -40,13 +40,22 @@ class HomeMenu extends StatelessWidget { }) : _collapsedNotifier = collapsedNotifier, super(key: key); + @override + State createState() => _HomeMenuState(); +} + +class _HomeMenuState extends State { + /// Maps the hashmap of the menu items to their index in reorderable list view. + //TODO @gaganyadav80: Retain this map to persist on app restarts. + final Map _menuItemIndex = {}; + @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( create: (context) { - final menuBloc = getIt(param1: user, param2: workspaceSetting.workspace.id); + final menuBloc = getIt(param1: widget.user, param2: widget.workspaceSetting.workspace.id); menuBloc.add(const MenuEvent.initial()); return menuBloc; }, @@ -63,7 +72,7 @@ class HomeMenu extends StatelessWidget { BlocListener( listenWhen: (p, c) => p.isCollapse != c.isCollapse, listener: (context, state) { - _collapsedNotifier.value = state.isCollapse; + widget._collapsedNotifier.value = state.isCollapse; }, ) ], @@ -80,7 +89,8 @@ class HomeMenu extends StatelessWidget { return Container( color: theme.bg1, child: ChangeNotifierProvider( - create: (_) => MenuSharedState(view: workspaceSetting.hasLatestView() ? workspaceSetting.latestView : null), + create: (_) => + MenuSharedState(view: widget.workspaceSetting.hasLatestView() ? widget.workspaceSetting.latestView : null), child: Consumer(builder: (context, MenuSharedState sharedState, child) { return Column( mainAxisAlignment: MainAxisAlignment.start, @@ -115,26 +125,65 @@ class HomeMenu extends StatelessWidget { child: BlocSelector>( selector: (state) { List menuItems = []; - menuItems.add(MenuUser(user)); + // menuItems.add(MenuUser(user)); List appWidgets = state.apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(app)).toList()); - menuItems.addAll(appWidgets); + // menuItems.addAll(appWidgets); + for (int i = 0; i < appWidgets.length; i++) { + if (_menuItemIndex[appWidgets[i].key.hashCode] == null) { + _menuItemIndex[appWidgets[i].key.hashCode] = i; + } + + menuItems.insert(_menuItemIndex[appWidgets[i].key.hashCode]!, appWidgets[i]); + } + return menuItems; }, - builder: (context, menuItems) => ListView.separated( - itemCount: menuItems.length, - separatorBuilder: (context, index) { - if (index == 0) { - return const VSpace(20); - } else { - return VSpace(MenuAppSizes.appVPadding); - } - }, - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return menuItems[index]; - }, - ), + builder: (context, menuItems) { + return ReorderableListView.builder( + itemCount: menuItems.length, + buildDefaultDragHandles: false, + header: Padding( + padding: EdgeInsets.only(bottom: 20.0 - MenuAppSizes.appVPadding), + child: MenuUser(widget.user), + ), + onReorder: (oldIndex, newIndex) { + int index = newIndex > oldIndex ? newIndex - 1 : newIndex; + + Widget menu = menuItems.removeAt(oldIndex); + menuItems.insert(index, menu); + + final menuBloc = context.read(); + menuBloc.state.apps.forEach((a) { + var app = a.removeAt(oldIndex); + a.insert(index, app); + }); + + _menuItemIndex[menu.key.hashCode] = index; + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + //? @gaganyadav80: To mimic the ListView.separated behavior, we need to add a padding. + // EdgeInsets padding = EdgeInsets.zero; + // if (index == 0) { + // padding = EdgeInsets.only(bottom: MenuAppSizes.appVPadding / 2); + // } else if (index == menuItems.length - 1) { + // padding = EdgeInsets.only(top: MenuAppSizes.appVPadding / 2); + // } else { + // padding = EdgeInsets.symmetric(vertical: MenuAppSizes.appVPadding / 2); + // } + + return ReorderableDragStartListener( + key: ValueKey(menuItems[index].hashCode), + index: index, + child: Padding( + padding: EdgeInsets.symmetric(vertical: MenuAppSizes.appVPadding / 2), + child: menuItems[index], + ), + ); + }, + ); + }, ), ), ),