mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
893aae002e
commit
77d787a929
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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});
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user