feat: calendar UI improvements (#1941)

* chore: enable calendar

* feat: set font of the day event widget

* feat: support add/remove event

* chore: initial settings popover

* chore: calendar bloc can update layout settings

* fix: events overflow in day cell

* feat: calendar layout settings UI

* feat: layout calendar by another date field

* chore: i18n

* chore: hide the show weekend option

* chore: add popover mutex

* fix: clear existing events before adding new ones

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Richard Shiue 2023-03-20 21:16:37 +08:00 committed by GitHub
parent 893aae002e
commit 77d787a929
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1337 additions and 328 deletions

View File

@ -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"
}
}
}

View File

@ -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<RowInfo> rowInfos,
RowsChangedReason,
);
typedef OnGroupByField = void Function(List<GroupPB>);
typedef OnUpdateGroup = void Function(List<GroupPB>);
typedef OnDeleteGroup = void Function(List<String>);
@ -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<RowInfo> 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<Either<Unit, FlowyError>> 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 {

View File

@ -10,9 +10,14 @@ import 'row/row_cache.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<String> ids);
typedef OnRowsUpdated = void Function(List<String> ids);
typedef OnRowsDeleted = void Function(List<String> ids);
typedef OnRowsChanged = void Function(
List<RowInfo>,
RowsChangedReason,
UnmodifiableListView<RowInfo> rows,
UnmodifiableMapView<String, RowInfo> rowByRowId,
RowsChangedReason reason,
);
typedef OnError = void Function(FlowyError);

View File

@ -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<LayoutSettingPB, FlowyError>;
class DatabaseCalendarLayoutListener {
final String viewId;
PublishNotifier<NewLayoutFieldValue>? _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<Uint8List, FlowyError> result,
) {
switch (ty) {
case DatabaseNotification.DidSetNewLayoutField:
result.fold(
(payload) => _newLayoutFieldNotifier?.value =
left(LayoutSettingPB.fromBuffer(payload)),
(error) => _newLayoutFieldNotifier?.value = right(error),
);
break;
default:
break;
}
}
Future<void> stop() async {
await _listener?.stop();
_newLayoutFieldNotifier?.dispose();
_newLayoutFieldNotifier = null;
}
}

View File

@ -37,11 +37,15 @@ class RowCache {
final RowCacheDelegate _delegate;
final RowChangesetNotifier _rowChangeReasonNotifier;
UnmodifiableListView<RowInfo> get visibleRows {
UnmodifiableListView<RowInfo> get rowInfos {
var visibleRows = [..._rowList.rows];
return UnmodifiableListView(visibleRows);
}
UnmodifiableMapView<String, RowInfo> 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<RowPB> rows) {
for (final row in rows) {
final rowInfo = buildGridRow(row);

View File

@ -9,14 +9,14 @@ class RowList {
List<RowInfo> get rows => List.from(_rowInfos);
/// Use Map for faster access the raw row data.
final HashMap<String, RowInfo> _rowInfoByRowId = HashMap();
final HashMap<String, RowInfo> 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;
}
}

View File

@ -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<RowInfo> get rowInfos => _rowCache.visibleRows;
UnmodifiableListView<RowInfo> 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<void> 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;
}
}

View File

