From 46173b394032b70674385841a6d9f4c65e26360b Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 20 Oct 2024 14:08:47 +0200 Subject: [PATCH 1/7] feat: gallery view poc --- .../desktop/sidebar/sidebar_test.dart | 4 + .../board/presentation/board_page.dart | 8 +- .../database/domain/layout_service.dart | 4 + .../gallery/application/gallery_bloc.dart | 226 ++++++++++++++ .../lib/plugins/database/gallery/gallery.dart | 34 +++ .../gallery/presentation/gallery_card.dart | 131 ++++++++ .../gallery/presentation/gallery_page.dart | 289 ++++++++++++++++++ .../presentation/toolbar/gallery_toolbar.dart | 63 ++++ .../tab_bar/desktop/tab_bar_add_button.dart | 30 +- .../plugins/database/widgets/card/card.dart | 37 ++- .../database/widgets/card/card_bloc.dart | 6 +- .../database/widgets/database_layout_ext.dart | 3 + .../base/insert_page_command.dart | 4 + .../database_view_block_component.dart | 17 +- .../lib/startup/plugin/plugin.dart | 1 + .../lib/startup/tasks/load_plugin.dart | 5 + .../workspace/application/view/view_ext.dart | 14 +- .../home/menu/view/view_item.dart | 2 + .../resources/flowy_icons/16x/gallery.svg | 1 + frontend/resources/translations/en.json | 6 + .../src/deps_resolve/folder_deps.rs | 10 +- .../src/entities/setting_entities.rs | 3 + .../src/services/database_view/layout_deps.rs | 2 + .../src/services/database_view/view_editor.rs | 1 + .../field_settings/field_settings_builder.rs | 1 + .../rust-lib/flowy-database2/src/template.rs | 53 ++++ .../flowy-folder/src/entities/view.rs | 4 +- frontend/rust-lib/flowy-folder/src/manager.rs | 4 +- .../flowy-folder/src/view_operation.rs | 1 + 29 files changed, 911 insertions(+), 53 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/database/gallery/application/gallery_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/gallery/gallery.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/toolbar/gallery_toolbar.dart create mode 100644 frontend/resources/flowy_icons/16x/gallery.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart index 304e8e2e35e35..b01339537f857 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database/gallery/presentation/gallery_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; @@ -65,6 +66,9 @@ void main() { case ViewLayoutPB.Calendar: expect(find.byType(CalendarPage), findsOneWidget); break; + case ViewLayoutPB.Gallery: + expect(find.byType(GalleryPage), findsOneWidget); + break; case ViewLayoutPB.Chat: break; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 66395724df215..55377f49c56d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -360,12 +360,8 @@ class _BoardContentState extends State<_BoardContent> { ), footerBuilder: (_, groupData) => MultiBlocProvider( providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: context.read(), - ), + BlocProvider.value(value: context.read()), + BlocProvider.value(value: context.read()), ], child: BoardColumnFooter( columnData: groupData, diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart index 58f06d06a8069..2cd21e7009510 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/layout_service.dart @@ -9,6 +9,8 @@ ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) { return ViewLayoutPB.Calendar; case DatabaseLayoutPB.Grid: return ViewLayoutPB.Grid; + case DatabaseLayoutPB.Gallery: + return ViewLayoutPB.Gallery; default: throw UnimplementedError; } @@ -22,6 +24,8 @@ DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) { return DatabaseLayoutPB.Calendar; case ViewLayoutPB.Grid: return DatabaseLayoutPB.Grid; + case ViewLayoutPB.Gallery: + return DatabaseLayoutPB.Gallery; default: throw UnimplementedError; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/application/gallery_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/application/gallery_bloc.dart new file mode 100644 index 0000000000000..5cb8f483330a8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/application/gallery_bloc.dart @@ -0,0 +1,226 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/defines.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; +import 'package:appflowy/plugins/database/application/field/sort_entities.dart'; +import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../application/database_controller.dart'; + +part 'gallery_bloc.freezed.dart'; + +class GalleryBloc extends Bloc { + GalleryBloc({required ViewPB view, required this.databaseController}) + : super(GalleryState.initial(view.id)) { + _dispatch(); + } + + final DatabaseController databaseController; + + String get viewId => databaseController.viewId; + + UserProfilePB? _userProfile; + UserProfilePB? get userProfile => _userProfile; + + void _dispatch() { + on( + (event, emit) async { + await event.when( + initial: () async { + final response = await UserEventGetUserProfile().send(); + response.fold( + (userProfile) => _userProfile = userProfile, + (err) => Log.error(err), + ); + + _startListening(); + await _openGallery(emit); + }, + openRowDetail: (row) { + emit( + state.copyWith( + createdRow: row, + openRowDetail: true, + ), + ); + }, + createRow: (openRowDetail) async { + final result = await RowBackendService.createRow(viewId: viewId); + result.fold( + (createdRow) => emit( + state.copyWith( + createdRow: createdRow, + openRowDetail: openRowDetail ?? false, + ), + ), + (err) => Log.error(err), + ); + }, + resetCreatedRow: () { + emit(state.copyWith(createdRow: null, openRowDetail: false)); + }, + deleteRow: (rowInfo) async { + await RowBackendService.deleteRows(viewId, [rowInfo.rowId]); + }, + moveRow: (int from, int to) { + final List rows = [...state.rowInfos]; + + final fromRow = rows[from].rowId; + final toRow = rows[to].rowId; + + rows.insert(to, rows.removeAt(from)); + emit(state.copyWith(rowInfos: rows)); + + databaseController.moveRow(fromRowId: fromRow, toRowId: toRow); + }, + didReceiveFieldUpdate: (fields) { + emit(state.copyWith(fields: fields)); + }, + didLoadRows: (newRowInfos, reason) { + emit( + state.copyWith( + rowInfos: newRowInfos, + rowCount: newRowInfos.length, + reason: reason, + ), + ); + }, + didReceveFilters: (filters) { + emit(state.copyWith(filters: filters)); + }, + didReceveSorts: (sorts) { + emit(state.copyWith(reorderable: sorts.isEmpty, sorts: sorts)); + }, + ); + }, + ); + } + + RowCache get rowCache => databaseController.rowCache; + + void _startListening() { + final onDatabaseChanged = DatabaseCallbacks( + onNumOfRowsChanged: (rowInfos, _, reason) { + if (!isClosed) { + add(GalleryEvent.didLoadRows(rowInfos, reason)); + } + }, + onRowsCreated: (rows) { + for (final row in rows) { + if (!isClosed && row.isHiddenInView) { + add(GalleryEvent.openRowDetail(row.rowMeta)); + } + } + }, + onRowsUpdated: (rows, reason) { + if (!isClosed) { + add( + GalleryEvent.didLoadRows( + databaseController.rowCache.rowInfos, + reason, + ), + ); + } + }, + onFieldsChanged: (fields) { + if (!isClosed) { + add(GalleryEvent.didReceiveFieldUpdate(fields)); + } + }, + onFiltersChanged: (filters) { + if (!isClosed) { + add(GalleryEvent.didReceveFilters(filters)); + } + }, + onSortsChanged: (sorts) { + if (!isClosed) { + add(GalleryEvent.didReceveSorts(sorts)); + } + }, + ); + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); + } + + Future _openGallery(Emitter emit) async { + final result = await databaseController.open(); + result.fold( + (grid) { + databaseController.setIsLoading(false); + emit( + state.copyWith( + loadingState: LoadingState.finish(FlowyResult.success(null)), + ), + ); + }, + (err) => emit( + state.copyWith( + loadingState: LoadingState.finish(FlowyResult.failure(err)), + ), + ), + ); + } +} + +@freezed +class GalleryEvent with _$GalleryEvent { + const factory GalleryEvent.initial() = InitialGrid; + const factory GalleryEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail; + const factory GalleryEvent.createRow({bool? openRowDetail}) = _CreateRow; + const factory GalleryEvent.resetCreatedRow() = _ResetCreatedRow; + const factory GalleryEvent.deleteRow(RowInfo rowInfo) = _DeleteRow; + const factory GalleryEvent.moveRow(int from, int to) = _MoveRow; + const factory GalleryEvent.didLoadRows( + List rows, + ChangedReason reason, + ) = _DidReceiveRowUpdate; + const factory GalleryEvent.didReceiveFieldUpdate( + List fields, + ) = _DidReceiveFieldUpdate; + + const factory GalleryEvent.didReceveFilters(List filters) = + _DidReceiveFilters; + const factory GalleryEvent.didReceveSorts(List sorts) = + _DidReceiveSorts; +} + +@freezed +class GalleryState with _$GalleryState { + const factory GalleryState({ + required String viewId, + required List fields, + required List rowInfos, + required int rowCount, + required RowMetaPB? createdRow, + required LoadingState loadingState, + required bool reorderable, + required ChangedReason reason, + required List sorts, + required List filters, + required bool openRowDetail, + }) = _GalleryState; + + factory GalleryState.initial(String viewId) => GalleryState( + fields: [], + rowInfos: [], + rowCount: 0, + createdRow: null, + viewId: viewId, + reorderable: true, + loadingState: const LoadingState.loading(), + reason: const InitialListState(), + filters: [], + sorts: [], + openRowDetail: false, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/gallery.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/gallery.dart new file mode 100644 index 0000000000000..53da20aed4559 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/gallery.dart @@ -0,0 +1,34 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class GalleryPluginBuilder implements PluginBuilder { + @override + Plugin build(dynamic data) { + if (data is ViewPB) { + return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data); + } else { + throw FlowyPluginException.invalidData; + } + } + + @override + String get menuName => LocaleKeys.databaseGallery_menuName.tr(); + + @override + FlowySvgData get icon => FlowySvgs.gallery_s; + + @override + PluginType get pluginType => PluginType.gallery; + + @override + ViewLayoutPB get layoutType => ViewLayoutPB.Gallery; +} + +class GalleryPluginConfig implements PluginConfig { + @override + bool get creatable => true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart new file mode 100644 index 0000000000000..bc24ee977290a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart @@ -0,0 +1,131 @@ +import 'package:appflowy/plugins/database/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database/widgets/card/card.dart'; +import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class GalleryCard extends StatefulWidget { + const GalleryCard({ + super.key, + required this.viewId, + required this.controller, + required this.rowMeta, + required this.rowCache, + required this.onTap, + required this.cellBuilder, + required this.styleConfiguration, + this.onShiftTap, + this.userProfile, + }); + + final String viewId; + final FieldController controller; + final RowMetaPB rowMeta; + final RowCache rowCache; + + final CardCellBuilder cellBuilder; + final RowCardStyleConfiguration styleConfiguration; + + final void Function(BuildContext context) onTap; + final void Function(BuildContext context)? onShiftTap; + + final UserProfilePB? userProfile; + + @override + State createState() => _GalleryCardState(); +} + +class _GalleryCardState extends State { + late final CardBloc _bloc; + + @override + void initState() { + super.initState(); + _bloc = CardBloc( + viewId: widget.viewId, + fieldController: widget.controller, + rowController: RowController( + viewId: widget.viewId, + rowMeta: widget.rowMeta, + rowCache: widget.rowCache, + ), + ); + } + + @override + void dispose() { + _bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _bloc..add(const CardEvent.initial()), + child: GestureDetector( + onTap: () => widget.onTap(context), + child: Container( + constraints: const BoxConstraints(maxWidth: 200), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + blurRadius: 4, + color: const Color(0xFF1F2329).withOpacity(0.02), + ), + BoxShadow( + blurRadius: 4, + spreadRadius: -2, + color: const Color(0xFF1F2329).withOpacity(0.02), + ), + ], + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CardCover( + cover: state.rowMeta.cover, + userProfile: widget.userProfile, + showDefaultCover: true, + ), + ..._makeCells(context, state.rowMeta, state.cells), + ], + ); + }, + ), + ), + ), + ); + } + + List _makeCells( + BuildContext context, + RowMetaPB rowMeta, + List cells, + ) { + return cells + .mapIndexed( + (int index, CellMeta cellMeta) => Padding( + padding: widget.styleConfiguration.cardPadding, + child: CardContentCell( + cellBuilder: widget.cellBuilder, + cellMeta: cellMeta, + rowMeta: rowMeta, + isTitle: index == 0, + styleMap: widget.styleConfiguration.cellStyleMap, + ), + ), + ) + .toList(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart new file mode 100644 index 0000000000000..1366fe1e90b1a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart @@ -0,0 +1,289 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/plugins/database/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database/gallery/application/gallery_bloc.dart'; +import 'package:appflowy/plugins/database/gallery/presentation/gallery_card.dart'; +import 'package:appflowy/plugins/database/gallery/presentation/toolbar/gallery_toolbar.dart'; +import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; +import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; +import 'package:appflowy/plugins/database/widgets/card/card.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart'; +import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; +import 'package:appflowy/shared/flowy_error_page.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:reorderables/reorderables.dart'; + +class GalleryPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { + final _toggleExtension = ToggleExtensionNotifier(); + + @override + Widget content( + BuildContext context, + ViewPB view, + DatabaseController controller, + bool shrinkWrap, + String? initialRowId, + ) { + return GalleryPage( + key: _makeValueKey(controller), + view: view, + databaseController: controller, + ); + } + + @override + Widget settingBar(BuildContext context, DatabaseController controller) { + return GalleryToolbar( + key: _makeValueKey(controller), + controller: controller, + toggleExtension: _toggleExtension, + ); + } + + @override + Widget settingBarExtension( + BuildContext context, + DatabaseController controller, + ) { + return DatabaseViewSettingExtension( + key: _makeValueKey(controller), + viewId: controller.viewId, + databaseController: controller, + toggleExtension: _toggleExtension, + ); + } + + @override + void dispose() { + _toggleExtension.dispose(); + super.dispose(); + } + + ValueKey _makeValueKey(DatabaseController controller) => + ValueKey(controller.viewId); +} + +class GalleryPage extends StatelessWidget { + const GalleryPage({ + super.key, + required this.view, + required this.databaseController, + }); + + final ViewPB view; + final DatabaseController databaseController; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => GalleryBloc( + view: view, + databaseController: databaseController, + )..add(const GalleryEvent.initial()), + child: BlocBuilder( + builder: (context, state) => state.loadingState.map( + loading: (_) => const Center(child: CircularProgressIndicator()), + finish: (result) => result.successOrFail.fold( + (_) => GalleryContent( + key: ValueKey(view.id), + viewId: view.id, + controller: databaseController, + cellBuilder: CardCellBuilder( + databaseController: databaseController, + ), + ), + (err) => Center(child: AppFlowyErrorPage(error: err)), + ), + idle: (_) => const SizedBox.shrink(), + ), + ), + ); + } +} + +class GalleryContent extends StatefulWidget { + const GalleryContent({ + super.key, + required this.viewId, + required this.controller, + required this.cellBuilder, + }); + + final String viewId; + final DatabaseController controller; + final CardCellBuilder cellBuilder; + + @override + State createState() => _GalleryContentState(); +} + +class _GalleryContentState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: 8, + horizontal: context + .read() + .horizontalPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + const double minItemWidth = 200; + const double maxItemWidth = 350; + const double spacing = 8; + + final maxItemsPerRow = calculateItemsPerRow( + maxWidth, + minItemWidth, + maxItemWidth, + spacing, + ); + + final itemCount = state.rowCount + 1; + final itemsPerRow = + itemCount < maxItemsPerRow ? itemCount : maxItemsPerRow; + + // Calculate the width of each item in the current row configuration + // TODO: Refine behavior, without the 0.0...1 buffer, resizing can cause odd behavior + final totalSpacing = (itemsPerRow - 1) * spacing + 0.000001; + final itemWidth = (maxWidth - totalSpacing) / itemsPerRow; + + return ReorderableWrap( + enableReorder: state.sorts.isEmpty, + spacing: spacing, + runSpacing: spacing, + onReorder: (oldIndex, newIndex) { + if (oldIndex != newIndex && newIndex < itemCount - 1) { + context + .read() + .add(GalleryEvent.moveRow(oldIndex, newIndex)); + } + }, + buildDraggableFeedback: (_, __, child) => Material( + type: MaterialType.transparency, + child: Opacity(opacity: 0.8, child: child), + ), + children: state.rowInfos.map((rowInfo) { + return SizedBox( + key: ValueKey(rowInfo.rowId), + width: itemWidth, + child: GalleryCard( + controller: widget.controller.fieldController, + userProfile: context.read().userProfile, + cellBuilder: widget.cellBuilder, + rowMeta: rowInfo.rowMeta, + viewId: widget.viewId, + rowCache: widget.controller.rowCache, + styleConfiguration: RowCardStyleConfiguration( + cardPadding: const EdgeInsets.all(4), + cellStyleMap: desktopBoardCardCellStyleMap(context), + ), + onTap: (_) => _openCard( + context: context, + databaseController: widget.controller, + rowMeta: rowInfo.rowMeta, + ), + ), + ); + }).toList() + ..add(_AddCard(itemWidth: itemWidth)), + ); + }, + ), + ); + }, + ); + } + + void _openCard({ + required BuildContext context, + required DatabaseController databaseController, + required RowMetaPB rowMeta, + }) { + final rowController = RowController( + rowMeta: rowMeta, + viewId: databaseController.viewId, + rowCache: databaseController.rowCache, + ); + + FlowyOverlay.show( + context: context, + builder: (_) => RowDetailPage( + databaseController: databaseController, + rowController: rowController, + userProfile: context.read().userProfile, + ), + ); + } + + int calculateItemsPerRow( + double maxWidth, + double minItemWidth, + double maxItemWidth, + double spacing, + ) { + int itemsPerRow = (maxWidth / (minItemWidth + spacing)).floor(); + + while (itemsPerRow > 1 && + ((maxWidth - (itemsPerRow - 1) * spacing) / itemsPerRow) > + maxItemWidth) { + itemsPerRow--; + } + + return itemsPerRow; + } +} + +class _AddCard extends StatelessWidget { + const _AddCard({required this.itemWidth}); + + final double itemWidth; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => + context.read().add(const GalleryEvent.createRow()), + // This disables the long press behavior of dragging the card + onLongPress: () {}, + child: FlowyHover( + resetHoverOnRebuild: false, + builder: (context, isHovering) => SizedBox( + width: itemWidth == double.infinity ? 175 : itemWidth, + height: itemWidth == double.infinity ? 175 : 140, + child: DottedBorder( + dashPattern: const [3, 3], + radius: const Radius.circular(8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 32, + ), + borderType: BorderType.RRect, + color: isHovering + ? Theme.of(context).primaryColor + : Theme.of(context).hintColor, + child: Center( + child: FlowyText( + LocaleKeys.databaseGallery_addCard.tr(), + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/toolbar/gallery_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/toolbar/gallery_toolbar.dart new file mode 100644 index 0000000000000..82179109472e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/toolbar/gallery_toolbar.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart'; +import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class GalleryToolbar extends StatelessWidget { + const GalleryToolbar({ + super.key, + required this.controller, + required this.toggleExtension, + }); + + final DatabaseController controller; + final ToggleExtensionNotifier toggleExtension; + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => FilterEditorBloc( + viewId: controller.viewId, + fieldController: controller.fieldController, + ), + ), + BlocProvider( + create: (_) => SortEditorBloc( + viewId: controller.viewId, + fieldController: controller.fieldController, + ), + ), + ], + child: ValueListenableBuilder( + valueListenable: controller.isLoading, + builder: (context, isLoading, child) { + if (isLoading) { + return const SizedBox.shrink(); + } + + return SizedBox( + height: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilterButton(toggleExtension: toggleExtension), + const HSpace(6), + SortButton(toggleExtension: toggleExtension), + const HSpace(6), + SettingButton(databaseController: controller), + ], + ), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart index e91e739b97b89..b42b427aac91b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart @@ -50,14 +50,12 @@ class _AddDatabaseViewButtonState extends State { iconColorOnHover: Theme.of(context).colorScheme.onSurface, ), ), - popupBuilder: (BuildContext context) { - return TabBarAddButtonAction( - onTap: (action) { - popoverController.close(); - widget.onTap(action); - }, - ); - }, + popupBuilder: (_) => TabBarAddButtonAction( + onTap: (action) { + popoverController.close(); + widget.onTap(action); + }, + ), ); } } @@ -69,19 +67,17 @@ class TabBarAddButtonAction extends StatelessWidget { @override Widget build(BuildContext context) { - final cells = DatabaseLayoutPB.values.map((layout) { - return TabBarAddButtonActionCell( - action: layout, - onTap: onTap, - ); - }).toList(); + final cells = DatabaseLayoutPB.values.map( + (layout) { + return TabBarAddButtonActionCell(action: layout, onTap: onTap); + }, + ).toList(); return ListView.separated( shrinkWrap: true, itemCount: cells.length, - itemBuilder: (BuildContext context, int index) => cells[index], - separatorBuilder: (BuildContext context, int index) => - VSpace(GridSize.typeOptionSeparatorHeight), + itemBuilder: (_, index) => cells[index], + separatorBuilder: (_, __) => VSpace(GridSize.typeOptionSeparatorHeight), padding: const EdgeInsets.symmetric(vertical: 4.0), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart index 3ee506f918a42..b2ba3ecddfb87 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart @@ -264,7 +264,7 @@ class _CardContent extends StatelessWidget { ) { return cells .mapIndexed( - (int index, CellMeta cellMeta) => _CardContentCell( + (int index, CellMeta cellMeta) => CardContentCell( cellBuilder: cellBuilder, cellMeta: cellMeta, rowMeta: rowMeta, @@ -276,8 +276,9 @@ class _CardContent extends StatelessWidget { } } -class _CardContentCell extends StatefulWidget { - const _CardContentCell({ +class CardContentCell extends StatefulWidget { + const CardContentCell({ + super.key, required this.cellBuilder, required this.cellMeta, required this.rowMeta, @@ -292,10 +293,10 @@ class _CardContentCell extends StatefulWidget { final bool isTitle; @override - State<_CardContentCell> createState() => _CardContentCellState(); + State createState() => _CardContentCellState(); } -class _CardContentCellState extends State<_CardContentCell> { +class _CardContentCellState extends State { late final EditableCardNotifier? cellNotifier; @override @@ -340,11 +341,13 @@ class CardCover extends StatelessWidget { this.cover, this.userProfile, this.isCompact = false, + this.showDefaultCover = false, }); final RowCoverPB? cover; final UserProfilePB? userProfile; final bool isCompact; + final bool showDefaultCover; @override Widget build(BuildContext context) { @@ -352,6 +355,26 @@ class CardCover extends StatelessWidget { cover!.data.isEmpty || cover!.uploadType == FileUploadTypePB.CloudFile && userProfile == null) { + if (showDefaultCover) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + color: Theme.of(context).cardColor, + ), + child: _renderCover( + context, + RowCoverPB( + coverType: CoverTypePB.ColorCover, + data: "0xFFEFEFEF", + ), + ), + ); + } + return const SizedBox.shrink(); } @@ -365,9 +388,7 @@ class CardCover extends StatelessWidget { color: Theme.of(context).cardColor, ), child: Row( - children: [ - Expanded(child: _renderCover(context, cover!)), - ], + children: [Expanded(child: _renderCover(context, cover!))], ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart index 04f9bb652cdda..3c14cf8a6d767 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/foundation.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -17,9 +17,9 @@ part 'card_bloc.freezed.dart'; class CardBloc extends Bloc { CardBloc({ required this.fieldController, - required this.groupFieldId, + this.groupFieldId, required this.viewId, - required bool isEditing, + bool isEditing = false, required this.rowController, }) : super( CardState.initial( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart index f8118a7e51cab..bf2b7c65ecf5b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart @@ -10,6 +10,7 @@ extension DatabaseLayoutExtension on DatabaseLayoutPB { DatabaseLayoutPB.Board => LocaleKeys.board_menuName.tr(), DatabaseLayoutPB.Calendar => LocaleKeys.calendar_menuName.tr(), DatabaseLayoutPB.Grid => LocaleKeys.grid_menuName.tr(), + DatabaseLayoutPB.Gallery => LocaleKeys.databaseGallery_menuName.tr(), _ => "", }; } @@ -19,6 +20,7 @@ extension DatabaseLayoutExtension on DatabaseLayoutPB { DatabaseLayoutPB.Board => ViewLayoutPB.Board, DatabaseLayoutPB.Calendar => ViewLayoutPB.Calendar, DatabaseLayoutPB.Grid => ViewLayoutPB.Grid, + DatabaseLayoutPB.Gallery => ViewLayoutPB.Gallery, _ => throw UnimplementedError(), }; } @@ -28,6 +30,7 @@ extension DatabaseLayoutExtension on DatabaseLayoutPB { DatabaseLayoutPB.Board => FlowySvgs.board_s, DatabaseLayoutPB.Calendar => FlowySvgs.calendar_s, DatabaseLayoutPB.Grid => FlowySvgs.grid_s, + DatabaseLayoutPB.Gallery => FlowySvgs.gallery_s, _ => throw UnimplementedError(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index 9bcd0e18b83b5..5845d14e94a27 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -131,6 +131,8 @@ extension InsertDatabase on EditorState { return LocaleKeys.board_referencedBoardPrefix.tr(); case ViewLayoutPB.Calendar: return LocaleKeys.calendar_referencedCalendarPrefix.tr(); + case ViewLayoutPB.Gallery: + return LocaleKeys.databaseGallery_referencedGalleryPrefix.tr(); default: throw UnimplementedError(); } @@ -144,6 +146,8 @@ extension InsertDatabase on EditorState { return DatabaseBlockKeys.boardType; case ViewLayoutPB.Calendar: return DatabaseBlockKeys.calendarType; + case ViewLayoutPB.Gallery: + return DatabaseBlockKeys.galleryType; default: throw Exception('Unknown layout type'); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart index 630d612bcd89b..6c77b1d6d5530 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart @@ -10,15 +10,14 @@ class DatabaseBlockKeys { static const String gridType = 'grid'; static const String boardType = 'board'; static const String calendarType = 'calendar'; + static const String galleryType = 'gallery'; static const String parentID = 'parent_id'; static const String viewID = 'view_id'; } class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder { - DatabaseViewBlockComponentBuilder({ - super.configuration, - }); + DatabaseViewBlockComponentBuilder({super.configuration}); @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -28,10 +27,7 @@ class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder { node: node, configuration: configuration, showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), + actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), ); } @@ -71,12 +67,7 @@ class _DatabaseBlockComponentWidgetState Widget child = BuiltInPageWidget( node: widget.node, editorState: editorState, - builder: (viewPB) { - return DatabaseViewWidget( - key: ValueKey(viewPB.id), - view: viewPB, - ); - }, + builder: (view) => DatabaseViewWidget(key: ValueKey(view.id), view: view), ); child = Padding( diff --git a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart index 920a994927baf..ab41e319a81cd 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart @@ -19,6 +19,7 @@ enum PluginType { calendar, databaseDocument, chat, + gallery, } typedef PluginId = String; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart b/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart index 9a75607d74270..fd6a5cc236ea1 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/load_plugin.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/ai_chat/chat.dart'; import 'package:appflowy/plugins/database/calendar/calendar.dart'; import 'package:appflowy/plugins/database/board/board.dart'; +import 'package:appflowy/plugins/database/gallery/gallery.dart'; import 'package:appflowy/plugins/database/grid/grid.dart'; import 'package:appflowy/plugins/database_document/database_document_plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -26,6 +27,10 @@ class PluginLoadTask extends LaunchTask { builder: CalendarPluginBuilder(), config: CalendarPluginConfig(), ); + registerPlugin( + builder: GalleryPluginBuilder(), + config: GalleryPluginConfig(), + ); registerPlugin( builder: DatabaseDocumentPluginBuilder(), config: DatabaseDocumentPluginConfig(), diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 09231f882f3a9..4fa058368c57f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -6,6 +6,7 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc. import 'package:appflowy/plugins/ai_chat/chat.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; +import 'package:appflowy/plugins/database/gallery/presentation/gallery_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/mobile_grid_page.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; @@ -60,6 +61,7 @@ extension ViewExtension on ViewPB { ViewLayoutPB.Grid => FlowySvgs.icon_grid_s, ViewLayoutPB.Document => FlowySvgs.icon_document_s, ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s, + ViewLayoutPB.Gallery => FlowySvgs.gallery_s, _ => FlowySvgs.document_s, }, size: size, @@ -71,6 +73,7 @@ extension ViewExtension on ViewPB { ViewLayoutPB.Document => PluginType.document, ViewLayoutPB.Grid => PluginType.grid, ViewLayoutPB.Chat => PluginType.chat, + ViewLayoutPB.Gallery => PluginType.gallery, _ => throw UnimplementedError(), }; @@ -78,6 +81,7 @@ extension ViewExtension on ViewPB { Map arguments = const {}, }) { switch (layout) { + case ViewLayoutPB.Gallery: case ViewLayoutPB.Board: case ViewLayoutPB.Calendar: case ViewLayoutPB.Grid: @@ -109,6 +113,7 @@ extension ViewExtension on ViewPB { ViewLayoutPB.Board => BoardPageTabBarBuilderImpl(), ViewLayoutPB.Calendar => CalendarPageTabBarBuilderImpl(), ViewLayoutPB.Grid => DesktopGridTabBarBuilderImpl(), + ViewLayoutPB.Gallery => GalleryPageTabBarBuilderImpl(), _ => throw UnimplementedError, }; @@ -116,6 +121,8 @@ extension ViewExtension on ViewPB { ViewLayoutPB.Board => BoardPageTabBarBuilderImpl(), ViewLayoutPB.Calendar => CalendarPageTabBarBuilderImpl(), ViewLayoutPB.Grid => MobileGridTabBarBuilderImpl(), + // TODO(Gallery): Custom tab bar for gallery + ViewLayoutPB.Gallery => GalleryPageTabBarBuilderImpl(), _ => throw UnimplementedError, }; @@ -281,6 +288,7 @@ extension ViewLayoutExtension on ViewLayoutPB { ViewLayoutPB.Calendar => FlowySvgs.calendar_s, ViewLayoutPB.Document => FlowySvgs.document_s, ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s, + ViewLayoutPB.Gallery => FlowySvgs.gallery_s, _ => throw Exception('Unknown layout type'), }; @@ -289,7 +297,8 @@ extension ViewLayoutExtension on ViewLayoutPB { ViewLayoutPB.Chat || ViewLayoutPB.Grid || ViewLayoutPB.Board || - ViewLayoutPB.Calendar => + ViewLayoutPB.Calendar || + ViewLayoutPB.Gallery => false, _ => throw Exception('Unknown layout type'), }; @@ -297,7 +306,8 @@ extension ViewLayoutExtension on ViewLayoutPB { bool get isDatabaseView => switch (this) { ViewLayoutPB.Grid || ViewLayoutPB.Board || - ViewLayoutPB.Calendar => + ViewLayoutPB.Calendar || + ViewLayoutPB.Gallery => true, ViewLayoutPB.Document || ViewLayoutPB.Chat => false, _ => throw Exception('Unknown layout type'), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 36e7e1de996d9..21030af3507ba 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -816,6 +816,8 @@ class _SingleInnerViewItemState extends State { return LocaleKeys.newBoardText.tr(); case ViewLayoutPB.Calendar: return LocaleKeys.newCalendarText.tr(); + case ViewLayoutPB.Gallery: + return LocaleKeys.newGalleryText.tr(); case ViewLayoutPB.Chat: return LocaleKeys.chat_newChat.tr(); } diff --git a/frontend/resources/flowy_icons/16x/gallery.svg b/frontend/resources/flowy_icons/16x/gallery.svg new file mode 100644 index 0000000000000..1da994395fd13 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/gallery.svg @@ -0,0 +1 @@ +Gallery Wide Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index c464f277213d5..23b4f1255598f 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -171,6 +171,7 @@ "newDocumentText": "New document", "newGridText": "New grid", "newCalendarText": "New calendar", + "newGalleryText": "New gallery", "newBoardText": "New board", "chat": { "newChat": "AI Chat", @@ -2631,6 +2632,11 @@ "uploadFailedDescription": "The file upload failed", "uploadingDescription": "The file is being uploaded" }, + "databaseGallery": { + "menuName": "Gallery", + "referencedGalleryPrefix": "View of", + "addCard": "+ New card" + }, "gallery": { "preview": "Open in full screen", "copy": "Copy", diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index f794de6662f6b..27b2ae3ddface 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -7,7 +7,9 @@ use collab_integrate::CollabKVDB; use flowy_ai::ai_manager::AIManager; use flowy_database2::entities::DatabaseLayoutPB; use flowy_database2::services::share::csv::CSVFormat; -use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid}; +use flowy_database2::template::{ + make_default_board, make_default_calendar, make_default_gallery, make_default_grid, +}; use flowy_database2::DatabaseManager; use flowy_document::entities::DocumentDataPB; use flowy_document::manager::DocumentManager; @@ -79,7 +81,8 @@ pub fn folder_operation_handlers( let chat_folder_operation = Arc::new(ChatFolderOperation(chat_manager)); map.insert(ViewLayout::Board, database_folder_operation.clone()); map.insert(ViewLayout::Grid, database_folder_operation.clone()); - map.insert(ViewLayout::Calendar, database_folder_operation); + map.insert(ViewLayout::Calendar, database_folder_operation.clone()); + map.insert(ViewLayout::Gallery, database_folder_operation); map.insert(ViewLayout::Chat, chat_folder_operation); Arc::new(map) } @@ -430,6 +433,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { ViewLayoutPB::Board => DatabaseLayoutPB::Board, ViewLayoutPB::Calendar => DatabaseLayoutPB::Calendar, ViewLayoutPB::Grid => DatabaseLayoutPB::Grid, + ViewLayoutPB::Gallery => DatabaseLayoutPB::Gallery, ViewLayoutPB::Document | ViewLayoutPB::Chat => { return Err(FlowyError::not_support()); }, @@ -468,6 +472,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { ViewLayout::Grid => make_default_grid(view_id, &name), ViewLayout::Board => make_default_board(view_id, &name), ViewLayout::Calendar => make_default_calendar(view_id, &name), + ViewLayout::Gallery => make_default_gallery(view_id, &name), ViewLayout::Document | ViewLayout::Chat => { return Err( FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)), @@ -541,6 +546,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { ViewLayout::Grid => DatabaseLayoutPB::Grid, ViewLayout::Board => DatabaseLayoutPB::Board, ViewLayout::Calendar => DatabaseLayoutPB::Calendar, + ViewLayout::Gallery => DatabaseLayoutPB::Gallery, }; if old.layout != new.layout { diff --git a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs index 4b8f0eebe981d..744bf9261aae7 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs @@ -46,6 +46,7 @@ pub enum DatabaseLayoutPB { Grid = 0, Board = 1, Calendar = 2, + Gallery = 3, } impl std::convert::From for DatabaseLayoutPB { @@ -54,6 +55,7 @@ impl std::convert::From for DatabaseLayoutPB { DatabaseLayout::Grid => DatabaseLayoutPB::Grid, DatabaseLayout::Board => DatabaseLayoutPB::Board, DatabaseLayout::Calendar => DatabaseLayoutPB::Calendar, + DatabaseLayout::Gallery => DatabaseLayoutPB::Gallery, } } } @@ -64,6 +66,7 @@ impl std::convert::From for DatabaseLayout { DatabaseLayoutPB::Grid => DatabaseLayout::Grid, DatabaseLayoutPB::Board => DatabaseLayout::Board, DatabaseLayoutPB::Calendar => DatabaseLayout::Calendar, + DatabaseLayoutPB::Gallery => DatabaseLayout::Gallery, } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs index 93167266634d2..7d73d504c7ede 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs @@ -85,6 +85,7 @@ impl DatabaseLayoutDepsResolver { }, } }, + DatabaseLayout::Gallery => (None, None, None), } } @@ -132,6 +133,7 @@ impl DatabaseLayoutDepsResolver { database.insert_layout_setting(view_id, &self.database_layout, layout_setting); } }, + DatabaseLayout::Gallery => {}, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index d59f65ca3db27..65e1ac1eeb7a3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -831,6 +831,7 @@ impl DatabaseViewEditor { } } }, + DatabaseLayout::Gallery => {}, } layout_setting diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs index 95d70184c2c51..0196d0e4da4cd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs @@ -49,6 +49,7 @@ pub fn default_field_visibility(layout_type: DatabaseLayout) -> FieldVisibility DatabaseLayout::Grid => FieldVisibility::AlwaysShown, DatabaseLayout::Board => FieldVisibility::HideWhenEmpty, DatabaseLayout::Calendar => FieldVisibility::HideWhenEmpty, + DatabaseLayout::Gallery => FieldVisibility::AlwaysShown, } } diff --git a/frontend/rust-lib/flowy-database2/src/template.rs b/frontend/rust-lib/flowy-database2/src/template.rs index 2f93f4fd8ecf8..432b48e0c63a2 100644 --- a/frontend/rust-lib/flowy-database2/src/template.rs +++ b/frontend/rust-lib/flowy-database2/src/template.rs @@ -178,3 +178,56 @@ pub fn make_default_calendar(view_id: &str, name: &str) -> CreateDatabaseParams fields, } } + +pub fn make_default_gallery(view_id: &str, name: &str) -> CreateDatabaseParams { + let database_id = gen_database_id(); + let timestamp = timestamp(); + + // text + let text_field = FieldBuilder::from_field_type(FieldType::RichText) + .name("Title") + .primary(true) + .build(); + + // date + let date_field = FieldBuilder::from_field_type(FieldType::DateTime) + .name("Date") + .build(); + let date_field_id = date_field.id.clone(); + + // multi select + let multi_select_field = FieldBuilder::from_field_type(FieldType::MultiSelect) + .name("Tags") + .build(); + + let fields = vec![text_field, date_field, multi_select_field]; + + let field_settings = default_field_settings_for_fields(&fields, DatabaseLayout::Gallery); + + let mut layout_settings = LayoutSettings::default(); + layout_settings.insert( + DatabaseLayout::Gallery, + CalendarLayoutSetting::new(date_field_id).into(), + ); + + CreateDatabaseParams { + database_id: database_id.clone(), + inline_view_id: view_id.to_string(), + views: vec![CreateViewParams { + database_id, + view_id: view_id.to_string(), + name: name.to_string(), + layout: DatabaseLayout::Gallery, + layout_settings, + filters: vec![], + group_settings: vec![], + sorts: vec![], + field_settings, + created_at: timestamp, + modified_at: timestamp, + ..Default::default() + }], + rows: vec![], + fields, + } +} diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 9b75ea34c2eee..db17576390b9f 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -137,13 +137,14 @@ pub enum ViewLayoutPB { Board = 2, Calendar = 3, Chat = 4, + Gallery = 5, } impl ViewLayoutPB { pub fn is_database(&self) -> bool { matches!( self, - ViewLayoutPB::Grid | ViewLayoutPB::Board | ViewLayoutPB::Calendar + ViewLayoutPB::Grid | ViewLayoutPB::Board | ViewLayoutPB::Calendar | ViewLayoutPB::Gallery ) } } @@ -156,6 +157,7 @@ impl std::convert::From for ViewLayoutPB { ViewLayout::Document => ViewLayoutPB::Document, ViewLayout::Calendar => ViewLayoutPB::Calendar, ViewLayout::Chat => ViewLayoutPB::Chat, + ViewLayout::Gallery => ViewLayoutPB::Gallery, } } } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 285c817d0cb69..7e40b7d851ed9 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1044,7 +1044,9 @@ impl FolderManager { let object_id = duplicated_view.id.clone(); let collab_type = match duplicated_view.layout { ViewLayout::Document => CollabType::Document, - ViewLayout::Board | ViewLayout::Grid | ViewLayout::Calendar => CollabType::Database, + ViewLayout::Board | ViewLayout::Grid | ViewLayout::Calendar | ViewLayout::Gallery => { + CollabType::Database + }, ViewLayout::Chat => CollabType::Unknown, }; // don't block the whole import process if the view can't be encoded diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index d20c8e4e8396f..59ad6dbab9e39 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -149,6 +149,7 @@ impl From for ViewLayout { ViewLayoutPB::Board => ViewLayout::Board, ViewLayoutPB::Calendar => ViewLayout::Calendar, ViewLayoutPB::Chat => ViewLayout::Chat, + ViewLayoutPB::Gallery => ViewLayout::Gallery, } } } From d0a3474f6bffb6dfe9ac9062c225e65a42c2366a Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 20 Oct 2024 15:03:29 +0200 Subject: [PATCH 2/7] fix: use footer --- .../plugins/database/gallery/presentation/gallery_page.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart index 1366fe1e90b1a..1538904107d55 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart @@ -176,6 +176,7 @@ class _GalleryContentState extends State { type: MaterialType.transparency, child: Opacity(opacity: 0.8, child: child), ), + footer: _AddCard(itemWidth: itemWidth), children: state.rowInfos.map((rowInfo) { return SizedBox( key: ValueKey(rowInfo.rowId), @@ -198,8 +199,7 @@ class _GalleryContentState extends State { ), ), ); - }).toList() - ..add(_AddCard(itemWidth: itemWidth)), + }).toList(), ); }, ), @@ -258,8 +258,6 @@ class _AddCard extends StatelessWidget { behavior: HitTestBehavior.translucent, onTap: () => context.read().add(const GalleryEvent.createRow()), - // This disables the long press behavior of dragging the card - onLongPress: () {}, child: FlowyHover( resetHoverOnRebuild: false, builder: (context, isHovering) => SizedBox( From 9b0f0cef0b62a27a7768c865e6f18f7e338ca98f Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 20 Oct 2024 17:24:57 +0200 Subject: [PATCH 3/7] feat: add card hover actions --- .../gallery/presentation/gallery_card.dart | 109 +++++++++++------- .../gallery/presentation/gallery_page.dart | 26 ++--- .../grid/presentation/widgets/row/action.dart | 7 ++ .../plugins/database/widgets/card/card.dart | 7 +- 4 files changed, 94 insertions(+), 55 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart index bc24ee977290a..40017d44a656b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart @@ -1,12 +1,17 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart'; import 'package:appflowy/plugins/database/widgets/card/card.dart'; import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/card/container/accessory.dart'; +import 'package:appflowy/plugins/database/widgets/card/container/card_container.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,12 +47,13 @@ class GalleryCard extends StatefulWidget { } class _GalleryCardState extends State { - late final CardBloc _bloc; + final popoverController = PopoverController(); + late final CardBloc bloc; @override void initState() { super.initState(); - _bloc = CardBloc( + bloc = CardBloc( viewId: widget.viewId, fieldController: widget.controller, rowController: RowController( @@ -60,50 +66,75 @@ class _GalleryCardState extends State { @override void dispose() { - _bloc.close(); + bloc.close(); + popoverController.close(); super.dispose(); } @override Widget build(BuildContext context) { + final accessories = widget.styleConfiguration.showAccessory + ? const [MoreCardOptionsAccessory()] + : null; + return BlocProvider.value( - value: _bloc..add(const CardEvent.initial()), - child: GestureDetector( - onTap: () => widget.onTap(context), - child: Container( - constraints: const BoxConstraints(maxWidth: 200), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - border: Border.all(color: Theme.of(context).dividerColor), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - blurRadius: 4, - color: const Color(0xFF1F2329).withOpacity(0.02), - ), - BoxShadow( - blurRadius: 4, - spreadRadius: -2, - color: const Color(0xFF1F2329).withOpacity(0.02), + value: bloc..add(const CardEvent.initial()), + child: Builder( + builder: (context) { + return AppFlowyPopover( + controller: popoverController, + triggerActions: PopoverTriggerFlags.none, + constraints: BoxConstraints.loose(const Size(140, 200)), + offset: const Offset(5, 0), + popupBuilder: (_) => RowActionMenu.gallery( + viewId: widget.viewId, + rowId: bloc.rowController.rowId, + ), + child: RowCardContainer( + buildAccessoryWhen: () => + !context.watch().state.isEditing, + accessories: accessories ?? [], + openAccessory: (_) { + popoverController.show(); + }, + onTap: (_) => widget.onTap(context), + child: Container( + constraints: const BoxConstraints(maxWidth: 200), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + blurRadius: 4, + color: const Color(0xFF1F2329).withOpacity(0.02), + ), + BoxShadow( + blurRadius: 4, + spreadRadius: -2, + color: const Color(0xFF1F2329).withOpacity(0.02), + ), + ], + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CardCover( + cover: state.rowMeta.cover, + userProfile: widget.userProfile, + showDefaultCover: true, + ), + ..._makeCells(context, state.rowMeta, state.cells), + ], + ); + }, + ), ), - ], - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - CardCover( - cover: state.rowMeta.cover, - userProfile: widget.userProfile, - showDefaultCover: true, - ), - ..._makeCells(context, state.rowMeta, state.cells), - ], - ); - }, - ), - ), + ), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart index 1538904107d55..021d891a252a5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart @@ -22,6 +22,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:reorderables/reorderables.dart'; +const double _minItemWidth = 200; +const double _maxItemWidth = 350; + class GalleryPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { final _toggleExtension = ToggleExtensionNotifier(); @@ -141,16 +144,10 @@ class _GalleryContentState extends State { child: LayoutBuilder( builder: (context, constraints) { final maxWidth = constraints.maxWidth; - const double minItemWidth = 200; - const double maxItemWidth = 350; + const double spacing = 8; - final maxItemsPerRow = calculateItemsPerRow( - maxWidth, - minItemWidth, - maxItemWidth, - spacing, - ); + final maxItemsPerRow = calculateItemsPerRow(maxWidth, spacing); final itemCount = state.rowCount + 1; final itemsPerRow = @@ -180,7 +177,8 @@ class _GalleryContentState extends State { children: state.rowInfos.map((rowInfo) { return SizedBox( key: ValueKey(rowInfo.rowId), - width: itemWidth, + width: + itemWidth > _maxItemWidth ? _maxItemWidth : itemWidth, child: GalleryCard( controller: widget.controller.fieldController, userProfile: context.read().userProfile, @@ -231,15 +229,13 @@ class _GalleryContentState extends State { int calculateItemsPerRow( double maxWidth, - double minItemWidth, - double maxItemWidth, double spacing, ) { - int itemsPerRow = (maxWidth / (minItemWidth + spacing)).floor(); + int itemsPerRow = (maxWidth / (_minItemWidth + spacing)).floor(); while (itemsPerRow > 1 && ((maxWidth - (itemsPerRow - 1) * spacing) / itemsPerRow) > - maxItemWidth) { + _maxItemWidth) { itemsPerRow--; } @@ -261,7 +257,7 @@ class _AddCard extends StatelessWidget { child: FlowyHover( resetHoverOnRebuild: false, builder: (context, isHovering) => SizedBox( - width: itemWidth == double.infinity ? 175 : itemWidth, + width: itemWidth > _maxItemWidth ? _maxItemWidth : itemWidth, height: itemWidth == double.infinity ? 175 : 140, child: DottedBorder( dashPattern: const [3, 3], @@ -272,7 +268,7 @@ class _AddCard extends StatelessWidget { ), borderType: BorderType.RRect, color: isHovering - ? Theme.of(context).primaryColor + ? Theme.of(context).colorScheme.primary : Theme.of(context).hintColor, child: Center( child: FlowyText( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart index ab93967d525bf..d389ed78b10c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart @@ -28,6 +28,13 @@ class RowActionMenu extends StatelessWidget { required this.groupId, }) : actions = const [RowAction.duplicate, RowAction.delete]; + const RowActionMenu.gallery({ + super.key, + required this.viewId, + required this.rowId, + }) : groupId = null, + actions = const [RowAction.duplicate, RowAction.delete]; + final String viewId; final RowId rowId; final List actions; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart index b2ba3ecddfb87..9e10abac2a600 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart @@ -335,6 +335,9 @@ class _CardContentCellState extends State { } } +const _defaultCoverColorDark = "0xFFABABAB"; +const _defaultCoverColorLight = "0xFFE0E0E0"; + class CardCover extends StatelessWidget { const CardCover({ super.key, @@ -369,7 +372,9 @@ class CardCover extends StatelessWidget { context, RowCoverPB( coverType: CoverTypePB.ColorCover, - data: "0xFFEFEFEF", + data: Theme.of(context).brightness == Brightness.dark + ? _defaultCoverColorDark + : _defaultCoverColorLight, ), ), ); From 558fce8434ce002e734e15c2d41413b212a2ce89 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 20 Oct 2024 17:43:47 +0200 Subject: [PATCH 4/7] fix: insert default rows when creating gallery --- frontend/rust-lib/flowy-database2/src/template.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/rust-lib/flowy-database2/src/template.rs b/frontend/rust-lib/flowy-database2/src/template.rs index 432b48e0c63a2..fc43fdb0eb8af 100644 --- a/frontend/rust-lib/flowy-database2/src/template.rs +++ b/frontend/rust-lib/flowy-database2/src/template.rs @@ -214,7 +214,7 @@ pub fn make_default_gallery(view_id: &str, name: &str) -> CreateDatabaseParams { database_id: database_id.clone(), inline_view_id: view_id.to_string(), views: vec![CreateViewParams { - database_id, + database_id: database_id.clone(), view_id: view_id.to_string(), name: name.to_string(), layout: DatabaseLayout::Gallery, @@ -227,7 +227,11 @@ pub fn make_default_gallery(view_id: &str, name: &str) -> CreateDatabaseParams { modified_at: timestamp, ..Default::default() }], - rows: vec![], + rows: vec![ + CreateRowParams::new(gen_row_id(), database_id.clone()), + CreateRowParams::new(gen_row_id(), database_id.clone()), + CreateRowParams::new(gen_row_id(), database_id), + ], fields, } } From 6925eab6fc88945bc6a87fd6c5b8e3dfc160a69e Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 20 Oct 2024 17:50:58 +0200 Subject: [PATCH 5/7] fix: hover improvements when reordering --- .../gallery/presentation/gallery_page.dart | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart index 021d891a252a5..5c21383c4c90f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart @@ -130,6 +130,8 @@ class GalleryContent extends StatefulWidget { } class _GalleryContentState extends State { + bool isReordering = false; + @override Widget build(BuildContext context) { return BlocBuilder( @@ -168,12 +170,18 @@ class _GalleryContentState extends State { .read() .add(GalleryEvent.moveRow(oldIndex, newIndex)); } + setState(() => isReordering = false); }, + onReorderStarted: (_) => setState(() => isReordering = true), + onNoReorder: (_) => setState(() => isReordering = false), buildDraggableFeedback: (_, __, child) => Material( type: MaterialType.transparency, child: Opacity(opacity: 0.8, child: child), ), - footer: _AddCard(itemWidth: itemWidth), + footer: _AddCard( + itemWidth: itemWidth, + disableHover: isReordering, + ), children: state.rowInfos.map((rowInfo) { return SizedBox( key: ValueKey(rowInfo.rowId), @@ -244,9 +252,13 @@ class _GalleryContentState extends State { } class _AddCard extends StatelessWidget { - const _AddCard({required this.itemWidth}); + const _AddCard({ + required this.itemWidth, + this.disableHover = false, + }); final double itemWidth; + final bool disableHover; @override Widget build(BuildContext context) { @@ -256,6 +268,7 @@ class _AddCard extends StatelessWidget { context.read().add(const GalleryEvent.createRow()), child: FlowyHover( resetHoverOnRebuild: false, + buildWhenOnHover: () => !disableHover, builder: (context, isHovering) => SizedBox( width: itemWidth > _maxItemWidth ? _maxItemWidth : itemWidth, height: itemWidth == double.infinity ? 175 : 140, @@ -267,7 +280,7 @@ class _AddCard extends StatelessWidget { vertical: 32, ), borderType: BorderType.RRect, - color: isHovering + color: isHovering && !disableHover ? Theme.of(context).colorScheme.primary : Theme.of(context).hintColor, child: Center( From a3e280ca5ce99c548ad896c38d12e36f124809c5 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 20 Oct 2024 21:14:32 +0200 Subject: [PATCH 6/7] fix: add database settings --- .../database/gallery/presentation/gallery_page.dart | 8 ++++---- .../database/widgets/setting/database_settings_list.dart | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart index 5c21383c4c90f..28383c606d304 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart @@ -158,7 +158,8 @@ class _GalleryContentState extends State { // Calculate the width of each item in the current row configuration // TODO: Refine behavior, without the 0.0...1 buffer, resizing can cause odd behavior final totalSpacing = (itemsPerRow - 1) * spacing + 0.000001; - final itemWidth = (maxWidth - totalSpacing) / itemsPerRow; + double itemWidth = (maxWidth - totalSpacing) / itemsPerRow; + itemWidth = itemWidth.isFinite ? itemWidth : double.infinity; return ReorderableWrap( enableReorder: state.sorts.isEmpty, @@ -185,8 +186,7 @@ class _GalleryContentState extends State { children: state.rowInfos.map((rowInfo) { return SizedBox( key: ValueKey(rowInfo.rowId), - width: - itemWidth > _maxItemWidth ? _maxItemWidth : itemWidth, + width: itemWidth, child: GalleryCard( controller: widget.controller.fieldController, userProfile: context.read().userProfile, @@ -270,7 +270,7 @@ class _AddCard extends StatelessWidget { resetHoverOnRebuild: false, buildWhenOnHover: () => !disableHover, builder: (context, isHovering) => SizedBox( - width: itemWidth > _maxItemWidth ? _maxItemWidth : itemWidth, + width: itemWidth, height: itemWidth == double.infinity ? 175 : 140, child: DottedBorder( dashPattern: const [3, 3], diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart index 79d5e2410e7da..76455987f296e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart @@ -70,6 +70,7 @@ List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { DatabaseSettingAction.showCalendarLayout, ]; case DatabaseLayoutPB.Grid: + case DatabaseLayoutPB.Gallery: return [ DatabaseSettingAction.showProperties, DatabaseSettingAction.showLayout, From 20d6a230041294018ec975aa0fe8da915bd470fe Mon Sep 17 00:00:00 2001 From: Mathias Mogensen Date: Sun, 20 Oct 2024 21:30:22 +0200 Subject: [PATCH 7/7] fix: minor cleanup --- .../gallery/presentation/gallery_card.dart | 34 +++++++++++-------- .../gallery/presentation/gallery_page.dart | 3 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart index 40017d44a656b..b6e146b040c08 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart @@ -91,14 +91,11 @@ class _GalleryCardState extends State { rowId: bloc.rowController.rowId, ), child: RowCardContainer( - buildAccessoryWhen: () => - !context.watch().state.isEditing, accessories: accessories ?? [], - openAccessory: (_) { - popoverController.show(); - }, + openAccessory: (_) => popoverController.show(), onTap: (_) => widget.onTap(context), child: Container( + clipBehavior: Clip.antiAlias, constraints: const BoxConstraints(maxWidth: 200), decoration: BoxDecoration( color: Theme.of(context).cardColor, @@ -126,7 +123,17 @@ class _GalleryCardState extends State { userProfile: widget.userProfile, showDefaultCover: true, ), - ..._makeCells(context, state.rowMeta, state.cells), + Padding( + padding: widget.styleConfiguration.cardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: _makeCells( + context, + state.rowMeta, + state.cells, + ), + ), + ), ], ); }, @@ -146,15 +153,12 @@ class _GalleryCardState extends State { ) { return cells .mapIndexed( - (int index, CellMeta cellMeta) => Padding( - padding: widget.styleConfiguration.cardPadding, - child: CardContentCell( - cellBuilder: widget.cellBuilder, - cellMeta: cellMeta, - rowMeta: rowMeta, - isTitle: index == 0, - styleMap: widget.styleConfiguration.cellStyleMap, - ), + (int index, CellMeta cellMeta) => CardContentCell( + cellBuilder: widget.cellBuilder, + cellMeta: cellMeta, + rowMeta: rowMeta, + isTitle: index == 0, + styleMap: widget.styleConfiguration.cellStyleMap, ), ) .toList(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart index 28383c606d304..5077f2f6bbdba 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart @@ -156,7 +156,7 @@ class _GalleryContentState extends State { itemCount < maxItemsPerRow ? itemCount : maxItemsPerRow; // Calculate the width of each item in the current row configuration - // TODO: Refine behavior, without the 0.0...1 buffer, resizing can cause odd behavior + // Without the 0.0...1 buffer, resizing can cause odd behavior final totalSpacing = (itemsPerRow - 1) * spacing + 0.000001; double itemWidth = (maxWidth - totalSpacing) / itemsPerRow; itemWidth = itemWidth.isFinite ? itemWidth : double.infinity; @@ -195,7 +195,6 @@ class _GalleryContentState extends State { viewId: widget.viewId, rowCache: widget.controller.rowCache, styleConfiguration: RowCardStyleConfiguration( - cardPadding: const EdgeInsets.all(4), cellStyleMap: desktopBoardCardCellStyleMap(context), ), onTap: (_) => _openCard(