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",
|
"addFilter": "Add Filter",
|
||||||
"deleteFilter": "Delete filter",
|
"deleteFilter": "Delete filter",
|
||||||
"filterBy": "Filter by...",
|
"filterBy": "Filter by...",
|
||||||
"typeAValue": "Type a value..."
|
"typeAValue": "Type a value...",
|
||||||
|
"layout": "Layout"
|
||||||
},
|
},
|
||||||
"textFilter": {
|
"textFilter": {
|
||||||
"contains": "Contains",
|
"contains": "Contains",
|
||||||
@ -393,6 +394,12 @@
|
|||||||
"jumpToday": "Jump to Today",
|
"jumpToday": "Jump to Today",
|
||||||
"previousMonth": "Previous Month",
|
"previousMonth": "Previous Month",
|
||||||
"nextMonth": "Next 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/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/plugins/database_view/application/view/view_cache.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database/calendar_entities.pb.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 'row/row_cache.dart';
|
||||||
import 'group/group_listener.dart';
|
import 'group/group_listener.dart';
|
||||||
|
|
||||||
typedef OnRowsChanged = void Function(
|
|
||||||
List<RowInfo> rowInfos,
|
|
||||||
RowsChangedReason,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef OnGroupByField = void Function(List<GroupPB>);
|
typedef OnGroupByField = void Function(List<GroupPB>);
|
||||||
typedef OnUpdateGroup = void Function(List<GroupPB>);
|
typedef OnUpdateGroup = void Function(List<GroupPB>);
|
||||||
typedef OnDeleteGroup = void Function(List<String>);
|
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 {
|
class DatabaseCallbacks {
|
||||||
OnDatabaseChanged? onDatabaseChanged;
|
OnDatabaseChanged? onDatabaseChanged;
|
||||||
OnRowsChanged? onRowsChanged;
|
|
||||||
OnFieldsChanged? onFieldsChanged;
|
OnFieldsChanged? onFieldsChanged;
|
||||||
OnFiltersChanged? onFiltersChanged;
|
OnFiltersChanged? onFiltersChanged;
|
||||||
|
OnRowsChanged? onRowsChanged;
|
||||||
|
OnRowsDeleted? onRowsDeleted;
|
||||||
|
OnRowsUpdated? onRowsUpdated;
|
||||||
|
OnRowsCreated? onRowsCreated;
|
||||||
|
|
||||||
DatabaseCallbacks({
|
DatabaseCallbacks({
|
||||||
this.onDatabaseChanged,
|
this.onDatabaseChanged,
|
||||||
this.onRowsChanged,
|
this.onRowsChanged,
|
||||||
this.onFieldsChanged,
|
this.onFieldsChanged,
|
||||||
this.onFiltersChanged,
|
this.onFiltersChanged,
|
||||||
|
this.onRowsUpdated,
|
||||||
|
this.onRowsDeleted,
|
||||||
|
this.onRowsCreated,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,21 +85,23 @@ class DatabaseController {
|
|||||||
DatabaseCallbacks? _databaseCallbacks;
|
DatabaseCallbacks? _databaseCallbacks;
|
||||||
GroupCallbacks? _groupCallbacks;
|
GroupCallbacks? _groupCallbacks;
|
||||||
LayoutCallbacks? _layoutCallbacks;
|
LayoutCallbacks? _layoutCallbacks;
|
||||||
|
CalendarLayoutCallbacks? _calendarLayoutCallbacks;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
List<RowInfo> get rowInfos => _viewCache.rowInfos;
|
|
||||||
RowCache get rowCache => _viewCache.rowCache;
|
RowCache get rowCache => _viewCache.rowCache;
|
||||||
|
|
||||||
// Listener
|
// Listener
|
||||||
final DatabaseGroupListener groupListener;
|
final DatabaseGroupListener groupListener;
|
||||||
final DatabaseLayoutListener layoutListener;
|
final DatabaseLayoutListener layoutListener;
|
||||||
|
final DatabaseCalendarLayoutListener calendarLayoutListener;
|
||||||
|
|
||||||
DatabaseController({required ViewPB view, required this.layoutType})
|
DatabaseController({required ViewPB view, required this.layoutType})
|
||||||
: viewId = view.id,
|
: viewId = view.id,
|
||||||
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
|
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
|
||||||
fieldController = FieldController(viewId: view.id),
|
fieldController = FieldController(viewId: view.id),
|
||||||
groupListener = DatabaseGroupListener(view.id),
|
groupListener = DatabaseGroupListener(view.id),
|
||||||
layoutListener = DatabaseLayoutListener(view.id) {
|
layoutListener = DatabaseLayoutListener(view.id),
|
||||||
|
calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
|
||||||
_viewCache = DatabaseViewCache(
|
_viewCache = DatabaseViewCache(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
fieldController: fieldController,
|
fieldController: fieldController,
|
||||||
@ -99,16 +110,21 @@ class DatabaseController {
|
|||||||
_listenOnFieldsChanged();
|
_listenOnFieldsChanged();
|
||||||
_listenOnGroupChanged();
|
_listenOnGroupChanged();
|
||||||
_listenOnLayoutChanged();
|
_listenOnLayoutChanged();
|
||||||
|
if (layoutType == LayoutTypePB.Calendar) {
|
||||||
|
_listenOnCalendarLayoutChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addListener({
|
void addListener({
|
||||||
DatabaseCallbacks? onDatabaseChanged,
|
DatabaseCallbacks? onDatabaseChanged,
|
||||||
LayoutCallbacks? onLayoutChanged,
|
LayoutCallbacks? onLayoutChanged,
|
||||||
GroupCallbacks? onGroupChanged,
|
GroupCallbacks? onGroupChanged,
|
||||||
|
CalendarLayoutCallbacks? onCalendarLayoutChanged,
|
||||||
}) {
|
}) {
|
||||||
_layoutCallbacks = onLayoutChanged;
|
_layoutCallbacks = onLayoutChanged;
|
||||||
_databaseCallbacks = onDatabaseChanged;
|
_databaseCallbacks = onDatabaseChanged;
|
||||||
_groupCallbacks = onGroupChanged;
|
_groupCallbacks = onGroupChanged;
|
||||||
|
_calendarLayoutCallbacks = onCalendarLayoutChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> open() async {
|
Future<Either<Unit, FlowyError>> open() async {
|
||||||
@ -218,9 +234,17 @@ class DatabaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _listenOnRowsChanged() {
|
void _listenOnRowsChanged() {
|
||||||
_viewCache.addListener(onRowsChanged: (reason) {
|
final callbacks =
|
||||||
_databaseCallbacks?.onRowsChanged?.call(rowInfos, reason);
|
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() {
|
void _listenOnFieldsChanged() {
|
||||||
@ -266,6 +290,14 @@ class DatabaseController {
|
|||||||
}, (r) => Log.error(r));
|
}, (r) => Log.error(r));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _listenOnCalendarLayoutChanged() {
|
||||||
|
calendarLayoutListener.start(onCalendarLayoutChanged: (result) {
|
||||||
|
result.fold((l) {
|
||||||
|
_calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
|
||||||
|
}, (r) => Log.error(r));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RowDataBuilder {
|
class RowDataBuilder {
|
||||||
|
@ -10,9 +10,14 @@ import 'row/row_cache.dart';
|
|||||||
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
|
||||||
typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
typedef OnFiltersChanged = void Function(List<FilterInfo>);
|
||||||
typedef OnDatabaseChanged = void Function(DatabasePB);
|
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(
|
typedef OnRowsChanged = void Function(
|
||||||
List<RowInfo>,
|
UnmodifiableListView<RowInfo> rows,
|
||||||
RowsChangedReason,
|
UnmodifiableMapView<String, RowInfo> rowByRowId,
|
||||||
|
RowsChangedReason reason,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef OnError = void Function(FlowyError);
|
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 RowCacheDelegate _delegate;
|
||||||
final RowChangesetNotifier _rowChangeReasonNotifier;
|
final RowChangesetNotifier _rowChangeReasonNotifier;
|
||||||
|
|
||||||
UnmodifiableListView<RowInfo> get visibleRows {
|
UnmodifiableListView<RowInfo> get rowInfos {
|
||||||
var visibleRows = [..._rowList.rows];
|
var visibleRows = [..._rowList.rows];
|
||||||
return UnmodifiableListView(visibleRows);
|
return UnmodifiableListView(visibleRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UnmodifiableMapView<String, RowInfo> get rowByRowId {
|
||||||
|
return UnmodifiableMapView(_rowList.rowInfoByRowId);
|
||||||
|
}
|
||||||
|
|
||||||
CellCache get cellCache => _cellCache;
|
CellCache get cellCache => _cellCache;
|
||||||
|
|
||||||
RowCache({
|
RowCache({
|
||||||
@ -61,6 +65,10 @@ class RowCache {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowInfo? getRow(String rowId) {
|
||||||
|
return _rowList.get(rowId);
|
||||||
|
}
|
||||||
|
|
||||||
void setInitialRows(List<RowPB> rows) {
|
void setInitialRows(List<RowPB> rows) {
|
||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
final rowInfo = buildGridRow(row);
|
final rowInfo = buildGridRow(row);
|
||||||
|
@ -9,14 +9,14 @@ class RowList {
|
|||||||
List<RowInfo> get rows => List.from(_rowInfos);
|
List<RowInfo> get rows => List.from(_rowInfos);
|
||||||
|
|
||||||
/// Use Map for faster access the raw row data.
|
/// 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) {
|
RowInfo? get(String rowId) {
|
||||||
return _rowInfoByRowId[rowId];
|
return rowInfoByRowId[rowId];
|
||||||
}
|
}
|
||||||
|
|
||||||
int? indexOfRow(String rowId) {
|
int? indexOfRow(String rowId) {
|
||||||
final rowInfo = _rowInfoByRowId[rowId];
|
final rowInfo = rowInfoByRowId[rowId];
|
||||||
if (rowInfo != null) {
|
if (rowInfo != null) {
|
||||||
return _rowInfos.indexOf(rowInfo);
|
return _rowInfos.indexOf(rowInfo);
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ class RowList {
|
|||||||
} else {
|
} else {
|
||||||
_rowInfos.add(rowInfo);
|
_rowInfos.add(rowInfo);
|
||||||
}
|
}
|
||||||
_rowInfoByRowId[rowId] = rowInfo;
|
rowInfoByRowId[rowId] = rowInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
InsertedIndex? insert(int index, RowInfo rowInfo) {
|
InsertedIndex? insert(int index, RowInfo rowInfo) {
|
||||||
@ -47,21 +47,21 @@ class RowList {
|
|||||||
if (oldRowInfo != null) {
|
if (oldRowInfo != null) {
|
||||||
_rowInfos.insert(insertedIndex, rowInfo);
|
_rowInfos.insert(insertedIndex, rowInfo);
|
||||||
_rowInfos.remove(oldRowInfo);
|
_rowInfos.remove(oldRowInfo);
|
||||||
_rowInfoByRowId[rowId] = rowInfo;
|
rowInfoByRowId[rowId] = rowInfo;
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
_rowInfos.insert(insertedIndex, rowInfo);
|
_rowInfos.insert(insertedIndex, rowInfo);
|
||||||
_rowInfoByRowId[rowId] = rowInfo;
|
rowInfoByRowId[rowId] = rowInfo;
|
||||||
return InsertedIndex(index: insertedIndex, rowId: rowId);
|
return InsertedIndex(index: insertedIndex, rowId: rowId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeletedIndex? remove(String rowId) {
|
DeletedIndex? remove(String rowId) {
|
||||||
final rowInfo = _rowInfoByRowId[rowId];
|
final rowInfo = rowInfoByRowId[rowId];
|
||||||
if (rowInfo != null) {
|
if (rowInfo != null) {
|
||||||
final index = _rowInfos.indexOf(rowInfo);
|
final index = _rowInfos.indexOf(rowInfo);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
_rowInfoByRowId.remove(rowInfo.rowPB.id);
|
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
||||||
_rowInfos.remove(rowInfo);
|
_rowInfos.remove(rowInfo);
|
||||||
}
|
}
|
||||||
return DeletedIndex(index: index, rowInfo: rowInfo);
|
return DeletedIndex(index: index, rowInfo: rowInfo);
|
||||||
@ -105,7 +105,7 @@ class RowList {
|
|||||||
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
|
if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
|
||||||
newRows.add(rowInfo);
|
newRows.add(rowInfo);
|
||||||
} else {
|
} else {
|
||||||
_rowInfoByRowId.remove(rowInfo.rowPB.id);
|
rowInfoByRowId.remove(rowInfo.rowPB.id);
|
||||||
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
|
deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -136,7 +136,7 @@ class RowList {
|
|||||||
_rowInfos.clear();
|
_rowInfos.clear();
|
||||||
|
|
||||||
for (final rowId in rowIds) {
|
for (final rowId in rowIds) {
|
||||||
final rowInfo = _rowInfoByRowId[rowId];
|
final rowInfo = rowInfoByRowId[rowId];
|
||||||
if (rowInfo != null) {
|
if (rowInfo != null) {
|
||||||
_rowInfos.add(rowInfo);
|
_rowInfos.add(rowInfo);
|
||||||
}
|
}
|
||||||
@ -155,6 +155,6 @@ class RowList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool contains(String rowId) {
|
bool contains(String rowId) {
|
||||||
return _rowInfoByRowId[rowId] != null;
|
return rowInfoByRowId[rowId] != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,50 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import '../defines.dart';
|
||||||
import '../field/field_controller.dart';
|
import '../field/field_controller.dart';
|
||||||
import '../row/row_cache.dart';
|
import '../row/row_cache.dart';
|
||||||
import 'view_listener.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
|
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information
|
||||||
class DatabaseViewCache {
|
class DatabaseViewCache {
|
||||||
final String viewId;
|
final String viewId;
|
||||||
late RowCache _rowCache;
|
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;
|
RowCache get rowCache => _rowCache;
|
||||||
|
|
||||||
|
RowInfo? getRow(String rowId) => _rowCache.getRow(rowId);
|
||||||
|
|
||||||
DatabaseViewCache({
|
DatabaseViewCache({
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required FieldController fieldController,
|
required FieldController fieldController,
|
||||||
}) : _gridViewListener = DatabaseViewListener(viewId: viewId) {
|
}) : _databaseViewListener = DatabaseViewListener(viewId: viewId) {
|
||||||
final delegate = RowDelegatesImpl(fieldController);
|
final delegate = RowDelegatesImpl(fieldController);
|
||||||
_rowCache = RowCache(
|
_rowCache = RowCache(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
@ -24,10 +52,28 @@ class DatabaseViewCache {
|
|||||||
cacheDelegate: delegate,
|
cacheDelegate: delegate,
|
||||||
);
|
);
|
||||||
|
|
||||||
_gridViewListener.start(
|
_databaseViewListener.start(
|
||||||
onRowsChanged: (result) {
|
onRowsChanged: (result) {
|
||||||
result.fold(
|
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),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -50,23 +96,22 @@ class DatabaseViewCache {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_rowCache.onRowsChanged(
|
||||||
|
(reason) => _callbacks?.onRowsChanged?.call(
|
||||||
|
rowInfos,
|
||||||
|
_rowCache.rowByRowId,
|
||||||
|
reason,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _gridViewListener.stop();
|
await _databaseViewListener.stop();
|
||||||
await _rowCache.dispose();
|
await _rowCache.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void addListener({
|
void addListener(DatabaseViewCallbacks callbacks) {
|
||||||
required void Function(RowsChangedReason) onRowsChanged,
|
_callbacks = callbacks;
|
||||||
bool Function()? listenWhen,
|
|
||||||
}) {
|
|
||||||
_rowCache.onRowsChanged((reason) {
|
|
||||||
if (listenWhen != null && listenWhen() == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRowsChanged(reason);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,11 @@ part 'calendar_bloc.freezed.dart';
|
|||||||
|
|
||||||
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
||||||
final DatabaseController _databaseController;
|
final DatabaseController _databaseController;
|
||||||
|
Map<String, FieldInfo> fieldInfoByFieldId = {};
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
String get viewId => _databaseController.viewId;
|
String get viewId => _databaseController.viewId;
|
||||||
|
FieldController get fieldController => _databaseController.fieldController;
|
||||||
CellCache get cellCache => _databaseController.rowCache.cellCache;
|
CellCache get cellCache => _databaseController.rowCache.cellCache;
|
||||||
RowCache get rowCache => _databaseController.rowCache;
|
RowCache get rowCache => _databaseController.rowCache;
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
view: view,
|
view: view,
|
||||||
layoutType: LayoutTypePB.Calendar,
|
layoutType: LayoutTypePB.Calendar,
|
||||||
),
|
),
|
||||||
super(CalendarState.initial(view.id)) {
|
super(CalendarState.initial()) {
|
||||||
on<CalendarEvent>(
|
on<CalendarEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
await event.when(
|
||||||
@ -44,16 +46,49 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
emit(state.copyWith(database: Some(database)));
|
emit(state.copyWith(database: Some(database)));
|
||||||
},
|
},
|
||||||
didLoadAllEvents: (events) {
|
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 {
|
createEvent: (DateTime date, String title) async {
|
||||||
await _createEvent(date, title);
|
await _createEvent(date, title);
|
||||||
},
|
},
|
||||||
didReceiveEvent: (CalendarEventData<CalendarCardData> newEvent) {
|
updateCalendarLayoutSetting:
|
||||||
emit(state.copyWith(events: [...state.events, newEvent]));
|
(CalendarLayoutSettingsPB layoutSetting) async {
|
||||||
|
await _updateCalendarLayoutSetting(layoutSetting);
|
||||||
},
|
},
|
||||||
didUpdateFieldInfos: (Map<String, FieldInfo> fieldInfoByFieldId) {
|
didUpdateEvent: (CalendarEventData<CalendarDayEvent> eventData) {
|
||||||
emit(state.copyWith(fieldInfoByFieldId: fieldInfoByFieldId));
|
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 {
|
Future<void> _createEvent(DateTime date, String title) async {
|
||||||
state.settings.fold(
|
return state.settings.fold(
|
||||||
() => null,
|
() => null,
|
||||||
(settings) async {
|
(settings) async {
|
||||||
final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
|
final dateField = _getCalendarFieldInfo(settings.layoutFieldId);
|
||||||
@ -110,8 +145,8 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
result.fold(
|
return result.fold(
|
||||||
(newRow) => _loadEvent(newRow.id),
|
(newRow) {},
|
||||||
(err) => Log.error(err),
|
(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);
|
final payload = RowIdPB(viewId: viewId, rowId: rowId);
|
||||||
DatabaseEventGetCalendarEvent(payload).send().then((result) {
|
return DatabaseEventGetCalendarEvent(payload).send().then((result) {
|
||||||
result.fold(
|
return result.fold(
|
||||||
(eventPB) {
|
(eventPB) {
|
||||||
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
|
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
|
||||||
if (calendarEvent != null) {
|
return calendarEvent;
|
||||||
add(CalendarEvent.didReceiveEvent(calendarEvent));
|
},
|
||||||
}
|
(r) {
|
||||||
|
Log.error(r);
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
(r) => Log.error(r),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -140,7 +181,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
result.fold(
|
result.fold(
|
||||||
(events) {
|
(events) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
final calendarEvents = <CalendarEventData<CalendarCardData>>[];
|
final calendarEvents = <CalendarEventData<CalendarDayEvent>>[];
|
||||||
for (final eventPB in events.items) {
|
for (final eventPB in events.items) {
|
||||||
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
|
final calendarEvent = _calendarEventDataFromEventPB(eventPB);
|
||||||
if (calendarEvent != null) {
|
if (calendarEvent != null) {
|
||||||
@ -156,9 +197,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
CalendarEventData<CalendarCardData>? _calendarEventDataFromEventPB(
|
CalendarEventData<CalendarDayEvent>? _calendarEventDataFromEventPB(
|
||||||
CalendarEventPB eventPB) {
|
CalendarEventPB eventPB) {
|
||||||
final fieldInfo = state.fieldInfoByFieldId[eventPB.titleFieldId];
|
final fieldInfo = fieldInfoByFieldId[eventPB.titleFieldId];
|
||||||
if (fieldInfo != null) {
|
if (fieldInfo != null) {
|
||||||
final cellId = CellIdentifier(
|
final cellId = CellIdentifier(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
@ -166,7 +207,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
fieldInfo: fieldInfo,
|
fieldInfo: fieldInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
final eventData = CalendarCardData(
|
final eventData = CalendarDayEvent(
|
||||||
event: eventPB,
|
event: eventPB,
|
||||||
cellId: cellId,
|
cellId: cellId,
|
||||||
);
|
);
|
||||||
@ -192,10 +233,31 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
},
|
},
|
||||||
onFieldsChanged: (fieldInfos) {
|
onFieldsChanged: (fieldInfos) {
|
||||||
if (isClosed) return;
|
if (isClosed) return;
|
||||||
final fieldInfoByFieldId = {
|
fieldInfoByFieldId = {
|
||||||
for (var fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo
|
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,
|
onLoadLayout: _didReceiveLayoutSetting,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
|
||||||
|
onCalendarLayoutChanged: _didReceiveNewLayoutField);
|
||||||
|
|
||||||
_databaseController.addListener(
|
_databaseController.addListener(
|
||||||
onDatabaseChanged: onDatabaseChanged,
|
onDatabaseChanged: onDatabaseChanged,
|
||||||
onLayoutChanged: onLayoutChanged,
|
onLayoutChanged: onLayoutChanged,
|
||||||
|
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,44 +282,75 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
|
|||||||
add(CalendarEvent.didReceiveCalendarSettings(layoutSetting.calendar));
|
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
|
@freezed
|
||||||
class CalendarEvent with _$CalendarEvent {
|
class CalendarEvent with _$CalendarEvent {
|
||||||
const factory CalendarEvent.initial() = _InitialCalendar;
|
const factory CalendarEvent.initial() = _InitialCalendar;
|
||||||
|
|
||||||
|
// Called after loading the calendar layout setting from the backend
|
||||||
const factory CalendarEvent.didReceiveCalendarSettings(
|
const factory CalendarEvent.didReceiveCalendarSettings(
|
||||||
CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings;
|
CalendarLayoutSettingsPB settings) = _ReceiveCalendarSettings;
|
||||||
|
|
||||||
|
// Called after loading all the current evnets
|
||||||
const factory CalendarEvent.didLoadAllEvents(Events events) =
|
const factory CalendarEvent.didLoadAllEvents(Events events) =
|
||||||
_ReceiveCalendarEvents;
|
_ReceiveCalendarEvents;
|
||||||
const factory CalendarEvent.didReceiveEvent(
|
|
||||||
CalendarEventData<CalendarCardData> event) = _ReceiveEvent;
|
// Called when specific event was updated
|
||||||
const factory CalendarEvent.didUpdateFieldInfos(
|
const factory CalendarEvent.didUpdateEvent(
|
||||||
Map<String, FieldInfo> fieldInfoByFieldId) = _DidUpdateFieldInfos;
|
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) =
|
const factory CalendarEvent.createEvent(DateTime date, String title) =
|
||||||
_CreateEvent;
|
_CreateEvent;
|
||||||
|
|
||||||
|
// Called when updating the calendar's layout settings
|
||||||
|
const factory CalendarEvent.updateCalendarLayoutSetting(
|
||||||
|
CalendarLayoutSettingsPB layoutSetting) = _UpdateCalendarLayoutSetting;
|
||||||
|
|
||||||
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
|
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
|
||||||
_ReceiveDatabaseUpdate;
|
_ReceiveDatabaseUpdate;
|
||||||
|
|
||||||
|
const factory CalendarEvent.didReceiveNewLayoutField(
|
||||||
|
CalendarLayoutSettingsPB layoutSettings) = _DidReceiveNewLayoutField;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class CalendarState with _$CalendarState {
|
class CalendarState with _$CalendarState {
|
||||||
const factory CalendarState({
|
const factory CalendarState({
|
||||||
required String databaseId,
|
|
||||||
required Option<DatabasePB> database,
|
required Option<DatabasePB> database,
|
||||||
required Events events,
|
required Events allEvents,
|
||||||
required Map<String, FieldInfo> fieldInfoByFieldId,
|
required Events initialEvents,
|
||||||
|
CalendarEventData<CalendarDayEvent>? newEvent,
|
||||||
|
required List<String> deleteEventIds,
|
||||||
|
CalendarEventData<CalendarDayEvent>? updateEvent,
|
||||||
required Option<CalendarLayoutSettingsPB> settings,
|
required Option<CalendarLayoutSettingsPB> settings,
|
||||||
required DatabaseLoadingState loadingState,
|
required DatabaseLoadingState loadingState,
|
||||||
required Option<FlowyError> noneOrError,
|
required Option<FlowyError> noneOrError,
|
||||||
}) = _CalendarState;
|
}) = _CalendarState;
|
||||||
|
|
||||||
factory CalendarState.initial(String databaseId) => CalendarState(
|
factory CalendarState.initial() => CalendarState(
|
||||||
database: none(),
|
database: none(),
|
||||||
databaseId: databaseId,
|
allEvents: [],
|
||||||
fieldInfoByFieldId: {},
|
initialEvents: [],
|
||||||
events: [],
|
deleteEventIds: [],
|
||||||
settings: none(),
|
settings: none(),
|
||||||
noneOrError: none(),
|
noneOrError: none(),
|
||||||
loadingState: const _Loading(),
|
loadingState: const _Loading(),
|
||||||
@ -277,8 +374,10 @@ class CalendarEditingRow {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class CalendarCardData {
|
class CalendarDayEvent {
|
||||||
final CalendarEventPB event;
|
final CalendarEventPB event;
|
||||||
final CellIdentifier cellId;
|
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 {
|
class CalendarPluginConfig implements PluginConfig {
|
||||||
@override
|
@override
|
||||||
bool get creatable => false;
|
bool get creatable => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CalendarPlugin extends Plugin {
|
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/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/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:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:calendar_view/calendar_view.dart';
|
import 'package:calendar_view/calendar_view.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/image.dart';
|
import 'package:flowy_infra/image.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import '../../grid/presentation/layout/sizes.dart';
|
import 'calendar_day.dart';
|
||||||
import '../../widgets/row/cell_builder.dart';
|
|
||||||
import '../../widgets/row/row_detail.dart';
|
|
||||||
import 'layout/sizes.dart';
|
import 'layout/sizes.dart';
|
||||||
import 'toolbar/calendar_toolbar.dart';
|
import 'toolbar/calendar_toolbar.dart';
|
||||||
|
|
||||||
@ -29,7 +22,7 @@ class CalendarPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CalendarPageState extends State<CalendarPage> {
|
class _CalendarPageState extends State<CalendarPage> {
|
||||||
final _eventController = EventController<CalendarCardData>();
|
final _eventController = EventController<CalendarDayEvent>();
|
||||||
GlobalKey<MonthViewState>? _calendarState;
|
GlobalKey<MonthViewState>? _calendarState;
|
||||||
late CalendarBloc _calendarBloc;
|
late CalendarBloc _calendarBloc;
|
||||||
|
|
||||||
@ -58,21 +51,55 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
value: _calendarBloc,
|
value: _calendarBloc,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: BlocListener<CalendarBloc, CalendarState>(
|
child: MultiBlocListener(
|
||||||
listenWhen: (previous, current) => previous.events != current.events,
|
listeners: [
|
||||||
listener: (context, state) {
|
BlocListener<CalendarBloc, CalendarState>(
|
||||||
if (state.events.isNotEmpty) {
|
listenWhen: (p, c) => p.initialEvents != c.initialEvents,
|
||||||
_eventController.removeWhere((element) => true);
|
listener: (context, state) {
|
||||||
_eventController.addAll(state.events);
|
_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>(
|
child: BlocBuilder<CalendarBloc, CalendarState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// const _ToolbarBlocAdaptor(),
|
// const _ToolbarBlocAdaptor(),
|
||||||
_toolbar(),
|
const CalendarToolbar(),
|
||||||
_buildCalendar(_eventController),
|
_buildCalendar(
|
||||||
|
_eventController,
|
||||||
|
state.settings
|
||||||
|
.foldLeft(0, (previous, a) => a.firstDayOfWeek),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -82,16 +109,13 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _toolbar() {
|
Widget _buildCalendar(EventController eventController, int firstDayOfWeek) {
|
||||||
return const CalendarToolbar();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCalendar(EventController eventController) {
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: MonthView(
|
child: MonthView(
|
||||||
key: _calendarState,
|
key: _calendarState,
|
||||||
controller: _eventController,
|
controller: _eventController,
|
||||||
cellAspectRatio: 1.75,
|
cellAspectRatio: .9,
|
||||||
|
startDay: _weekdayFromInt(firstDayOfWeek),
|
||||||
borderColor: Theme.of(context).dividerColor,
|
borderColor: Theme.of(context).dividerColor,
|
||||||
headerBuilder: _headerNavigatorBuilder,
|
headerBuilder: _headerNavigatorBuilder,
|
||||||
weekDayBuilder: _headerWeekDayBuilder,
|
weekDayBuilder: _headerWeekDayBuilder,
|
||||||
@ -154,47 +178,19 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
|
|
||||||
Widget _calendarDayBuilder(
|
Widget _calendarDayBuilder(
|
||||||
DateTime date,
|
DateTime date,
|
||||||
List<CalendarEventData<CalendarCardData>> calenderEvents,
|
List<CalendarEventData<CalendarDayEvent>> calenderEvents,
|
||||||
isToday,
|
isToday,
|
||||||
isInMonth,
|
isInMonth,
|
||||||
) {
|
) {
|
||||||
final builder = CardCellBuilder(_calendarBloc.cellCache);
|
final events = calenderEvents.map((value) => value.event!).toList();
|
||||||
final cells = calenderEvents.map((value) => value.event!).map((event) {
|
|
||||||
final child = builder.buildCell(cellId: event.cellId);
|
|
||||||
|
|
||||||
return FlowyHover(
|
return CalendarDayCard(
|
||||||
child: GestureDetector(
|
viewId: widget.view.id,
|
||||||
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(
|
|
||||||
isToday: isToday,
|
isToday: isToday,
|
||||||
isInMonth: isInMonth,
|
isInMonth: isInMonth,
|
||||||
|
events: events,
|
||||||
date: date,
|
date: date,
|
||||||
children: cells,
|
rowCache: _calendarBloc.rowCache,
|
||||||
onCreateEvent: (date) {
|
onCreateEvent: (date) {
|
||||||
_calendarBloc.add(
|
_calendarBloc.add(
|
||||||
CalendarEvent.createEvent(
|
CalendarEvent.createEvent(
|
||||||
@ -205,175 +201,9 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _CalendarCard extends StatelessWidget {
|
WeekDays _weekdayFromInt(int dayOfWeek) {
|
||||||
final bool isToday;
|
// MonthView places the first day of week on the second column for some reason.
|
||||||
final bool isInMonth;
|
return WeekDays.values[(dayOfWeek + 1) % 7];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../application/calendar_bloc.dart';
|
||||||
|
import 'calendar_setting.dart';
|
||||||
|
|
||||||
class CalendarToolbar extends StatelessWidget {
|
class CalendarToolbar extends StatelessWidget {
|
||||||
const CalendarToolbar({super.key});
|
const CalendarToolbar({super.key});
|
||||||
@ -10,14 +19,65 @@ class CalendarToolbar extends StatelessWidget {
|
|||||||
height: 40,
|
height: 40,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: const [
|
children: [
|
||||||
FlowyTextButton(
|
_SettingButton(),
|
||||||
"Settings",
|
|
||||||
fillColor: Colors.transparent,
|
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
add(GridEvent.didReceiveGridUpdate(database));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRowsChanged: (rowInfos, reason) {
|
onRowsChanged: (rowInfos, _, reason) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
|
add(GridEvent.didReceiveRowUpdate(rowInfos, reason));
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
required CellIdentifier cellId,
|
required CellIdentifier cellId,
|
||||||
EditableCardNotifier? cellNotifier,
|
EditableCardNotifier? cellNotifier,
|
||||||
CardConfiguration<CustomCardData>? cardConfiguration,
|
CardConfiguration<CustomCardData>? cardConfiguration,
|
||||||
|
Map<FieldType, CardCellStyle>? styles,
|
||||||
}) {
|
}) {
|
||||||
final cellControllerBuilder = CellControllerBuilder(
|
final cellControllerBuilder = CellControllerBuilder(
|
||||||
cellId: cellId,
|
cellId: cellId,
|
||||||
@ -30,6 +31,7 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final key = cellId.key();
|
final key = cellId.key();
|
||||||
|
final style = styles?[cellId.fieldType];
|
||||||
switch (cellId.fieldType) {
|
switch (cellId.fieldType) {
|
||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
return CheckboxCardCell(
|
return CheckboxCardCell(
|
||||||
@ -70,6 +72,7 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
return TextCardCell(
|
return TextCardCell(
|
||||||
cellControllerBuilder: cellControllerBuilder,
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
editableNotifier: cellNotifier,
|
editableNotifier: cellNotifier,
|
||||||
|
style: isStyleOrNull<TextCardCellStyle>(style),
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
case FieldType.URL:
|
case FieldType.URL:
|
||||||
|
@ -24,10 +24,21 @@ class CardConfiguration<CustomCardData> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class CardCell<T> extends StatefulWidget {
|
abstract class CardCellStyle {}
|
||||||
final T? cardData;
|
|
||||||
|
|
||||||
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 {
|
class EditableCardNotifier {
|
||||||
|
@ -9,7 +9,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import '../bloc/select_option_card_cell_bloc.dart';
|
import '../bloc/select_option_card_cell_bloc.dart';
|
||||||
import 'card_cell.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 CellControllerBuilder cellControllerBuilder;
|
||||||
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
|
final CellRenderHook<List<SelectOptionPB>, T>? renderHook;
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
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:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -9,7 +8,14 @@ import '../bloc/text_card_cell_bloc.dart';
|
|||||||
import '../define.dart';
|
import '../define.dart';
|
||||||
import 'card_cell.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
|
@override
|
||||||
final EditableCardNotifier? editableNotifier;
|
final EditableCardNotifier? editableNotifier;
|
||||||
final CellControllerBuilder cellControllerBuilder;
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
@ -17,8 +23,9 @@ class TextCardCell extends CardCell with EditableCell {
|
|||||||
const TextCardCell({
|
const TextCardCell({
|
||||||
required this.cellControllerBuilder,
|
required this.cellControllerBuilder,
|
||||||
this.editableNotifier,
|
this.editableNotifier,
|
||||||
|
TextCardCellStyle? style,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key, style: style);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TextCardCell> createState() => _TextCardCellState();
|
State<TextCardCell> createState() => _TextCardCellState();
|
||||||
@ -129,6 +136,14 @@ class _TextCardCellState extends State<TextCardCell> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _fontSize() {
|
||||||
|
if (widget.style != null) {
|
||||||
|
return widget.style!.fontSize;
|
||||||
|
} else {
|
||||||
|
return 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildText(TextCardCellState state) {
|
Widget _buildText(TextCardCellState state) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
@ -136,7 +151,7 @@ class _TextCardCellState extends State<TextCardCell> {
|
|||||||
),
|
),
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
state.content,
|
state.content,
|
||||||
fontSize: 14,
|
fontSize: _fontSize(),
|
||||||
maxLines: null, // Enable multiple lines
|
maxLines: null, // Enable multiple lines
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -150,7 +165,7 @@ class _TextCardCellState extends State<TextCardCell> {
|
|||||||
onChanged: (value) => focusChanged(),
|
onChanged: (value) => focusChanged(),
|
||||||
onEditingComplete: () => focusNode.unfocus(),
|
onEditingComplete: () => focusNode.unfocus(),
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
style: Theme.of(context).textTheme.bodyMedium!.size(FontSizes.s14),
|
style: Theme.of(context).textTheme.bodyMedium!.size(_fontSize()),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
// Magic number 4 makes the textField take up the same space as FlowyText
|
// Magic number 4 makes the textField take up the same space as FlowyText
|
||||||
contentPadding: EdgeInsets.symmetric(
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
@ -73,7 +73,7 @@ class BoardTestContext {
|
|||||||
BoardTestContext(this.gridView, this._boardDataController);
|
BoardTestContext(this.gridView, this._boardDataController);
|
||||||
|
|
||||||
List<RowInfo> get rowInfos {
|
List<RowInfo> get rowInfos {
|
||||||
return _boardDataController.rowInfos;
|
return _boardDataController.rowCache.rowInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FieldInfo> get fieldContexts => fieldController.fieldInfos;
|
List<FieldInfo> get fieldContexts => fieldController.fieldInfos;
|
||||||
|
@ -26,7 +26,7 @@ class GridTestContext {
|
|||||||
GridTestContext(this.gridView, this.gridController);
|
GridTestContext(this.gridView, this.gridController);
|
||||||
|
|
||||||
List<RowInfo> get rowInfos {
|
List<RowInfo> get rowInfos {
|
||||||
return gridController.rowInfos;
|
return gridController.rowCache.rowInfos;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FieldInfo> get fieldContexts => fieldController.fieldInfos;
|
List<FieldInfo> get fieldContexts => fieldController.fieldInfos;
|
||||||
|
@ -35,8 +35,6 @@ pub enum DatabaseNotification {
|
|||||||
DidUpdateLayoutSettings = 80,
|
DidUpdateLayoutSettings = 80,
|
||||||
// Trigger when the layout field of the database is changed
|
// Trigger when the layout field of the database is changed
|
||||||
DidSetNewLayoutField = 81,
|
DidSetNewLayoutField = 81,
|
||||||
|
|
||||||
DidArrangeCalendarWithNewField = 82,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::default::Default for DatabaseNotification {
|
impl std::default::Default for DatabaseNotification {
|
||||||
|
Loading…
Reference in New Issue
Block a user