diff --git a/frontend/appflowy_flutter/assets/translations/en.json b/frontend/appflowy_flutter/assets/translations/en.json index f19519d68c..f1aade4e48 100644 --- a/frontend/appflowy_flutter/assets/translations/en.json +++ b/frontend/appflowy_flutter/assets/translations/en.json @@ -216,7 +216,8 @@ "addFilter": "Add Filter", "deleteFilter": "Delete filter", "filterBy": "Filter by...", - "typeAValue": "Type a value..." + "typeAValue": "Type a value...", + "layout": "Layout" }, "textFilter": { "contains": "Contains", @@ -393,6 +394,12 @@ "jumpToday": "Jump to Today", "previousMonth": "Previous Month", "nextMonth": "Next Month" + }, + "settings": { + "showWeekNumbers": "Show week numbers", + "showWeekends": "Show weekends", + "firstDayOfWeek": "First day of week", + "layoutDateField": "Layout calendar by" } } } \ No newline at end of file diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index dac75a6e52..6cdbc964e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -1,4 +1,5 @@ import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart'; import 'package:appflowy/plugins/database_view/application/view/view_cache.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.dart'; @@ -18,11 +19,6 @@ import 'layout/layout_setting_listener.dart'; import 'row/row_cache.dart'; import 'group/group_listener.dart'; -typedef OnRowsChanged = void Function( - List rowInfos, - RowsChangedReason, -); - typedef OnGroupByField = void Function(List); typedef OnUpdateGroup = void Function(List); typedef OnDeleteGroup = void Function(List); @@ -52,16 +48,29 @@ class LayoutCallbacks { }); } +class CalendarLayoutCallbacks { + final void Function(LayoutSettingPB) onCalendarLayoutChanged; + + CalendarLayoutCallbacks({required this.onCalendarLayoutChanged}); +} + class DatabaseCallbacks { OnDatabaseChanged? onDatabaseChanged; - OnRowsChanged? onRowsChanged; OnFieldsChanged? onFieldsChanged; OnFiltersChanged? onFiltersChanged; + OnRowsChanged? onRowsChanged; + OnRowsDeleted? onRowsDeleted; + OnRowsUpdated? onRowsUpdated; + OnRowsCreated? onRowsCreated; + DatabaseCallbacks({ this.onDatabaseChanged, this.onRowsChanged, this.onFieldsChanged, this.onFiltersChanged, + this.onRowsUpdated, + this.onRowsDeleted, + this.onRowsCreated, }); } @@ -76,21 +85,23 @@ class DatabaseController { DatabaseCallbacks? _databaseCallbacks; GroupCallbacks? _groupCallbacks; LayoutCallbacks? _layoutCallbacks; + CalendarLayoutCallbacks? _calendarLayoutCallbacks; // Getters - List get rowInfos => _viewCache.rowInfos; RowCache get rowCache => _viewCache.rowCache; // Listener final DatabaseGroupListener groupListener; final DatabaseLayoutListener layoutListener; + final DatabaseCalendarLayoutListener calendarLayoutListener; DatabaseController({required ViewPB view, required this.layoutType}) : viewId = view.id, _databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id), fieldController = FieldController(viewId: view.id), groupListener = DatabaseGroupListener(view.id), - layoutListener = DatabaseLayoutListener(view.id) { + layoutListener = DatabaseLayoutListener(view.id), + calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) { _viewCache = DatabaseViewCache( viewId: viewId, fieldController: fieldController, @@ -99,16 +110,21 @@ class DatabaseController { _listenOnFieldsChanged(); _listenOnGroupChanged(); _listenOnLayoutChanged(); + if (layoutType == LayoutTypePB.Calendar) { + _listenOnCalendarLayoutChanged(); + } } void addListener({ DatabaseCallbacks? onDatabaseChanged, LayoutCallbacks? onLayoutChanged, GroupCallbacks? onGroupChanged, + CalendarLayoutCallbacks? onCalendarLayoutChanged, }) { _layoutCallbacks = onLayoutChanged; _databaseCallbacks = onDatabaseChanged; _groupCallbacks = onGroupChanged; + _calendarLayoutCallbacks = onCalendarLayoutChanged; } Future> open() async { @@ -218,9 +234,17 @@ class DatabaseController { } void _listenOnRowsChanged() { - _viewCache.addListener(onRowsChanged: (reason) { - _databaseCallbacks?.onRowsChanged?.call(rowInfos, reason); + final callbacks = + DatabaseViewCallbacks(onRowsChanged: (rows, rowByRowId, reason) { + _databaseCallbacks?.onRowsChanged?.call(rows, rowByRowId, reason); + }, onRowsDeleted: (ids) { + _databaseCallbacks?.onRowsDeleted?.call(ids); + }, onRowsUpdated: (ids) { + _databaseCallbacks?.onRowsUpdated?.call(ids); + }, onRowsCreated: (ids) { + _databaseCallbacks?.onRowsCreated?.call(ids); }); + _viewCache.addListener(callbacks); } void _listenOnFieldsChanged() { @@ -266,6 +290,14 @@ class DatabaseController { }, (r) => Log.error(r)); }); } + + void _listenOnCalendarLayoutChanged() { + calendarLayoutListener.start(onCalendarLayoutChanged: (result) { + result.fold((l) { + _calendarLayoutCallbacks?.onCalendarLayoutChanged(l); + }, (r) => Log.error(r)); + }); + } } class RowDataBuilder { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart index d724028e63..e45a428db7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -10,9 +10,14 @@ import 'row/row_cache.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnFiltersChanged = void Function(List); typedef OnDatabaseChanged = void Function(DatabasePB); + +typedef OnRowsCreated = void Function(List ids); +typedef OnRowsUpdated = void Function(List ids); +typedef OnRowsDeleted = void Function(List ids); typedef OnRowsChanged = void Function( - List, - RowsChangedReason, + UnmodifiableListView rows, + UnmodifiableMapView rowByRowId, + RowsChangedReason reason, ); typedef OnError = void Function(FlowyError); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart new file mode 100644 index 0000000000..9ec8c1f656 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/layout/calendar_setting_listener.dart @@ -0,0 +1,49 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/grid_notification.dart'; +import 'package:flowy_infra/notifier.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:dartz/dartz.dart'; + +typedef NewLayoutFieldValue = Either; + +class DatabaseCalendarLayoutListener { + final String viewId; + PublishNotifier? _newLayoutFieldNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + DatabaseCalendarLayoutListener(this.viewId); + + void start( + {required void Function(NewLayoutFieldValue) onCalendarLayoutChanged}) { + _newLayoutFieldNotifier?.addPublishListener(onCalendarLayoutChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidSetNewLayoutField: + result.fold( + (payload) => _newLayoutFieldNotifier?.value = + left(LayoutSettingPB.fromBuffer(payload)), + (error) => _newLayoutFieldNotifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _newLayoutFieldNotifier?.dispose(); + _newLayoutFieldNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index 7f0265ccc7..608cc7b907 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -37,11 +37,15 @@ class RowCache { final RowCacheDelegate _delegate; final RowChangesetNotifier _rowChangeReasonNotifier; - UnmodifiableListView get visibleRows { + UnmodifiableListView get rowInfos { var visibleRows = [..._rowList.rows]; return UnmodifiableListView(visibleRows); } + UnmodifiableMapView get rowByRowId { + return UnmodifiableMapView(_rowList.rowInfoByRowId); + } + CellCache get cellCache => _cellCache; RowCache({ @@ -61,6 +65,10 @@ class RowCache { }); } + RowInfo? getRow(String rowId) { + return _rowList.get(rowId); + } + void setInitialRows(List rows) { for (final row in rows) { final rowInfo = buildGridRow(row); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart index bd163cce58..f6d4114495 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_list.dart @@ -9,14 +9,14 @@ class RowList { List get rows => List.from(_rowInfos); /// Use Map for faster access the raw row data. - final HashMap _rowInfoByRowId = HashMap(); + final HashMap rowInfoByRowId = HashMap(); RowInfo? get(String rowId) { - return _rowInfoByRowId[rowId]; + return rowInfoByRowId[rowId]; } int? indexOfRow(String rowId) { - final rowInfo = _rowInfoByRowId[rowId]; + final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { return _rowInfos.indexOf(rowInfo); } @@ -33,7 +33,7 @@ class RowList { } else { _rowInfos.add(rowInfo); } - _rowInfoByRowId[rowId] = rowInfo; + rowInfoByRowId[rowId] = rowInfo; } InsertedIndex? insert(int index, RowInfo rowInfo) { @@ -47,21 +47,21 @@ class RowList { if (oldRowInfo != null) { _rowInfos.insert(insertedIndex, rowInfo); _rowInfos.remove(oldRowInfo); - _rowInfoByRowId[rowId] = rowInfo; + rowInfoByRowId[rowId] = rowInfo; return null; } else { _rowInfos.insert(insertedIndex, rowInfo); - _rowInfoByRowId[rowId] = rowInfo; + rowInfoByRowId[rowId] = rowInfo; return InsertedIndex(index: insertedIndex, rowId: rowId); } } DeletedIndex? remove(String rowId) { - final rowInfo = _rowInfoByRowId[rowId]; + final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { final index = _rowInfos.indexOf(rowInfo); if (index != -1) { - _rowInfoByRowId.remove(rowInfo.rowPB.id); + rowInfoByRowId.remove(rowInfo.rowPB.id); _rowInfos.remove(rowInfo); } return DeletedIndex(index: index, rowInfo: rowInfo); @@ -105,7 +105,7 @@ class RowList { if (deletedRowByRowId[rowInfo.rowPB.id] == null) { newRows.add(rowInfo); } else { - _rowInfoByRowId.remove(rowInfo.rowPB.id); + rowInfoByRowId.remove(rowInfo.rowPB.id); deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo)); } }); @@ -136,7 +136,7 @@ class RowList { _rowInfos.clear(); for (final rowId in rowIds) { - final rowInfo = _rowInfoByRowId[rowId]; + final rowInfo = rowInfoByRowId[rowId]; if (rowInfo != null) { _rowInfos.add(rowInfo); } @@ -155,6 +155,6 @@ class RowList { } bool contains(String rowId) { - return _rowInfoByRowId[rowId] != null; + return rowInfoByRowId[rowId] != null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart index 6f58c3fbde..002bc40ec0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart @@ -1,22 +1,50 @@ import 'dart:async'; +import 'dart:collection'; import 'package:appflowy_backend/log.dart'; +import '../defines.dart'; import '../field/field_controller.dart'; import '../row/row_cache.dart'; import 'view_listener.dart'; +class DatabaseViewCallbacks { + /// Will get called when number of rows were changed that includes + /// update/delete/insert rows. The [onRowsChanged] will return all + /// the rows of the current database + final OnRowsChanged? onRowsChanged; + + // Will get called when creating new rows + final OnRowsCreated? onRowsCreated; + + /// Will get called when number of rows were updated + final OnRowsUpdated? onRowsUpdated; + + /// Will get called when number of rows were deleted + final OnRowsDeleted? onRowsDeleted; + + const DatabaseViewCallbacks({ + this.onRowsChanged, + this.onRowsCreated, + this.onRowsUpdated, + this.onRowsDeleted, + }); +} + /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information class DatabaseViewCache { final String viewId; late RowCache _rowCache; - final DatabaseViewListener _gridViewListener; + final DatabaseViewListener _databaseViewListener; + DatabaseViewCallbacks? _callbacks; - List get rowInfos => _rowCache.visibleRows; + UnmodifiableListView get rowInfos => _rowCache.rowInfos; RowCache get rowCache => _rowCache; + RowInfo? getRow(String rowId) => _rowCache.getRow(rowId); + DatabaseViewCache({ required this.viewId, required FieldController fieldController, - }) : _gridViewListener = DatabaseViewListener(viewId: viewId) { + }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) { final delegate = RowDelegatesImpl(fieldController); _rowCache = RowCache( viewId: viewId, @@ -24,10 +52,28 @@ class DatabaseViewCache { cacheDelegate: delegate, ); - _gridViewListener.start( + _databaseViewListener.start( onRowsChanged: (result) { result.fold( - (changeset) => _rowCache.applyRowsChanged(changeset), + (changeset) { + // Update the cache + _rowCache.applyRowsChanged(changeset); + + if (changeset.deletedRows.isNotEmpty) { + _callbacks?.onRowsDeleted?.call(changeset.deletedRows); + } + + if (changeset.updatedRows.isNotEmpty) { + _callbacks?.onRowsUpdated + ?.call(changeset.updatedRows.map((e) => e.row.id).toList()); + } + + if (changeset.insertedRows.isNotEmpty) { + _callbacks?.onRowsCreated?.call(changeset.insertedRows + .map((insertedRow) => insertedRow.row.id) + .toList()); + } + }, (err) => Log.error(err), ); }, @@ -50,23 +96,22 @@ class DatabaseViewCache { ); }, ); + + _rowCache.onRowsChanged( + (reason) => _callbacks?.onRowsChanged?.call( + rowInfos, + _rowCache.rowByRowId, + reason, + ), + ); } Future dispose() async { - await _gridViewListener.stop(); + await _databaseViewListener.stop(); await _rowCache.dispose(); } - void addListener({ - required void Function(RowsChangedReason) onRowsChanged, - bool Function()? listenWhen, - }) { - _rowCache.onRowsChanged((reason) { - if (listenWhen != null && listenWhen() == false) { - return; - } - - onRowsChanged(reason); - }); + void addListener(DatabaseViewCallbacks callbacks) { + _callbacks = callbacks; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index bf0e1ab8f6..f0cfb43b48 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -17,9 +17,11 @@ part 'calendar_bloc.freezed.dart'; class CalendarBloc extends Bloc { final DatabaseController _databaseController; + Map fieldInfoByFieldId = {}; // Getters String get viewId => _databaseController.viewId; + FieldController get fieldController => _databaseController.fieldController; CellCache get cellCache => _databaseController.rowCache.cellCache; RowCache get rowCache => _databaseController.rowCache; @@ -28,7 +30,7 @@ class CalendarBloc extends Bloc { view: view, layoutType: LayoutTypePB.Calendar, ), - super(CalendarState.initial(view.id)) { + super(CalendarState.initial()) { on( (event, emit) async { await event.when( @@ -44,16 +46,49 @@ class CalendarBloc extends Bloc { emit(state.copyWith(database: Some(database))); }, didLoadAllEvents: (events) { - emit(state.copyWith(events: events)); + emit(state.copyWith(initialEvents: events, allEvents: events)); + }, + didReceiveNewLayoutField: (CalendarLayoutSettingsPB layoutSettings) { + _loadAllEvents(); + emit(state.copyWith(settings: Some(layoutSettings))); }, createEvent: (DateTime date, String title) async { await _createEvent(date, title); }, - didReceiveEvent: (CalendarEventData newEvent) { - emit(state.copyWith(events: [...state.events, newEvent])); + updateCalendarLayoutSetting: + (CalendarLayoutSettingsPB layoutSetting) async { + await _updateCalendarLayoutSetting(layoutSetting); }, - didUpdateFieldInfos: (Map fieldInfoByFieldId) { - emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId)); + didUpdateEvent: (CalendarEventData eventData) { + var allEvents = [...state.allEvents]; + final index = allEvents.indexWhere( + (element) => element.event!.cellId == eventData.event!.cellId, + ); + if (index != -1) { + allEvents[index] = eventData; + } + emit(state.copyWith( + allEvents: allEvents, + updateEvent: eventData, + )); + }, + didReceiveNewEvent: (CalendarEventData event) { + emit(state.copyWith( + allEvents: [...state.allEvents, event], + newEvent: event, + )); + }, + didDeleteEvents: (List deletedRowIds) { + var events = [...state.allEvents]; + events.retainWhere( + (element) => !deletedRowIds.contains(element.event!.cellId.rowId), + ); + emit( + state.copyWith( + allEvents: events, + deleteEventIds: deletedRowIds, + ), + ); }, ); }, @@ -97,7 +132,7 @@ class CalendarBloc extends Bloc { } Future _createEvent(DateTime date, String title) async { - state.settings.fold( + return state.settings.fold( () => null, (settings) async { final dateField = _getCalendarFieldInfo(settings.layoutFieldId); @@ -110,8 +145,8 @@ class CalendarBloc extends Bloc { }, ); - result.fold( - (newRow) => _loadEvent(newRow.id), + return result.fold( + (newRow) {}, (err) => Log.error(err), ); } @@ -119,17 +154,23 @@ class CalendarBloc extends Bloc { ); } - Future _loadEvent(String rowId) async { + Future _updateCalendarLayoutSetting( + CalendarLayoutSettingsPB layoutSetting) async { + return _databaseController.updateCalenderLayoutSetting(layoutSetting); + } + + Future?> _loadEvent(String rowId) async { final payload = RowIdPB(viewId: viewId, rowId: rowId); - DatabaseEventGetCalendarEvent(payload).send().then((result) { - result.fold( + return DatabaseEventGetCalendarEvent(payload).send().then((result) { + return result.fold( (eventPB) { final calendarEvent = _calendarEventDataFromEventPB(eventPB); - if (calendarEvent != null) { - add(CalendarEvent.didReceiveEvent(calendarEvent)); - } + return calendarEvent; + }, + (r) { + Log.error(r); + return null; }, - (r) => Log.error(r), ); }); } @@ -140,7 +181,7 @@ class CalendarBloc extends Bloc { result.fold( (events) { if (!isClosed) { - final calendarEvents = >[]; + final calendarEvents = >[]; for (final eventPB in events.items) { final calendarEvent = _calendarEventDataFromEventPB(eventPB); if (calendarEvent != null) { @@ -156,9 +197,9 @@ class CalendarBloc extends Bloc { }); } - CalendarEventData? _calendarEventDataFromEventPB( + CalendarEventData? _calendarEventDataFromEventPB( CalendarEventPB eventPB) { - final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId]; + final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId]; if (fieldInfo != null) { final cellId = CellIdentifier( viewId: viewId, @@ -166,7 +207,7 @@ class CalendarBloc extends Bloc { fieldInfo: fieldInfo, ); - final eventData = CalendarCardData( + final eventData = CalendarDayEvent( event: eventPB, cellId: cellId, ); @@ -192,10 +233,31 @@ class CalendarBloc extends Bloc { }, onFieldsChanged: (fieldInfos) { if (isClosed) return; - final fieldInfoByFieldId = { + fieldInfoByFieldId = { for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo }; - add(CalendarEvent.didUpdateFieldInfos(fieldInfoByFieldId)); + }, + onRowsChanged: ((onRowsChanged, rowByRowId, reason) {}), + onRowsCreated: ((ids) async { + for (final id in ids) { + final event = await _loadEvent(id); + if (event != null && !isClosed) { + add(CalendarEvent.didReceiveNewEvent(event)); + } + } + }), + onRowsDeleted: (ids) { + if (isClosed) return; + add(CalendarEvent.didDeleteEvents(ids)); + }, + onRowsUpdated: (ids) async { + if (isClosed) return; + for (final id in ids) { + final event = await _loadEvent(id); + if (event != null) { + add(CalendarEvent.didUpdateEvent(event)); + } + } }, ); @@ -204,9 +266,13 @@ class CalendarBloc extends Bloc { onLoadLayout: _didReceiveLayoutSetting, ); + final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks( + onCalendarLayoutChanged: _didReceiveNewLayoutField); + _databaseController.addListener( onDatabaseChanged: onDatabaseChanged, onLayoutChanged: onLayoutChanged, + onCalendarLayoutChanged: onCalendarLayoutFieldChanged, ); } @@ -216,44 +282,75 @@ class CalendarBloc extends Bloc { add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar)); } } + + void _didReceiveNewLayoutField(LayoutSettingPB layoutSetting) { + if (layoutSetting.hasCalendar()) { + if (isClosed) return; + add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar)); + } + } } -typedef Events = List>; +typedef Events = List>; @freezed class CalendarEvent with _$CalendarEvent { const factory CalendarEvent.initial() = _InitialCalendar; + + // Called after loading the calendar layout setting from the backend const factory CalendarEvent.didReceiveCalendarSettings( CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings; + + // Called after loading all the current evnets const factory CalendarEvent.didLoadAllEvents(Events events) = _ReceiveCalendarEvents; - const factory CalendarEvent.didReceiveEvent( - CalendarEventData event) = _ReceiveEvent; - const factory CalendarEvent.didUpdateFieldInfos( - Map fieldInfoByFieldId) = _DidUpdateFieldInfos; + + // Called when specific event was updated + const factory CalendarEvent.didUpdateEvent( + CalendarEventData event) = _DidUpdateEvent; + + // Called after creating a new event + const factory CalendarEvent.didReceiveNewEvent( + CalendarEventData event) = _DidReceiveNewEvent; + + // Called when deleting events + const factory CalendarEvent.didDeleteEvents(List rowIds) = + _DidDeleteEvents; + + // Called when creating a new event const factory CalendarEvent.createEvent(DateTime date, String title) = _CreateEvent; + + // Called when updating the calendar's layout settings + const factory CalendarEvent.updateCalendarLayoutSetting( + CalendarLayoutSettingsPB layoutSetting) = _UpdateCalendarLayoutSetting; + const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) = _ReceiveDatabaseUpdate; + + const factory CalendarEvent.didReceiveNewLayoutField( + CalendarLayoutSettingsPB layoutSettings) = _DidReceiveNewLayoutField; } @freezed class CalendarState with _$CalendarState { const factory CalendarState({ - required String databaseId, required Option database, - required Events events, - required Map fieldInfoByFieldId, + required Events allEvents, + required Events initialEvents, + CalendarEventData? newEvent, + required List deleteEventIds, + CalendarEventData? updateEvent, required Option settings, required DatabaseLoadingState loadingState, required Option noneOrError, }) = _CalendarState; - factory CalendarState.initial(String databaseId) => CalendarState( + factory CalendarState.initial() => CalendarState( database: none(), - databaseId: databaseId, - fieldInfoByFieldId: {}, - events: [], + allEvents: [], + initialEvents: [], + deleteEventIds: [], settings: none(), noneOrError: none(), loadingState: const _Loading(), @@ -277,8 +374,10 @@ class CalendarEditingRow { }); } -class CalendarCardData { +class CalendarDayEvent { final CalendarEventPB event; final CellIdentifier cellId; - CalendarCardData({required this.cellId, required this.event}); + + String get eventId => cellId.rowId; + CalendarDayEvent({required this.cellId, required this.event}); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart new file mode 100644 index 0000000000..1ebc97d90a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_setting_bloc.dart @@ -0,0 +1,55 @@ +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:bloc/bloc.dart'; +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'calendar_setting_bloc.freezed.dart'; + +typedef DayOfWeek = int; + +class CalendarSettingBloc + extends Bloc { + CalendarSettingBloc({required CalendarLayoutSettingsPB? layoutSettings}) + : super(CalendarSettingState.initial(layoutSettings)) { + on((event, emit) { + event.when( + performAction: (action) { + emit(state.copyWith(selectedAction: Some(action))); + }, + updateLayoutSetting: (setting) { + emit(state.copyWith(layoutSetting: Some(setting))); + }, + ); + }); + } + + @override + Future close() async => super.close(); +} + +@freezed +class CalendarSettingState with _$CalendarSettingState { + const factory CalendarSettingState({ + required Option selectedAction, + required Option layoutSetting, + }) = _CalendarSettingState; + + factory CalendarSettingState.initial( + CalendarLayoutSettingsPB? layoutSettings) => + CalendarSettingState( + selectedAction: none(), + layoutSetting: layoutSettings == null ? none() : Some(layoutSettings), + ); +} + +@freezed +class CalendarSettingEvent with _$CalendarSettingEvent { + const factory CalendarSettingEvent.performAction( + CalendarSettingAction action) = _PerformAction; + const factory CalendarSettingEvent.updateLayoutSetting( + CalendarLayoutSettingsPB setting) = _UpdateLayoutSetting; +} + +enum CalendarSettingAction { + layout, +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart index a7ce510565..1ddfb7a64f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/calendar.dart @@ -34,7 +34,7 @@ class CalendarPluginBuilder extends PluginBuilder { class CalendarPluginConfig implements PluginConfig { @override - bool get creatable => false; + bool get creatable => true; } class CalendarPlugin extends Plugin { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart new file mode 100644 index 0000000000..3a71c2c4ff --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_day.dart @@ -0,0 +1,267 @@ +import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/cells/text_card_cell.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/field_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.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:provider/provider.dart'; + +import '../../grid/presentation/layout/sizes.dart'; +import '../application/calendar_bloc.dart'; + +class CalendarDayCard extends StatelessWidget { + final String viewId; + final bool isToday; + final bool isInMonth; + final DateTime date; + final RowCache _rowCache; + final CardCellBuilder _cellBuilder; + final List events; + final void Function(DateTime) onCreateEvent; + + CalendarDayCard({ + required this.viewId, + required this.isToday, + required this.isInMonth, + required this.date, + required this.onCreateEvent, + required RowCache rowCache, + required this.events, + Key? key, + }) : _rowCache = rowCache, + _cellBuilder = CardCellBuilder(rowCache.cellCache), + super(key: key); + + @override + Widget build(BuildContext context) { + Color backgroundColor = Theme.of(context).colorScheme.surface; + if (!isInMonth) { + backgroundColor = AFThemeExtension.of(context).lightGreyHover; + } + + return ChangeNotifierProvider( + create: (_) => _CardEnterNotifier(), + builder: ((context, child) { + final children = events.map((event) { + return _DayEventCell( + event: event, + viewId: viewId, + onClick: () => _showRowDetailPage(event, context), + child: _cellBuilder.buildCell( + cellId: event.cellId, + styles: {FieldType.RichText: TextCardCellStyle(10)}, + ), + ); + }).toList(); + + final child = Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _Header( + date: date, + isInMonth: isInMonth, + isToday: isToday, + onCreate: () => onCreateEvent(date), + ), + VSpace(GridSize.typeOptionSeparatorHeight), + Flexible( + child: ListView.separated( + itemBuilder: (BuildContext context, int index) { + return children[index]; + }, + itemCount: children.length, + separatorBuilder: (BuildContext context, int index) => + VSpace(GridSize.typeOptionSeparatorHeight), + ), + ), + ], + )); + + return Container( + color: backgroundColor, + child: MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (p) => notifyEnter(context, true), + onExit: (p) => notifyEnter(context, false), + child: child, + ), + ); + }), + ); + } + + void _showRowDetailPage(CalendarDayEvent event, BuildContext context) { + final dataController = RowController( + rowId: event.cellId.rowId, + viewId: viewId, + rowCache: _rowCache, + ); + + FlowyOverlay.show( + context: context, + builder: (BuildContext context) { + return RowDetailPage( + cellBuilder: GridCellBuilder( + cellCache: _rowCache.cellCache, + ), + dataController: dataController, + ); + }, + ); + } + + notifyEnter(BuildContext context, bool isEnter) { + Provider.of<_CardEnterNotifier>( + context, + listen: false, + ).onEnter = isEnter; + } +} + +class _DayEventCell extends StatelessWidget { + final String viewId; + final CalendarDayEvent event; + final VoidCallback onClick; + final Widget child; + const _DayEventCell({ + required this.viewId, + required this.event, + required this.onClick, + required this.child, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyHover( + child: GestureDetector( + onTap: onClick, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: child, + ), + ), + ); + } +} + +class _Header extends StatelessWidget { + final bool isToday; + final bool isInMonth; + final DateTime date; + final VoidCallback onCreate; + const _Header({ + required this.isToday, + required this.isInMonth, + required this.date, + required this.onCreate, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer<_CardEnterNotifier>( + builder: (context, notifier, _) { + final badge = _DayBadge( + isToday: isToday, + isInMonth: isInMonth, + date: date, + ); + return Row( + children: [ + if (notifier.onEnter) _NewEventButton(onClick: onCreate), + const Spacer(), + badge, + ], + ); + }, + ); + } +} + +class _NewEventButton extends StatelessWidget { + final VoidCallback onClick; + const _NewEventButton({ + required this.onClick, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyIconButton( + onPressed: onClick, + iconPadding: EdgeInsets.zero, + icon: svgWidget( + "home/add", + color: Theme.of(context).colorScheme.onSurface, + ), + width: 22, + ); + } +} + +class _DayBadge extends StatelessWidget { + final bool isToday; + final bool isInMonth; + final DateTime date; + const _DayBadge({ + required this.isToday, + required this.isInMonth, + required this.date, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + Color dayTextColor = Theme.of(context).colorScheme.onSurface; + String dayString = date.day == 1 + ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date) + : date.day.toString(); + + if (isToday) { + dayTextColor = Theme.of(context).colorScheme.onPrimary; + } + if (!isInMonth) { + dayTextColor = Theme.of(context).disabledColor; + } + + Widget day = Container( + decoration: BoxDecoration( + color: isToday ? Theme.of(context).colorScheme.primary : null, + borderRadius: Corners.s6Border, + ), + padding: GridSize.typeOptionContentInsets, + child: FlowyText.medium( + dayString, + color: dayTextColor, + ), + ); + + return day; + } +} + +class _CardEnterNotifier extends ChangeNotifier { + bool _onEnter = false; + + _CardEnterNotifier(); + + set onEnter(bool value) { + if (_onEnter != value) { + _onEnter = value; + notifyListeners(); + } + } + + bool get onEnter => _onEnter; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index 0eac7870a3..988654a8ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -1,22 +1,15 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart'; -import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.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:provider/provider.dart'; -import '../../grid/presentation/layout/sizes.dart'; -import '../../widgets/row/cell_builder.dart'; -import '../../widgets/row/row_detail.dart'; +import 'calendar_day.dart'; import 'layout/sizes.dart'; import 'toolbar/calendar_toolbar.dart'; @@ -29,7 +22,7 @@ class CalendarPage extends StatefulWidget { } class _CalendarPageState extends State { - final _eventController = EventController(); + final _eventController = EventController(); GlobalKey? _calendarState; late CalendarBloc _calendarBloc; @@ -58,21 +51,55 @@ class _CalendarPageState extends State { value: _calendarBloc, ) ], - child: BlocListener( - listenWhen: (previous, current) => previous.events != current.events, - listener: (context, state) { - if (state.events.isNotEmpty) { - _eventController.removeWhere((element) => true); - _eventController.addAll(state.events); - } - }, + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.initialEvents != c.initialEvents, + listener: (context, state) { + _eventController.removeWhere((_) => true); + _eventController.addAll(state.initialEvents); + }, + ), + BlocListener( + listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds, + listener: (context, state) { + _eventController.removeWhere( + (element) => + state.deleteEventIds.contains(element.event!.eventId), + ); + }, + ), + BlocListener( + listenWhen: (p, c) => p.updateEvent != c.updateEvent, + listener: (context, state) { + if (state.updateEvent != null) { + _eventController.removeWhere((element) => + state.updateEvent!.event!.eventId == + element.event!.eventId); + _eventController.add(state.updateEvent!); + } + }, + ), + BlocListener( + listenWhen: (p, c) => p.newEvent != c.newEvent, + listener: (context, state) { + if (state.newEvent != null) { + _eventController.add(state.newEvent!); + } + }, + ), + ], child: BlocBuilder( builder: (context, state) { return Column( children: [ // const _ToolbarBlocAdaptor(), - _toolbar(), - _buildCalendar(_eventController), + const CalendarToolbar(), + _buildCalendar( + _eventController, + state.settings + .foldLeft(0, (previous, a) => a.firstDayOfWeek), + ), ], ); }, @@ -82,16 +109,13 @@ class _CalendarPageState extends State { ); } - Widget _toolbar() { - return const CalendarToolbar(); - } - - Widget _buildCalendar(EventController eventController) { + Widget _buildCalendar(EventController eventController, int firstDayOfWeek) { return Expanded( child: MonthView( key: _calendarState, controller: _eventController, - cellAspectRatio: 1.75, + cellAspectRatio: .9, + startDay: _weekdayFromInt(firstDayOfWeek), borderColor: Theme.of(context).dividerColor, headerBuilder: _headerNavigatorBuilder, weekDayBuilder: _headerWeekDayBuilder, @@ -154,47 +178,19 @@ class _CalendarPageState extends State { Widget _calendarDayBuilder( DateTime date, - List> calenderEvents, + List> calenderEvents, isToday, isInMonth, ) { - final builder = CardCellBuilder(_calendarBloc.cellCache); - final cells = calenderEvents.map((value) => value.event!).map((event) { - final child = builder.buildCell(cellId: event.cellId); + final events = calenderEvents.map((value) => value.event!).toList(); - return FlowyHover( - child: GestureDetector( - onTap: () { - final dataController = RowController( - rowId: event.cellId.rowId, - viewId: widget.view.id, - rowCache: _calendarBloc.rowCache, - ); - - FlowyOverlay.show( - context: context, - builder: (BuildContext context) { - return RowDetailPage( - cellBuilder: - GridCellBuilder(cellCache: _calendarBloc.cellCache), - dataController: dataController, - ); - }, - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: child, - ), - ), - ); - }).toList(); - - return _CalendarCard( + return CalendarDayCard( + viewId: widget.view.id, isToday: isToday, isInMonth: isInMonth, + events: events, date: date, - children: cells, + rowCache: _calendarBloc.rowCache, onCreateEvent: (date) { _calendarBloc.add( CalendarEvent.createEvent( @@ -205,175 +201,9 @@ class _CalendarPageState extends State { }, ); } -} -class _CalendarCard extends StatelessWidget { - final bool isToday; - final bool isInMonth; - final DateTime date; - final List children; - final void Function(DateTime) onCreateEvent; - - const _CalendarCard({ - required this.isToday, - required this.isInMonth, - required this.date, - required this.children, - required this.onCreateEvent, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - Color backgroundColor = Theme.of(context).colorScheme.surface; - if (!isInMonth) { - backgroundColor = AFThemeExtension.of(context).lightGreyHover; - } - - return ChangeNotifierProvider( - create: (_) => _CardEnterNotifier(), - builder: ((context, child) { - return Container( - color: backgroundColor, - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (p) => notifyEnter(context, true), - onExit: (p) => notifyEnter(context, false), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - _Header( - date: date, - isInMonth: isInMonth, - isToday: isToday, - onCreate: () => onCreateEvent(date), - ), - ...children - ], - ), - ), - ), - ); - }), - ); - } - - notifyEnter(BuildContext context, bool isEnter) { - Provider.of<_CardEnterNotifier>( - context, - listen: false, - ).onEnter = isEnter; + WeekDays _weekdayFromInt(int dayOfWeek) { + // MonthView places the first day of week on the second column for some reason. + return WeekDays.values[(dayOfWeek + 1) % 7]; } } - -class _Header extends StatelessWidget { - final bool isToday; - final bool isInMonth; - final DateTime date; - final VoidCallback onCreate; - const _Header({ - required this.isToday, - required this.isInMonth, - required this.date, - required this.onCreate, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Consumer<_CardEnterNotifier>( - builder: (context, notifier, _) { - final badge = _DayBadge( - isToday: isToday, - isInMonth: isInMonth, - date: date, - ); - return Row( - children: [ - if (notifier.onEnter) _NewEventButton(onClick: onCreate), - const Spacer(), - badge, - ], - ); - }, - ); - } -} - -class _NewEventButton extends StatelessWidget { - final VoidCallback onClick; - const _NewEventButton({ - required this.onClick, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - onPressed: onClick, - iconPadding: EdgeInsets.zero, - icon: svgWidget( - "home/add", - color: Theme.of(context).colorScheme.onSurface, - ), - width: 22, - ); - } -} - -class _DayBadge extends StatelessWidget { - final bool isToday; - final bool isInMonth; - final DateTime date; - const _DayBadge({ - required this.isToday, - required this.isInMonth, - required this.date, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - Color dayTextColor = Theme.of(context).colorScheme.onSurface; - String dayString = date.day == 1 - ? DateFormat('MMM d', context.locale.toLanguageTag()).format(date) - : date.day.toString(); - - if (isToday) { - dayTextColor = Theme.of(context).colorScheme.onPrimary; - } - if (!isInMonth) { - dayTextColor = Theme.of(context).disabledColor; - } - - Widget day = Container( - decoration: BoxDecoration( - color: isToday ? Theme.of(context).colorScheme.primary : null, - borderRadius: Corners.s6Border, - ), - padding: GridSize.typeOptionContentInsets, - child: FlowyText.medium( - dayString, - color: dayTextColor, - ), - ); - - return day; - } -} - -class _CardEnterNotifier extends ChangeNotifier { - bool _onEnter = false; - - _CardEnterNotifier(); - - set onEnter(bool value) { - if (_onEnter != value) { - _onEnter = value; - notifyListeners(); - } - } - - bool get onEnter => _onEnter; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart new file mode 100644 index 0000000000..ff69888813 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -0,0 +1,410 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart' + hide DateFormat; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:protobuf/protobuf.dart'; + +import 'calendar_setting.dart'; + +class CalendarLayoutSetting extends StatefulWidget { + final CalendarSettingContext settingContext; + final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated; + + const CalendarLayoutSetting({ + required this.onUpdated, + required this.settingContext, + super.key, + }); + + @override + State createState() => _CalendarLayoutSettingState(); +} + +class _CalendarLayoutSettingState extends State { + late final PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final CalendarLayoutSettingsPB? settings = state.layoutSetting + .foldLeft(null, (previous, settings) => settings); + + if (settings == null) { + return const CircularProgressIndicator(); + } + final availableSettings = _availableCalendarSettings(settings); + + final items = availableSettings.map((setting) { + switch (setting) { + case CalendarLayoutSettingAction.showWeekNumber: + return ShowWeekNumber( + showWeekNumbers: settings.showWeekNumbers, + onUpdated: (showWeekNumbers) { + _updateLayoutSettings( + context, + showWeekNumbers: showWeekNumbers, + onUpdated: widget.onUpdated, + ); + }, + ); + case CalendarLayoutSettingAction.showWeekends: + return ShowWeekends( + showWeekends: settings.showWeekends, + onUpdated: (showWeekends) { + _updateLayoutSettings( + context, + showWeekends: showWeekends, + onUpdated: widget.onUpdated, + ); + }, + ); + case CalendarLayoutSettingAction.firstDayOfWeek: + return FirstDayOfWeek( + firstDayOfWeek: settings.firstDayOfWeek, + popoverMutex: popoverMutex, + onUpdated: (firstDayOfWeek) { + _updateLayoutSettings( + context, + onUpdated: widget.onUpdated, + firstDayOfWeek: firstDayOfWeek, + ); + }, + ); + case CalendarLayoutSettingAction.layoutField: + return LayoutDateField( + fieldController: widget.settingContext.fieldController, + viewId: widget.settingContext.viewId, + fieldId: settings.layoutFieldId, + popoverMutex: popoverMutex, + onUpdated: (fieldId) { + _updateLayoutSettings(context, + onUpdated: widget.onUpdated, layoutFieldId: fieldId); + }, + ); + default: + return ShowWeekends( + showWeekends: settings.showWeekends, + onUpdated: (showWeekends) { + _updateLayoutSettings(context, + onUpdated: widget.onUpdated, showWeekends: showWeekends); + }, + ); + } + }).toList(); + + return SizedBox( + width: 200, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: items.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) => items[index], + padding: const EdgeInsets.all(6.0), + ), + ); + }, + ); + } + + List _availableCalendarSettings( + CalendarLayoutSettingsPB layoutSettings) { + List settings = [ + CalendarLayoutSettingAction.layoutField, + // CalendarLayoutSettingAction.layoutType, + // CalendarLayoutSettingAction.showWeekNumber, + ]; + + switch (layoutSettings.layoutTy) { + case CalendarLayoutPB.DayLayout: + // settings.add(CalendarLayoutSettingAction.showTimeLine); + break; + case CalendarLayoutPB.MonthLayout: + settings.addAll([ + // CalendarLayoutSettingAction.showWeekends, + // if (layoutSettings.showWeekends) + CalendarLayoutSettingAction.firstDayOfWeek, + ]); + break; + case CalendarLayoutPB.WeekLayout: + settings.addAll([ + // CalendarLayoutSettingAction.showWeekends, + // if (layoutSettings.showWeekends) + CalendarLayoutSettingAction.firstDayOfWeek, + // CalendarLayoutSettingAction.showTimeLine, + ]); + break; + } + + return settings; + } + + void _updateLayoutSettings( + BuildContext context, { + required Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated, + bool? showWeekends, + bool? showWeekNumbers, + int? firstDayOfWeek, + String? layoutFieldId, + }) { + CalendarLayoutSettingsPB setting = context + .read() + .state + .layoutSetting + .foldLeft(null, (previous, settings) => settings)!; + setting.freeze(); + setting = setting.rebuild((setting) { + if (showWeekends != null) { + setting.showWeekends = !showWeekends; + } + if (showWeekNumbers != null) { + setting.showWeekNumbers = !showWeekNumbers; + } + if (firstDayOfWeek != null) { + setting.firstDayOfWeek = firstDayOfWeek; + } + if (layoutFieldId != null) { + setting.layoutFieldId = layoutFieldId; + } + }); + context + .read() + .add(CalendarSettingEvent.updateLayoutSetting(setting)); + onUpdated(setting); + } +} + +class LayoutDateField extends StatelessWidget { + final String fieldId; + final String viewId; + final FieldController fieldController; + final PopoverMutex popoverMutex; + final Function(String fieldId) onUpdated; + + const LayoutDateField({ + required this.fieldId, + required this.fieldController, + required this.viewId, + required this.popoverMutex, + required this.onUpdated, + super.key, + }); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(300, 400)), + mutex: popoverMutex, + popupBuilder: (context) { + return BlocProvider( + create: (context) => getIt( + param1: viewId, param2: fieldController) + ..add(const DatabasePropertyEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final items = state.fieldContexts + .where((field) => field.fieldType == FieldType.DateTime) + .map( + (fieldInfo) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(fieldInfo.name), + onTap: () { + onUpdated(fieldInfo.id); + popoverMutex.close(); + }, + leftIcon: svgWidget('grid/field/date'), + rightIcon: fieldInfo.id == fieldId + ? svgWidget('grid/checkmark') + : null, + ), + ); + }, + ).toList(); + + return SizedBox( + width: 200, + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => items[index], + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: items.length, + ), + ); + }, + ), + ); + }, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + text: FlowyText.medium( + LocaleKeys.calendar_settings_layoutDateField.tr()), + ), + ), + ); + } +} + +class ShowWeekNumber extends StatelessWidget { + final bool showWeekNumbers; + final Function(bool showWeekNumbers) onUpdated; + + const ShowWeekNumber({ + required this.showWeekNumbers, + required this.onUpdated, + super.key, + }); + + @override + Widget build(BuildContext context) { + return _toggleItem( + onToggle: (showWeekNumbers) { + onUpdated(!showWeekNumbers); + }, + value: showWeekNumbers, + text: LocaleKeys.calendar_settings_showWeekNumbers.tr(), + ); + } +} + +class ShowWeekends extends StatelessWidget { + final bool showWeekends; + final Function(bool showWeekends) onUpdated; + const ShowWeekends({ + super.key, + required this.showWeekends, + required this.onUpdated, + }); + + @override + Widget build(BuildContext context) { + return _toggleItem( + onToggle: (showWeekends) { + onUpdated(!showWeekends); + }, + value: showWeekends, + text: LocaleKeys.calendar_settings_showWeekends.tr(), + ); + } +} + +class FirstDayOfWeek extends StatelessWidget { + final int firstDayOfWeek; + final PopoverMutex popoverMutex; + final Function(int firstDayOfWeek) onUpdated; + const FirstDayOfWeek({ + super.key, + required this.firstDayOfWeek, + required this.onUpdated, + required this.popoverMutex, + }); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(300, 400)), + mutex: popoverMutex, + popupBuilder: (context) { + final symbols = + DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; + // starts from sunday + final items = symbols.WEEKDAYS.asMap().entries.map((entry) { + final index = (entry.key - 1) % 7; + final string = entry.value; + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(string), + onTap: () { + onUpdated(index); + popoverMutex.close(); + }, + rightIcon: + firstDayOfWeek == index ? svgWidget('grid/checkmark') : null, + ), + ); + }).toList(); + + return SizedBox( + width: 100, + child: ListView.separated( + shrinkWrap: true, + itemBuilder: (context, index) => items[index], + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: 2, + ), + ); + }, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + text: FlowyText.medium( + LocaleKeys.calendar_settings_firstDayOfWeek.tr()), + ), + ), + ); + } +} + +Widget _toggleItem({ + required String text, + required bool value, + required void Function(bool) onToggle, +}) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), + child: Row( + children: [ + FlowyText.medium(text), + const Spacer(), + Toggle( + value: value, + onChanged: (value) => onToggle(!value), + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], + ), + ), + ); +} + +enum CalendarLayoutSettingAction { + layoutField, + layoutType, + showWeekends, + firstDayOfWeek, + showWeekNumber, + showTimeLine, +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart new file mode 100644 index 0000000000..d9777ebba8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_setting.dart @@ -0,0 +1,112 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-database/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:styled_widget/styled_widget.dart'; + +import 'calendar_layout_setting.dart'; + +/// The highest-level widget shown in the popover triggered by clicking the +/// "Settings" button. By default, shows [AllCalendarSettings] but upon +/// selecting a category, replaces contents with contents of the submenu. +class CalendarSetting extends StatelessWidget { + final CalendarSettingContext settingContext; + final CalendarLayoutSettingsPB? layoutSettings; + final Function(CalendarLayoutSettingsPB? layoutSettings) onUpdated; + + const CalendarSetting({ + required this.onUpdated, + required this.layoutSettings, + required this.settingContext, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CalendarSettingBloc(layoutSettings: layoutSettings), + child: BlocBuilder( + builder: (context, state) { + final CalendarSettingAction? action = + state.selectedAction.foldLeft(null, (previous, action) => action); + switch (action) { + case CalendarSettingAction.layout: + return CalendarLayoutSetting( + onUpdated: onUpdated, + settingContext: settingContext, + ); + default: + return const AllCalendarSettings().padding(all: 6.0); + } + }, + ), + ); + } +} + +/// Shows all of the available categories of settings that can be set here. +/// For now, this only includes the Layout category. +class AllCalendarSettings extends StatelessWidget { + const AllCalendarSettings({super.key}); + + @override + Widget build(BuildContext context) { + final items = CalendarSettingAction.values + .map((e) => _settingItem(context, e)) + .toList(); + + return SizedBox( + width: 140, + child: ListView.separated( + shrinkWrap: true, + controller: ScrollController(), + itemCount: items.length, + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + physics: StyledScrollPhysics(), + itemBuilder: (BuildContext context, int index) => items[index], + ), + ); + } + + Widget _settingItem(BuildContext context, CalendarSettingAction action) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(action.title()), + onTap: () { + context + .read() + .add(CalendarSettingEvent.performAction(action)); + }, + ), + ); + } +} + +extension _SettingExtension on CalendarSettingAction { + String title() { + switch (this) { + case CalendarSettingAction.layout: + return LocaleKeys.grid_settings_layout.tr(); + } + } +} + +class CalendarSettingContext { + final String viewId; + final FieldController fieldController; + + CalendarSettingContext({ + required this.viewId, + required this.fieldController, + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart index 4b1399763b..1f704b5e83 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/toolbar/calendar_toolbar.dart @@ -1,5 +1,14 @@ -import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../application/calendar_bloc.dart'; +import 'calendar_setting.dart'; class CalendarToolbar extends StatelessWidget { const CalendarToolbar({super.key}); @@ -10,14 +19,65 @@ class CalendarToolbar extends StatelessWidget { height: 40, child: Row( mainAxisAlignment: MainAxisAlignment.end, - children: const [ - FlowyTextButton( - "Settings", - fillColor: Colors.transparent, - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), - ), + children: [ + _SettingButton(), ], ), ); } } + +class _SettingButton extends StatefulWidget { + @override + State createState() => _SettingButtonState(); +} + +class _SettingButtonState extends State<_SettingButton> { + late PopoverController popoverController; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithRightAligned, + triggerActions: PopoverTriggerFlags.none, + constraints: BoxConstraints.loose(const Size(300, 400)), + margin: EdgeInsets.zero, + child: FlowyTextButton( + LocaleKeys.settings_title.tr(), + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.typeOptionContentInsets, + onPressed: () => popoverController.show(), + ), + popupBuilder: (BuildContext popoverContext) { + final bloc = context.watch(); + final settingContext = CalendarSettingContext( + viewId: bloc.viewId, + fieldController: bloc.fieldController, + ); + return CalendarSetting( + settingContext: settingContext, + layoutSettings: bloc.state.settings.fold( + () => null, + (settings) => settings, + ), + onUpdated: (layoutSettings) { + if (layoutSettings == null) { + return; + } + context + .read() + .add(CalendarEvent.updateCalendarLayoutSetting(layoutSettings)); + }, + ); + }, // use blocbuilder + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index c0cb7eb245..7d73ab9b4a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -72,7 +72,7 @@ class GridBloc extends Bloc { add(GridEvent.didReceiveGridUpdate(database)); } }, - onRowsChanged: (rowInfos, reason) { + onRowsChanged: (rowInfos, _, reason) { if (!isClosed) { add(GridEvent.didReceiveRowUpdate(rowInfos, reason)); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index 4bc655a305..ddf2dee63a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -23,6 +23,7 @@ class CardCellBuilder { required CellIdentifier cellId, EditableCardNotifier? cellNotifier, CardConfiguration? cardConfiguration, + Map? styles, }) { final cellControllerBuilder = CellControllerBuilder( cellId: cellId, @@ -30,6 +31,7 @@ class CardCellBuilder { ); final key = cellId.key(); + final style = styles?[cellId.fieldType]; switch (cellId.fieldType) { case FieldType.Checkbox: return CheckboxCardCell( @@ -70,6 +72,7 @@ class CardCellBuilder { return TextCardCell( cellControllerBuilder: cellControllerBuilder, editableNotifier: cellNotifier, + style: isStyleOrNull(style), key: key, ); case FieldType.URL: diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart index 6bc9ee9eac..e5942c8ab3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/card_cell.dart @@ -24,10 +24,21 @@ class CardConfiguration { } } -abstract class CardCell extends StatefulWidget { - final T? cardData; +abstract class CardCellStyle {} - const CardCell({super.key, this.cardData}); +S? isStyleOrNull(CardCellStyle? style) { + if (style is S) { + return style as S; + } else { + return null; + } +} + +abstract class CardCell extends StatefulWidget { + final T? cardData; + final S? style; + + const CardCell({super.key, this.cardData, this.style}); } class EditableCardNotifier { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart index 6eba4acef9..2251af0cf1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/select_option_card_cell.dart @@ -9,7 +9,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../bloc/select_option_card_cell_bloc.dart'; import 'card_cell.dart'; -class SelectOptionCardCell extends CardCell with EditableCell { +class SelectOptionCardCellStyle extends CardCellStyle {} + +class SelectOptionCardCell extends CardCell + with EditableCell { final CellControllerBuilder cellControllerBuilder; final CellRenderHook, T>? renderHook; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart index 8ffc834247..4eb9d9137e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,7 +8,14 @@ import '../bloc/text_card_cell_bloc.dart'; import '../define.dart'; import 'card_cell.dart'; -class TextCardCell extends CardCell with EditableCell { +class TextCardCellStyle extends CardCellStyle { + final double fontSize; + + TextCardCellStyle(this.fontSize); +} + +class TextCardCell extends CardCell + with EditableCell { @override final EditableCardNotifier? editableNotifier; final CellControllerBuilder cellControllerBuilder; @@ -17,8 +23,9 @@ class TextCardCell extends CardCell with EditableCell { const TextCardCell({ required this.cellControllerBuilder, this.editableNotifier, + TextCardCellStyle? style, Key? key, - }) : super(key: key); + }) : super(key: key, style: style); @override State createState() => _TextCardCellState(); @@ -129,6 +136,14 @@ class _TextCardCellState extends State { super.dispose(); } + double _fontSize() { + if (widget.style != null) { + return widget.style!.fontSize; + } else { + return 14; + } + } + Widget _buildText(TextCardCellState state) { return Padding( padding: EdgeInsets.symmetric( @@ -136,7 +151,7 @@ class _TextCardCellState extends State { ), child: FlowyText.medium( state.content, - fontSize: 14, + fontSize: _fontSize(), maxLines: null, // Enable multiple lines ), ); @@ -150,7 +165,7 @@ class _TextCardCellState extends State { onChanged: (value) => focusChanged(), onEditingComplete: () => focusNode.unfocus(), maxLines: null, - style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14), + style: Theme.of(context).textTheme.bodyMedium!.size(_fontSize()), decoration: InputDecoration( // Magic number 4 makes the textField take up the same space as FlowyText contentPadding: EdgeInsets.symmetric( diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index 88638912db..668338bfb5 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -73,7 +73,7 @@ class BoardTestContext { BoardTestContext(this.gridView, this._boardDataController); List get rowInfos { - return _boardDataController.rowInfos; + return _boardDataController.rowCache.rowInfos; } List get fieldContexts => fieldController.fieldInfos; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index c68dffb976..af85cc8bf2 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -26,7 +26,7 @@ class GridTestContext { GridTestContext(this.gridView, this.gridController); List get rowInfos { - return gridController.rowInfos; + return gridController.rowCache.rowInfos; } List get fieldContexts => fieldController.fieldInfos; diff --git a/frontend/rust-lib/flowy-database/src/notification.rs b/frontend/rust-lib/flowy-database/src/notification.rs index c0dd347411..0a693d4ebe 100644 --- a/frontend/rust-lib/flowy-database/src/notification.rs +++ b/frontend/rust-lib/flowy-database/src/notification.rs @@ -35,8 +35,6 @@ pub enum DatabaseNotification { DidUpdateLayoutSettings = 80, // Trigger when the layout field of the database is changed DidSetNewLayoutField = 81, - - DidArrangeCalendarWithNewField = 82, } impl std::default::Default for DatabaseNotification {