@ -17,9 +17,11 @@ part 'calendar_bloc.freezed.dart';
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
final DatabaseController _databaseController;
Map<String, FieldInfo> 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<CalendarEvent, CalendarState> {
view: view,
layoutType: LayoutTypePB.Calendar,
),
super(CalendarState.initial(view.id)) {
super(CalendarState.initial()) {
on<CalendarEvent>(
(event, emit) async {
await event.when(
@ -44,16 +46,49 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
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<CalendarCardData> newEvent) {
emit(state.copyWith(events: [...state.events, newEvent]));
updateCalendarLayoutSetting:
(CalendarLayoutSettingsPB layoutSetting) async {
await _updateCalendarLayoutSetting(layoutSetting);
},
didUpdateFieldInfos: (Map<String, FieldInfo> fieldInfoByFieldId) {
emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId));
didUpdateEvent: (CalendarEventData<CalendarDayEvent> 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<CalendarDayEvent> event) {
emit(state.copyWith(
allEvents: [...state.allEvents, event],
newEvent: event,
));
},
didDeleteEvents: (List<String> 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<CalendarEvent, CalendarState> {
}
Future<void> _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<CalendarEvent, CalendarState> {
},
);
result.fold(
(newRow) => _loadEvent(newRow.id),
return result.fold(
(newRow) {},
(err) => Log.error(err),
);
}
@ -119,17 +154,23 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
);
}
Future<void> _loadEvent(String rowId) async {
Future<void> _updateCalendarLayoutSetting(
CalendarLayoutSettingsPB layoutSetting) async {
return _databaseController.updateCalenderLayoutSetting(layoutSetting);
}
Future<CalendarEventData<CalendarDayEvent>?> _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<CalendarEvent, CalendarState> {
result.fold(
(events) {
if (!isClosed) {
final calendarEvents = <CalendarEventData<CalendarCardData>>[];
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
for (final eventPB in events.items) {
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
if (calendarEvent != null) {
@ -156,9 +197,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
});
}
CalendarEventData<CalendarCardData>? _calendarEventDataFromEventPB(
CalendarEventData<CalendarDayEvent>? _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<CalendarEvent, CalendarState> {
fieldInfo: fieldInfo,
);
final eventData = CalendarCardData(
final eventData = CalendarDayEvent(
event: eventPB,
cellId: cellId,
);
@ -192,10 +233,31 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
},
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<CalendarEvent, CalendarState> {
onLoadLayout: _didReceiveLayoutSetting,
);
final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
onCalendarLayoutChanged: _didReceiveNewLayoutField);
_databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged,
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
);
}
@ -216,44 +282,75 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));
}
}
void _didReceiveNewLayoutField(LayoutSettingPB layoutSetting) {
if (layoutSetting.hasCalendar()) {
if (isClosed) return;
add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
}
}
}
typedef Events = List<CalendarEventData<CalendarCardData>>;
typedef Events = List<CalendarEventData<CalendarDayEvent>>;
@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<CalendarCardData> event) = _ReceiveEvent;
const factory CalendarEvent.didUpdateFieldInfos(
Map<String, FieldInfo> fieldInfoByFieldId) = _DidUpdateFieldInfos;
// Called when specific event was updated
const factory CalendarEvent.didUpdateEvent(
CalendarEventData<CalendarDayEvent> event) = _DidUpdateEvent;
// Called after creating a new event
const factory CalendarEvent.didReceiveNewEvent(
CalendarEventData<CalendarDayEvent> event) = _DidReceiveNewEvent;
// Called when deleting events
const factory CalendarEvent.didDeleteEvents(List<String> 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<DatabasePB> database,
required Events events,
required Map<String, FieldInfo> fieldInfoByFieldId,
required Events allEvents,
required Events initialEvents,
CalendarEventData<CalendarDayEvent>? newEvent,
required List<String> deleteEventIds,
CalendarEventData<CalendarDayEvent>? updateEvent,
required Option<CalendarLayoutSettingsPB> settings,
required DatabaseLoadingState loadingState,
required Option<FlowyError> 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});
}

View File

@ -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<CalendarSettingEvent, CalendarSettingState> {
CalendarSettingBloc({required CalendarLayoutSettingsPB? layoutSettings})
: super(CalendarSettingState.initial(layoutSettings)) {
on<CalendarSettingEvent>((event, emit) {
event.when(
performAction: (action) {
emit(state.copyWith(selectedAction: Some(action)));
},
updateLayoutSetting: (setting) {
emit(state.copyWith(layoutSetting: Some(setting)));
},
);
});
}
@override
Future<void> close() async => super.close();
}
@freezed
class CalendarSettingState with _$CalendarSettingState {
const factory CalendarSettingState({
required Option<CalendarSettingAction> selectedAction,
required Option<CalendarLayoutSettingsPB> 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,
}

View File

@ -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 {

View File

@ -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<CalendarDayEvent> 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;
}

View File

@ -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<CalendarPage> {
final _eventController = EventController<CalendarCardData>();
final _eventController = EventController<CalendarDayEvent>();
GlobalKey<MonthViewState>? _calendarState;
late CalendarBloc _calendarBloc;
@ -58,21 +51,55 @@ class _CalendarPageState extends State<CalendarPage> {
value: _calendarBloc,
)
],
child: BlocListener<CalendarBloc, CalendarState>(
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<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.initialEvents != c.initialEvents,
listener: (context, state) {
_eventController.removeWhere((_) => true);
_eventController.addAll(state.initialEvents);
},
),
BlocListener<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.deleteEventIds != c.deleteEventIds,
listener: (context, state) {
_eventController.removeWhere(
(element) =>
state.deleteEventIds.contains(element.event!.eventId),
);
},
),
BlocListener<CalendarBloc, CalendarState>(
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<CalendarBloc, CalendarState>(
listenWhen: (p, c) => p.newEvent != c.newEvent,
listener: (context, state) {
if (state.newEvent != null) {
_eventController.add(state.newEvent!);
}
},
),
],
child: BlocBuilder<CalendarBloc, CalendarState>(
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<CalendarPage> {
);
}
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<CalendarPage> {
Widget _calendarDayBuilder(
DateTime date,
List<CalendarEventData<CalendarCardData>> calenderEvents,
List<CalendarEventData<CalendarDayEvent>> 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<CalendarPage> {
},
);
}
}
class _CalendarCard extends StatelessWidget {
final bool isToday;
final bool isInMonth;
final DateTime date;
final List<Widget> 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;
}

