From d25830aece14c3910fd4ae1f08fb30a55df5a892 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:24:38 +0800 Subject: [PATCH] feat: mobile grid fab (#4093) --- .../presentation/base/mobile_view_page.dart | 12 +- .../grid/application/grid_bloc.dart | 15 ++- .../grid/presentation/mobile_grid_page.dart | 50 ++++++-- .../grid/presentation/widgets/mobile_fab.dart | 109 ++++++++++++++++++ 4 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/mobile_fab.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 31295eedff..de7a2c5308 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -93,20 +93,12 @@ class _MobileViewPageState extends State { child: Builder( builder: (context) { final view = context.watch().state.view; - return _buildApp( - view, - actions, - body, - ); + return _buildApp(view, actions, body); }, ), ); } else { - return _buildApp( - null, - [], - body, - ); + return _buildApp(null, [], body); } }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index 117c2478da..5c7f692c14 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -26,8 +27,15 @@ class GridBloc extends Bloc { _startListening(); await _openGrid(emit); }, - createRow: () { - databaseController.createRow(); + createRow: () async { + final result = await databaseController.createRow(); + result.fold( + (createdRow) => emit(state.copyWith(createdRow: createdRow)), + (err) => Log.error(err), + ); + }, + resetCreatedRow: () { + emit(state.copyWith(createdRow: null)); }, deleteRow: (rowInfo) async { await RowBackendService.deleteRow(rowInfo.viewId, rowInfo.rowId); @@ -142,6 +150,7 @@ class GridBloc extends Bloc { class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; const factory GridEvent.createRow() = _CreateRow; + const factory GridEvent.resetCreatedRow() = _ResetCreatedRow; const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow; const factory GridEvent.moveRow(int from, int to) = _MoveRow; const factory GridEvent.didLoadRows( @@ -170,6 +179,7 @@ class GridState with _$GridState { required FieldList fields, required List rowInfos, required int rowCount, + required RowMetaPB? createdRow, required LoadingState loadingState, required bool reorderable, required ChangedReason reason, @@ -181,6 +191,7 @@ class GridState with _$GridState { fields: FieldList([]), rowInfos: [], rowCount: 0, + createdRow: null, grid: none(), viewId: viewId, reorderable: true, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart index 6a2b6303d0..61c7a36678 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/mobile_grid_page.dart @@ -25,6 +25,7 @@ import 'layout/layout.dart'; import 'layout/sizes.dart'; import 'widgets/footer/grid_footer.dart'; import 'widgets/header/grid_header.dart'; +import 'widgets/mobile_fab.dart'; import 'widgets/row/mobile_row.dart'; import 'widgets/shortcuts.dart'; @@ -147,23 +148,50 @@ class _GridPageContentState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + listenWhen: (previous, current) => + previous.createdRow != current.createdRow, + listener: (context, state) { + if (state.createdRow == null) { + return; + } + final bloc = context.read(); + context.push( + MobileRowDetailPage.routeName, + extra: { + MobileRowDetailPage.argRowId: state.createdRow!.id, + MobileRowDetailPage.argDatabaseController: bloc.databaseController, + }, + ); + bloc.add(const GridEvent.resetCreatedRow()); + }, buildWhen: (previous, current) => previous.fields != current.fields, builder: (context, state) { final contentWidth = GridLayout.headerWidth(state.fields.fields); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, + return Stack( children: [ - Padding( - padding: EdgeInsets.only(right: GridSize.leadingHeaderPadding), - child: - _GridHeader(headerScrollController: headerScrollController), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + EdgeInsets.only(right: GridSize.leadingHeaderPadding), + child: _GridHeader( + headerScrollController: headerScrollController, + ), + ), + _GridRows( + viewId: state.viewId, + contentWidth: contentWidth, + scrollController: _scrollController, + ), + ], ), - _GridRows( - viewId: state.viewId, - contentWidth: contentWidth, - scrollController: _scrollController, + Positioned( + bottom: 20, + right: 20, + child: getGridFabs(context), ), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/mobile_fab.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/mobile_fab.dart new file mode 100644 index 0000000000..bc699e4a9b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/mobile_fab.dart @@ -0,0 +1,109 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; +import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +Widget getGridFabs(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + MobileGridFab( + backgroundColor: Theme.of(context).colorScheme.surface, + foregroundColor: Theme.of(context).primaryColor, + onTap: () { + final bloc = context.read(); + if (bloc.state.rowInfos.isNotEmpty) { + context.push( + MobileRowDetailPage.routeName, + extra: { + MobileRowDetailPage.argRowId: bloc.state.rowInfos.first.rowId, + MobileRowDetailPage.argDatabaseController: + bloc.databaseController, + }, + ); + } + }, + boxShadow: const BoxShadow( + offset: Offset(0, 6), + color: Color(0x145D7D8B), + blurRadius: 18, + ), + icon: FlowySvgs.properties_s, + iconSize: const Size.square(24), + ), + const HSpace(16), + MobileGridFab( + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + onTap: () { + context.read().add(const GridEvent.createRow()); + }, + overlayColor: const MaterialStatePropertyAll(Color(0xFF009FD1)), + boxShadow: const BoxShadow( + offset: Offset(0, 8), + color: Color(0x6612BFEF), + blurRadius: 18, + spreadRadius: -5, + ), + icon: FlowySvgs.add_s, + iconSize: const Size.square(24), + ), + ], + ); +} + +class MobileGridFab extends StatelessWidget { + const MobileGridFab({ + super.key, + required this.backgroundColor, + required this.foregroundColor, + required this.boxShadow, + required this.onTap, + required this.icon, + required this.iconSize, + this.overlayColor, + }); + + final Color backgroundColor; + final Color foregroundColor; + final BoxShadow boxShadow; + final VoidCallback onTap; + final FlowySvgData icon; + final Size iconSize; + final MaterialStateProperty? overlayColor; + + @override + Widget build(BuildContext context) { + final radius = BorderRadius.circular(20); + return Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: radius, + boxShadow: [boxShadow], + ), + child: Material( + borderOnForeground: false, + color: backgroundColor, + borderRadius: radius, + child: InkWell( + borderRadius: radius, + overlayColor: overlayColor, + onTap: onTap, + child: SizedBox.square( + dimension: 56, + child: Center( + child: FlowySvg( + icon, + color: foregroundColor, + size: iconSize, + ), + ), + ), + ), + ), + ); + } +}