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..b6e146b040c08 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_card.dart @@ -0,0 +1,166 @@ +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'; + +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 { + final popoverController = PopoverController(); + 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(); + 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: 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( + accessories: accessories ?? [], + openAccessory: (_) => popoverController.show(), + onTap: (_) => widget.onTap(context), + child: Container( + clipBehavior: Clip.antiAlias, + 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, + ), + Padding( + padding: widget.styleConfiguration.cardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: _makeCells( + context, + state.rowMeta, + state.cells, + ), + ), + ), + ], + ); + }, + ), + ), + ), + ); + }, + ), + ); + } + + List _makeCells( + BuildContext context, + RowMetaPB rowMeta, + List cells, + ) { + return cells + .mapIndexed( + (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 new file mode 100644 index 0000000000000..5077f2f6bbdba --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/gallery/presentation/gallery_page.dart @@ -0,0 +1,295 @@ +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'; + +const double _minItemWidth = 200; +const double _maxItemWidth = 350; + +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 { + bool isReordering = false; + + @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 spacing = 8; + + final maxItemsPerRow = calculateItemsPerRow(maxWidth, spacing); + + final itemCount = state.rowCount + 1; + final itemsPerRow = + itemCount < maxItemsPerRow ? itemCount : maxItemsPerRow; + + // Calculate the width of each item in the current row configuration + // 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; + + 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)); + } + 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, + disableHover: isReordering, + ), + 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( + cellStyleMap: desktopBoardCardCellStyleMap(context), + ), + onTap: (_) => _openCard( + context: context, + databaseController: widget.controller, + rowMeta: rowInfo.rowMeta, + ), + ), + ); + }).toList(), + ); + }, + ), + ); + }, + ); + } + + 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 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, + this.disableHover = false, + }); + + final double itemWidth; + final bool disableHover; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => + context.read().add(const GalleryEvent.createRow()), + child: FlowyHover( + resetHoverOnRebuild: false, + buildWhenOnHover: () => !disableHover, + builder: (context, isHovering) => SizedBox( + width: 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 && !disableHover + ? Theme.of(context).colorScheme.primary + : 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/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/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..9e10abac2a600 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 @@ -334,17 +335,22 @@ class _CardContentCellState extends State<_CardContentCell> { } } +const _defaultCoverColorDark = "0xFFABABAB"; +const _defaultCoverColorLight = "0xFFE0E0E0"; + class CardCover extends StatelessWidget { const CardCover({ super.key, 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 +358,28 @@ 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: Theme.of(context).brightness == Brightness.dark + ? _defaultCoverColorDark + : _defaultCoverColorLight, + ), + ), + ); + } + return const SizedBox.shrink(); } @@ -365,9 +393,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/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, 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..fc43fdb0eb8af 100644 --- a/frontend/rust-lib/flowy-database2/src/template.rs +++ b/frontend/rust-lib/flowy-database2/src/template.rs @@ -178,3 +178,60 @@ 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: database_id.clone(), + 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![ + CreateRowParams::new(gen_row_id(), database_id.clone()), + CreateRowParams::new(gen_row_id(), database_id.clone()), + CreateRowParams::new(gen_row_id(), database_id), + ], + 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, } } }