View File

@ -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<CalendarLayoutSetting> createState() => _CalendarLayoutSettingState();
}
class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
late final PopoverMutex popoverMutex;
@override
void initState() {
popoverMutex = PopoverMutex();
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
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<CalendarLayoutSettingAction> _availableCalendarSettings(
CalendarLayoutSettingsPB layoutSettings) {
List<CalendarLayoutSettingAction> 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<CalendarSettingBloc>()
.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<CalendarSettingBloc>()
.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<DatabasePropertyBloc>(
param1: viewId, param2: fieldController)
..add(const DatabasePropertyEvent.initial()),
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
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,
}

View File

@ -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<CalendarSettingBloc>(
create: (context) => CalendarSettingBloc(layoutSettings: layoutSettings),
child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
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<CalendarSettingBloc>()
.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,
});
}

View File

@ -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<StatefulWidget> 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<CalendarBloc>();
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<CalendarBloc>()
.add(CalendarEvent.updateCalendarLayoutSetting(layoutSettings));
},
);
}, // use blocbuilder
);
}
}

View File

@ -72,7 +72,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
add(GridEvent.didReceiveGridUpdate(database));
}
},
onRowsChanged: (rowInfos, reason) {
onRowsChanged: (rowInfos, _, reason) {
if (!isClosed) {
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
}

View File

@ -23,6 +23,7 @@ class CardCellBuilder<CustomCardData> {
required CellIdentifier cellId,
EditableCardNotifier? cellNotifier,
CardConfiguration<CustomCardData>? cardConfiguration,
Map<FieldType, CardCellStyle>? styles,
}) {
final cellControllerBuilder = CellControllerBuilder(
cellId: cellId,
@ -30,6 +31,7 @@ class CardCellBuilder<CustomCardData> {
);
final key = cellId.key();
final style = styles?[cellId.fieldType];
switch (cellId.fieldType) {
case FieldType.Checkbox:
return CheckboxCardCell(
@ -70,6 +72,7 @@ class CardCellBuilder<CustomCardData> {
return TextCardCell(
cellControllerBuilder: cellControllerBuilder,
editableNotifier: cellNotifier,
style: isStyleOrNull<TextCardCellStyle>(style),
key: key,
);
case FieldType.URL:

View File

@ -24,10 +24,21 @@ class CardConfiguration<CustomCardData> {
}
}
abstract class CardCell<T> extends StatefulWidget {
final T? cardData;
abstract class CardCellStyle {}
const CardCell({super.key, this.cardData});
S? isStyleOrNull<S>(CardCellStyle? style) {
if (style is S) {
return style as S;
} else {
return null;
}
}
abstract class CardCell<T, S extends CardCellStyle> extends StatefulWidget {
final T? cardData;
final S? style;
const CardCell({super.key, this.cardData, this.style});
}
class EditableCardNotifier {

View File

@ -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<T> extends CardCell<T> with EditableCell {
class SelectOptionCardCellStyle extends CardCellStyle {}
class SelectOptionCardCell<T> extends CardCell<T, SelectOptionCardCellStyle>
with EditableCell {
final CellControllerBuilder cellControllerBuilder;
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;

View File

@ -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<String, TextCardCellStyle>
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<TextCardCell> createState() => _TextCardCellState();
@ -129,6 +136,14 @@ class _TextCardCellState extends State<TextCardCell> {
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<TextCardCell> {
),
child: FlowyText.medium(
state.content,
fontSize: 14,
fontSize: _fontSize(),
maxLines: null, // Enable multiple lines
),
);
@ -150,7 +165,7 @@ class _TextCardCellState extends State<TextCardCell> {
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(

View File

@ -73,7 +73,7 @@ class BoardTestContext {
BoardTestContext(this.gridView, this._boardDataController);
List<RowInfo> get rowInfos {
return _boardDataController.rowInfos;
return _boardDataController.rowCache.rowInfos;
}
List<FieldInfo> get fieldContexts => fieldController.fieldInfos;

View File

@ -26,7 +26,7 @@ class GridTestContext {
GridTestContext(this.gridView, this.gridController);
List<RowInfo> get rowInfos {
return gridController.rowInfos;
return gridController.rowCache.rowInfos;
}
List<FieldInfo> get fieldContexts => fieldController.fieldInfos;

View File

@ -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 {