From 628e53adc1c6f28a87db6141a531ab322c70614f Mon Sep 17 00:00:00 2001 From: gaganyadav80 Date: Sun, 6 Mar 2022 01:53:15 +0530 Subject: [PATCH 1/3] feat: adds drag-drop support to sidebar pages 1. On mobile it uses ReorderableDelayedDragStartListener (like long press) 2. On desktop it uses ReorderableDragStartListener (instantly) --- .../presentation/home/menu/menu.dart | 95 ++++++++++++++----- 1 file changed, 73 insertions(+), 22 deletions(-) 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 eeebf2b145..a3ca1a0876 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,20 @@ class HomeMenu extends StatelessWidget { }) : _collapsedNotifier = collapsedNotifier, super(key: key); + @override + State createState() => _HomeMenuState(); +} + +class _HomeMenuState extends State { + final List _menuItems = List.empty(growable: true); + @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 +70,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 +87,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, @@ -114,27 +122,70 @@ class HomeMenu extends StatelessWidget { behavior: const ScrollBehavior().copyWith(scrollbars: false), child: BlocSelector>( selector: (state) { - List menuItems = []; - menuItems.add(MenuUser(user)); + // List menuItems = []; + // menuItems.add(MenuUser(user)); List appWidgets = state.apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(app)).toList()); - menuItems.addAll(appWidgets); - 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); + + for (var app in appWidgets) { + if (!_menuItems.any((oldElement) => oldElement.key == app.key)) { + _menuItems.add(app); } - }, - physics: StyledScrollPhysics(), - itemBuilder: (BuildContext context, int index) { - return menuItems[index]; - }, - ), + } + // TODO @gaganyadav: fix: concurrent modification exception + // Unhandled Exception: Concurrent modification during iteration: Instance(length:3) of '_GrowableList'. + for (var item in _menuItems) { + if (!appWidgets.any((oldElement) => oldElement.key == item.key)) { + _menuItems.remove(item); + } + } + + // menuItems.addAll(appWidgets); + return _menuItems; + }, + 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); + }); + }, + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + //? 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], + ), + ); + }, + ); + }, ), ), ), From 8ab2fe3e30e93162a8bf21f62fcf74e197df5fd5 Mon Sep 17 00:00:00 2001 From: gaganyadav80 Date: Thu, 24 Mar 2022 15:36:57 +0530 Subject: [PATCH 2/3] feat: adds drag-drop to docs section --- .../home/menu/app/section/section.dart | 135 +++++++++++++++--- .../presentation/home/menu/menu.dart | 48 ++++--- frontend/app_flowy/pubspec.yaml | 1 + 3 files changed, 147 insertions(+), 37 deletions(-) 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 dcb2d454b9..c0d82930b1 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,3 +1,5 @@ +import 'dart:developer'; + import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/view/view_ext.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; @@ -5,14 +7,22 @@ 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'; import 'package:async/async.dart'; -class ViewSection extends StatelessWidget { +//? @gaganyadav80: Build 3 times on startup. Then one time on each doc change. + +class ViewSection extends StatefulWidget { final AppDataNotifier appData; const ViewSection({Key? key, required this.appData}) : super(key: key); + @override + State createState() => _ViewSectionState(); +} + +class _ViewSectionState extends State { @override Widget build(BuildContext context) { // The ViewSectionNotifier will be updated after AppDataNotifier changed passed by parent widget @@ -20,35 +30,120 @@ class ViewSection extends StatelessWidget { create: (_) { return ViewSectionNotifier( context: context, - views: appData.views, - initialSelectedView: appData.selectedView, + views: widget.appData.views, + initialSelectedView: widget.appData.selectedView, ); }, update: (_, notifier, controller) => controller!..update(notifier), child: Consumer(builder: (context, ViewSectionNotifier notifier, child) { - return _renderSectionItems(context, notifier.views); + log("BUILD: Section Bloc section update triggered + ${notifier.views.length}"); + 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) { + // return Container(); + // } +} + +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 viewWidgets = List.empty(growable: true); + // List sectionItems = List.empty(growable: true); + 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); + // log(widget.views.length.toString()); + // if (widget.views.isNotEmpty) { + // viewWidgets = widget.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); + // } + + // sectionItems.clear(); + + for (int i = 0; i < views.length; i++) { + if (_sectionItemIndex[views[i].id] == null) { + _sectionItemIndex[views[i].id] = i; + } + + // sectionItems.insert(_sectionItemIndex[viewWidgets[i].key.hashCode]!, viewWidgets[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( + // itemCount: sectionItems.length, + // buildDefaultDragHandles: false, + 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; + }); + }, + // physics: StyledScrollPhysics(), + // itemBuilder: (context, index) {}, + children: List.generate( + views.length, + (index) { + return Container( + key: ValueKey(views[index].id), + // index: index, + 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 a3ca1a0876..99f3d22946 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -1,6 +1,8 @@ export './app/header/header.dart'; export './app/menu_app.dart'; +import 'dart:developer'; + import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart'; import 'package:flowy_infra/notifier.dart'; @@ -45,7 +47,10 @@ class HomeMenu extends StatefulWidget { } class _HomeMenuState extends State { - final List _menuItems = List.empty(growable: true); + // final List _menuItems = List.empty(growable: true); + /// Maps the hashmap of the menu items to their index in reorderable list view. + //TODO @gaganyadav80: need to retain this to persist on app restarts. + final Map _menuItemIndex = {}; @override Widget build(BuildContext context) { @@ -122,26 +127,33 @@ class _HomeMenuState extends State { behavior: const ScrollBehavior().copyWith(scrollbars: false), child: BlocSelector>( selector: (state) { - // List menuItems = []; + List menuItems = []; // menuItems.add(MenuUser(user)); List appWidgets = state.apps.foldRight([], (apps, _) => apps.map((app) => MenuApp(app)).toList()); - - for (var app in appWidgets) { - if (!_menuItems.any((oldElement) => oldElement.key == app.key)) { - _menuItems.add(app); - } - } - // TODO @gaganyadav: fix: concurrent modification exception - // Unhandled Exception: Concurrent modification during iteration: Instance(length:3) of '_GrowableList'. - for (var item in _menuItems) { - if (!appWidgets.any((oldElement) => oldElement.key == item.key)) { - _menuItems.remove(item); - } - } - // menuItems.addAll(appWidgets); - return _menuItems; + 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]); + } + + // for (var app in appWidgets) { + // if (!_menuItems.any((oldElement) => oldElement.key == app.key)) { + // _menuItems.add(app); + // } + // } + // // TODO @gaganyadav80: fix: concurrent modification exception + // // Unhandled Exception: Concurrent modification during iteration: Instance(length:3) of '_GrowableList'. + // for (var item in _menuItems) { + // if (!appWidgets.any((oldElement) => oldElement.key == item.key)) { + // _menuItems.remove(item); + // } + // } + + return menuItems; }, builder: (context, menuItems) { return ReorderableListView.builder( @@ -162,6 +174,8 @@ class _HomeMenuState extends State { var app = a.removeAt(oldIndex); a.insert(index, app); }); + + _menuItemIndex[menu.key.hashCode] = index; }, physics: StyledScrollPhysics(), itemBuilder: (BuildContext context, int index) { diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 7f642f51b3..67b4a8aaef 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -73,6 +73,7 @@ dependencies: cupertino_icons: ^1.0.2 device_info_plus: ^3.2.1 fluttertoast: ^8.0.8 + reorderables: ^0.4.3 dev_dependencies: flutter_lints: ^1.0.0 From 933689269bb5a78c17914c75568883644f415451 Mon Sep 17 00:00:00 2001 From: gaganyadav80 Date: Thu, 24 Mar 2022 15:49:53 +0530 Subject: [PATCH 3/3] refactor: remove unused code --- .../home/menu/app/section/section.dart | 66 ++++++++----------- .../presentation/home/menu/menu.dart | 20 +----- 2 files changed, 30 insertions(+), 56 deletions(-) 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 c0d82930b1..79f595a428 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 @@ -12,17 +12,10 @@ import 'package:styled_widget/styled_widget.dart'; import 'item.dart'; import 'package:async/async.dart'; -//? @gaganyadav80: Build 3 times on startup. Then one time on each doc change. - -class ViewSection extends StatefulWidget { +class ViewSection extends StatelessWidget { final AppDataNotifier appData; const ViewSection({Key? key, required this.appData}) : super(key: key); - @override - State createState() => _ViewSectionState(); -} - -class _ViewSectionState extends State { @override Widget build(BuildContext context) { // The ViewSectionNotifier will be updated after AppDataNotifier changed passed by parent widget @@ -30,20 +23,43 @@ class _ViewSectionState extends State { create: (_) { return ViewSectionNotifier( context: context, - views: widget.appData.views, - initialSelectedView: widget.appData.selectedView, + views: appData.views, + initialSelectedView: appData.selectedView, ); }, update: (_, notifier, controller) => controller!..update(notifier), child: Consumer(builder: (context, ViewSectionNotifier notifier, child) { - log("BUILD: Section Bloc section update triggered + ${notifier.views.length}"); return RenderSectionItems(views: notifier.views); }), ); } // Widget _renderSectionItems(BuildContext context, List views) { - // return Container(); + // 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; // } } @@ -57,8 +73,6 @@ class RenderSectionItems extends StatefulWidget { } class _RenderSectionItemsState extends State { - // List viewWidgets = List.empty(growable: true); - // List sectionItems = List.empty(growable: true); List views = []; /// Maps the hasmap value of the section items to their index in the reorderable list. @@ -67,30 +81,11 @@ class _RenderSectionItemsState extends State { void _initItemList() { views.addAll(widget.views); - // log(widget.views.length.toString()); - // if (widget.views.isNotEmpty) { - // viewWidgets = widget.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); - // } - - // sectionItems.clear(); for (int i = 0; i < views.length; i++) { if (_sectionItemIndex[views[i].id] == null) { _sectionItemIndex[views[i].id] = i; } - - // sectionItems.insert(_sectionItemIndex[viewWidgets[i].key.hashCode]!, viewWidgets[i]); } } @@ -108,8 +103,6 @@ class _RenderSectionItemsState extends State { log("BUILD: Section items: ${views.length}"); return ReorderableColumn( - // itemCount: sectionItems.length, - // buildDefaultDragHandles: false, needsLongPressDraggable: false, onReorder: (oldIndex, index) { setState(() { @@ -120,14 +113,11 @@ class _RenderSectionItemsState extends State { _sectionItemIndex[section.id] = index; }); }, - // physics: StyledScrollPhysics(), - // itemBuilder: (context, index) {}, children: List.generate( views.length, (index) { return Container( key: ValueKey(views[index].id), - // index: index, child: views .map( (view) => ViewSectionItem( 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 99f3d22946..6db19495ac 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -1,8 +1,6 @@ export './app/header/header.dart'; export './app/menu_app.dart'; -import 'dart:developer'; - import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:app_flowy/workspace/presentation/plugins/trash/menu.dart'; import 'package:flowy_infra/notifier.dart'; @@ -47,9 +45,8 @@ class HomeMenu extends StatefulWidget { } class _HomeMenuState extends State { - // final List _menuItems = List.empty(growable: true); /// Maps the hashmap of the menu items to their index in reorderable list view. - //TODO @gaganyadav80: need to retain this to persist on app restarts. + //TODO @gaganyadav80: Retain this map to persist on app restarts. final Map _menuItemIndex = {}; @override @@ -140,19 +137,6 @@ class _HomeMenuState extends State { menuItems.insert(_menuItemIndex[appWidgets[i].key.hashCode]!, appWidgets[i]); } - // for (var app in appWidgets) { - // if (!_menuItems.any((oldElement) => oldElement.key == app.key)) { - // _menuItems.add(app); - // } - // } - // // TODO @gaganyadav80: fix: concurrent modification exception - // // Unhandled Exception: Concurrent modification during iteration: Instance(length:3) of '_GrowableList'. - // for (var item in _menuItems) { - // if (!appWidgets.any((oldElement) => oldElement.key == item.key)) { - // _menuItems.remove(item); - // } - // } - return menuItems; }, builder: (context, menuItems) { @@ -179,7 +163,7 @@ class _HomeMenuState extends State { }, physics: StyledScrollPhysics(), itemBuilder: (BuildContext context, int index) { - //? To mimic the ListView.separated behavior, we need to add a padding. + //? @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);