feat: create database view on same database (#2829)

* feat: create database view on same database

* feat: switch tag between views

* fix: calendar tool bar

* fix: set layout setting

* chore: update collab rev

* fix: board layout issue

* test: add integration tests

* test: add calendar start from day test
This commit is contained in:
Nathan.fooo
2023-06-20 23:48:34 +08:00
committed by GitHub
parent 79fc7c4cfe
commit e50d708c21
92 changed files with 3006 additions and 1419 deletions

View File

@ -1,5 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/layout/calendar_setting_listener.dart';
import 'package:appflowy/plugins/database_view/application/view/view_cache.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
@ -16,6 +15,7 @@ import 'dart:async';
import 'package:dartz/dartz.dart';
import 'database_view_service.dart';
import 'defines.dart';
import 'layout/layout_service.dart';
import 'layout/layout_setting_listener.dart';
import 'row/row_cache.dart';
import 'group/group_listener.dart';
@ -50,16 +50,11 @@ class DatabaseLayoutSettingCallbacks {
});
}
class CalendarLayoutCallbacks {
final void Function(DatabaseLayoutSettingPB) onCalendarLayoutChanged;
CalendarLayoutCallbacks({required this.onCalendarLayoutChanged});
}
class DatabaseCallbacks {
OnDatabaseChanged? onDatabaseChanged;
OnFieldsChanged? onFieldsChanged;
OnFiltersChanged? onFiltersChanged;
OnSortsChanged? onSortsChanged;
OnNumOfRowsChanged? onNumOfRowsChanged;
OnRowsDeleted? onRowsDeleted;
OnRowsUpdated? onRowsUpdated;
@ -70,6 +65,7 @@ class DatabaseCallbacks {
this.onNumOfRowsChanged,
this.onFieldsChanged,
this.onFiltersChanged,
this.onSortsChanged,
this.onRowsUpdated,
this.onRowsDeleted,
this.onRowsCreated,
@ -80,15 +76,14 @@ class DatabaseController {
final String viewId;
final DatabaseViewBackendService _databaseViewBackendSvc;
final FieldController fieldController;
DatabaseLayoutPB? databaseLayout;
DatabaseLayoutPB databaseLayout;
DatabaseLayoutSettingPB? databaseLayoutSetting;
late DatabaseViewCache _viewCache;
// Callbacks
DatabaseCallbacks? _databaseCallbacks;
GroupCallbacks? _groupCallbacks;
DatabaseLayoutSettingCallbacks? _layoutCallbacks;
CalendarLayoutCallbacks? _calendarLayoutCallbacks;
final List<DatabaseCallbacks> _databaseCallbacks = [];
final List<GroupCallbacks> _groupCallbacks = [];
final List<DatabaseLayoutSettingCallbacks> _layoutCallbacks = [];
// Getters
RowCache get rowCache => _viewCache.rowCache;
@ -96,15 +91,14 @@ class DatabaseController {
// Listener
final DatabaseGroupListener _groupListener;
final DatabaseLayoutSettingListener _layoutListener;
final DatabaseCalendarLayoutListener _calendarLayoutListener;
DatabaseController({required ViewPB view})
: viewId = view.id,
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
fieldController = FieldController(viewId: view.id),
_groupListener = DatabaseGroupListener(view.id),
_layoutListener = DatabaseLayoutSettingListener(view.id),
_calendarLayoutListener = DatabaseCalendarLayoutListener(view.id) {
databaseLayout = databaseLayoutFromViewLayout(view.layout),
_layoutListener = DatabaseLayoutSettingListener(view.id) {
_viewCache = DatabaseViewCache(
viewId: viewId,
fieldController: fieldController,
@ -115,29 +109,30 @@ class DatabaseController {
_listenOnLayoutChanged();
}
void setListener({
void addListener({
DatabaseCallbacks? onDatabaseChanged,
DatabaseLayoutSettingCallbacks? onLayoutChanged,
GroupCallbacks? onGroupChanged,
CalendarLayoutCallbacks? onCalendarLayoutChanged,
}) {
_layoutCallbacks = onLayoutChanged;
_databaseCallbacks = onDatabaseChanged;
_groupCallbacks = onGroupChanged;
_calendarLayoutCallbacks = onCalendarLayoutChanged;
if (onLayoutChanged != null) {
_layoutCallbacks.add(onLayoutChanged);
}
if (onDatabaseChanged != null) {
_databaseCallbacks.add(onDatabaseChanged);
}
if (onGroupChanged != null) {
_groupCallbacks.add(onGroupChanged);
}
}
Future<Either<Unit, FlowyError>> open() async {
return _databaseViewBackendSvc.openGrid().then((result) {
return _databaseViewBackendSvc.openDatabase().then((result) {
return result.fold(
(DatabasePB database) async {
databaseLayout = database.layoutType;
// Listen on layout changed if database layout is calendar
if (databaseLayout == DatabaseLayoutPB.Calendar) {
_listenOnCalendarLayoutChanged();
}
// Load the actual database field data.
final fieldsOrFail = await fieldController.loadFields(
fieldIds: database.fields,
@ -146,7 +141,9 @@ class DatabaseController {
(fields) {
// Notify the database is changed after the fields are loaded.
// The database won't can't be used until the fields are loaded.
_databaseCallbacks?.onDatabaseChanged?.call(database);
for (final callback in _databaseCallbacks) {
callback.onDatabaseChanged?.call(database);
}
_viewCache.rowCache.setInitialRows(database.rows);
return Future(() async {
await _loadGroups();
@ -217,11 +214,14 @@ class DatabaseController {
);
}
Future<void> updateCalenderLayoutSetting(
CalendarLayoutSettingPB layoutSetting,
Future<void> updateLayoutSetting(
CalendarLayoutSettingPB calendarlLayoutSetting,
) async {
await _databaseViewBackendSvc
.updateLayoutSetting(calendarLayoutSetting: layoutSetting)
.updateLayoutSetting(
calendarLayoutSetting: calendarlLayoutSetting,
layoutType: databaseLayout,
)
.then((result) {
result.fold((l) => null, (r) => Log.error(r));
});
@ -232,10 +232,9 @@ class DatabaseController {
await fieldController.dispose();
await _groupListener.stop();
await _viewCache.dispose();
_databaseCallbacks = null;
_groupCallbacks = null;
_layoutCallbacks = null;
_calendarLayoutCallbacks = null;
_databaseCallbacks.clear();
_groupCallbacks.clear();
_layoutCallbacks.clear();
}
Future<void> _loadGroups() async {
@ -243,7 +242,9 @@ class DatabaseController {
return Future(
() => result.fold(
(groups) {
_groupCallbacks?.onGroupByField?.call(groups.items);
for (final callback in _groupCallbacks) {
callback.onGroupByField?.call(groups.items);
}
},
(err) => Log.error(err),
),
@ -251,46 +252,63 @@ class DatabaseController {
}
Future<void> _loadLayoutSetting() async {
if (databaseLayout != null) {
_databaseViewBackendSvc.getLayoutSetting(databaseLayout!).then((result) {
result.fold(
(newDatabaseLayoutSetting) {
databaseLayoutSetting = newDatabaseLayoutSetting;
databaseLayoutSetting?.freeze();
_databaseViewBackendSvc.getLayoutSetting(databaseLayout).then((result) {
result.fold(
(newDatabaseLayoutSetting) {
databaseLayoutSetting = newDatabaseLayoutSetting;
databaseLayoutSetting?.freeze();
_layoutCallbacks?.onLoadLayout(newDatabaseLayoutSetting);
},
(r) => Log.error(r),
);
});
}
for (final callback in _layoutCallbacks) {
callback.onLoadLayout(newDatabaseLayoutSetting);
}
},
(r) => Log.error(r),
);
});
}
void _listenOnRowsChanged() {
final callbacks = DatabaseViewCallbacks(
onNumOfRowsChanged: (rows, rowByRowId, reason) {
_databaseCallbacks?.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
for (final callback in _databaseCallbacks) {
callback.onNumOfRowsChanged?.call(rows, rowByRowId, reason);
}
},
onRowsDeleted: (ids) {
_databaseCallbacks?.onRowsDeleted?.call(ids);
for (final callback in _databaseCallbacks) {
callback.onRowsDeleted?.call(ids);
}
},
onRowsUpdated: (ids, reason) {
_databaseCallbacks?.onRowsUpdated?.call(ids, reason);
for (final callback in _databaseCallbacks) {
callback.onRowsUpdated?.call(ids, reason);
}
},
onRowsCreated: (ids) {
_databaseCallbacks?.onRowsCreated?.call(ids);
for (final callback in _databaseCallbacks) {
callback.onRowsCreated?.call(ids);
}
},
);
_viewCache.setListener(callbacks);
_viewCache.addListener(callbacks);
}
void _listenOnFieldsChanged() {
fieldController.addListener(
onReceiveFields: (fields) {
_databaseCallbacks?.onFieldsChanged?.call(UnmodifiableListView(fields));
for (final callback in _databaseCallbacks) {
callback.onFieldsChanged?.call(UnmodifiableListView(fields));
}
},
onSorts: (sorts) {
for (final callback in _databaseCallbacks) {
callback.onSortsChanged?.call(sorts);
}
},
onFilters: (filters) {
_databaseCallbacks?.onFiltersChanged?.call(filters);
for (final callback in _databaseCallbacks) {
callback.onFiltersChanged?.call(filters);
}
},
);
}
@ -301,15 +319,21 @@ class DatabaseController {
result.fold(
(changeset) {
if (changeset.updateGroups.isNotEmpty) {
_groupCallbacks?.onUpdateGroup?.call(changeset.updateGroups);
for (final callback in _groupCallbacks) {
callback.onUpdateGroup?.call(changeset.updateGroups);
}
}
if (changeset.deletedGroups.isNotEmpty) {
_groupCallbacks?.onDeleteGroup?.call(changeset.deletedGroups);
for (final callback in _groupCallbacks) {
callback.onDeleteGroup?.call(changeset.deletedGroups);
}
}
for (final insertedGroup in changeset.insertedGroups) {
_groupCallbacks?.onInsertGroup?.call(insertedGroup);
for (final callback in _groupCallbacks) {
callback.onInsertGroup?.call(insertedGroup);
}
}
},
(r) => Log.error(r),
@ -318,7 +342,9 @@ class DatabaseController {
onGroupByNewField: (result) {
result.fold(
(groups) {
_groupCallbacks?.onGroupByField?.call(groups);
for (final callback in _groupCallbacks) {
callback.onGroupByField?.call(groups);
}
},
(r) => Log.error(r),
);
@ -330,24 +356,13 @@ class DatabaseController {
_layoutListener.start(
onLayoutChanged: (result) {
result.fold(
(newDatabaseLayoutSetting) {
databaseLayoutSetting = newDatabaseLayoutSetting;
(newLayout) {
databaseLayoutSetting = newLayout;
databaseLayoutSetting?.freeze();
_layoutCallbacks?.onLayoutChanged(newDatabaseLayoutSetting);
},
(r) => Log.error(r),
);
},
);
}
void _listenOnCalendarLayoutChanged() {
_calendarLayoutListener.start(
onCalendarLayoutChanged: (result) {
result.fold(
(l) {
_calendarLayoutCallbacks?.onCalendarLayoutChanged(l);
for (final callback in _layoutCallbacks) {
callback.onLayoutChanged(newLayout);
}
},
(r) => Log.error(r),
);

View File

@ -25,7 +25,7 @@ class DatabaseViewBackendService {
.then((value) => value.leftMap((l) => l.value));
}
Future<Either<DatabasePB, FlowyError>> openGrid() async {
Future<Either<DatabasePB, FlowyError>> openDatabase() async {
final payload = DatabaseViewIdPB(value: viewId);
return DatabaseEventGetDatabase(payload).send();
}
@ -113,9 +113,12 @@ class DatabaseViewBackendService {
}
Future<Either<Unit, FlowyError>> updateLayoutSetting({
required DatabaseLayoutPB layoutType,
CalendarLayoutSettingPB? calendarLayoutSetting,
}) {
final payload = LayoutSettingChangesetPB.create()..viewId = viewId;
final payload = LayoutSettingChangesetPB.create()
..viewId = viewId
..layoutType = layoutType;
if (calendarLayoutSetting != null) {
payload.calendar = calendarLayoutSetting;
}

View File

@ -1,5 +1,6 @@
import 'dart:collection';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -10,6 +11,7 @@ import 'row/row_service.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnSortsChanged = void Function(List<SortInfo>);
typedef OnDatabaseChanged = void Function(DatabasePB);
typedef OnRowsCreated = void Function(List<RowId> ids);

View File

@ -15,13 +15,13 @@ class DatabaseLayoutBackendService {
}) {
final payload = UpdateViewPayloadPB.create()
..viewId = viewId
..layout = _viewLayoutFromDatabaseLayout(layout);
..layout = viewLayoutFromDatabaseLayout(layout);
return FolderEventUpdateView(payload).send();
}
}
ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
ViewLayoutPB viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
switch (databaseLayout) {
case DatabaseLayoutPB.Board:
return ViewLayoutPB.Board;
@ -33,3 +33,16 @@ ViewLayoutPB _viewLayoutFromDatabaseLayout(DatabaseLayoutPB databaseLayout) {
throw UnimplementedError;
}
}
DatabaseLayoutPB databaseLayoutFromViewLayout(ViewLayoutPB viewLayout) {
switch (viewLayout) {
case ViewLayoutPB.Board:
return DatabaseLayoutPB.Board;
case ViewLayoutPB.Calendar:
return DatabaseLayoutPB.Calendar;
case ViewLayoutPB.Grid:
return DatabaseLayoutPB.Grid;
default:
throw UnimplementedError;
}
}

View File

@ -1,103 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dartz/dartz.dart';
part 'setting_bloc.freezed.dart';
class DatabaseSettingBloc
extends Bloc<DatabaseSettingEvent, DatabaseSettingState> {
final String viewId;
DatabaseSettingBloc({required this.viewId})
: super(DatabaseSettingState.initial()) {
on<DatabaseSettingEvent>(
(event, emit) async {
event.map(
performAction: (_PerformAction value) {
emit(state.copyWith(selectedAction: Some(value.action)));
},
);
},
);
}
}
@freezed
class DatabaseSettingEvent with _$DatabaseSettingEvent {
const factory DatabaseSettingEvent.performAction(
DatabaseSettingAction action,
) = _PerformAction;
}
@freezed
class DatabaseSettingState with _$DatabaseSettingState {
const factory DatabaseSettingState({
required Option<DatabaseSettingAction> selectedAction,
}) = _DatabaseSettingState;
factory DatabaseSettingState.initial() => DatabaseSettingState(
selectedAction: none(),
);
}
enum DatabaseSettingAction {
showProperties,
showLayout,
showGroup,
showCalendarLayout,
}
extension DatabaseSettingActionExtension on DatabaseSettingAction {
String iconName() {
switch (this) {
case DatabaseSettingAction.showProperties:
return 'grid/setting/properties';
case DatabaseSettingAction.showLayout:
return 'grid/setting/database_layout';
case DatabaseSettingAction.showGroup:
return 'grid/setting/group';
case DatabaseSettingAction.showCalendarLayout:
return 'grid/setting/calendar_layout';
}
}
String title() {
switch (this) {
case DatabaseSettingAction.showProperties:
return LocaleKeys.grid_settings_Properties.tr();
case DatabaseSettingAction.showLayout:
return LocaleKeys.grid_settings_databaseLayout.tr();
case DatabaseSettingAction.showGroup:
return LocaleKeys.grid_settings_group.tr();
case DatabaseSettingAction.showCalendarLayout:
return LocaleKeys.calendar_settings_name.tr();
}
}
}
/// Returns the list of actions that should be shown for the given database layout.
List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showGroup,
];
case DatabaseLayoutPB.Calendar:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showCalendarLayout,
];
case DatabaseLayoutPB.Grid:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
];
default:
return [];
}
}

View File

@ -0,0 +1,290 @@
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tar_bar_add_button.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'database_controller.dart';
import 'database_view_service.dart';
part 'tar_bar_bloc.freezed.dart';
class GridTabBarBloc extends Bloc<GridTabBarEvent, GridTabBarState> {
GridTabBarBloc({
bool isInlineView = false,
required ViewPB view,
}) : super(GridTabBarState.initial(view)) {
on<GridTabBarEvent>(
(event, emit) async {
event.when(
initial: () {
_listenInlineViewChanged();
_loadChildView();
},
didLoadChildViews: (List<ViewPB> childViews) {
emit(
state.copyWith(
tabBars: [
...state.tabBars,
...childViews.map(
(newChildView) => TarBar(view: newChildView),
),
],
tabBarControllerByViewId: _extendsTabBarController(childViews),
),
);
},
selectView: (String viewId) {
final index =
state.tabBars.indexWhere((element) => element.viewId == viewId);
if (index != -1) {
emit(
state.copyWith(selectedIndex: index),
);
}
},
createView: (action) {
_createLinkedView(action.name, action.layoutType);
},
deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId);
result.fold(
(l) {},
(r) => Log.error(r),
);
},
renameView: (String viewId, String newName) {
ViewBackendService.updateView(viewId: viewId, name: newName);
},
didUpdateChildViews: (updatePB) async {
if (updatePB.createChildViews.isNotEmpty) {
final allTabBars = [
...state.tabBars,
...updatePB.createChildViews.map((e) => TarBar(view: e))
];
emit(
state.copyWith(
tabBars: allTabBars,
selectedIndex: state.tabBars.length,
tabBarControllerByViewId:
_extendsTabBarController(updatePB.createChildViews),
),
);
}
if (updatePB.deleteChildViews.isNotEmpty) {
final allTabBars = [...state.tabBars];
final tabBarControllerByViewId = {
...state.tabBarControllerByViewId
};
var newSelectedIndex = state.selectedIndex;
for (final viewId in updatePB.deleteChildViews) {
final index = allTabBars.indexWhere(
(element) => element.viewId == viewId,
);
if (index != -1) {
final tarBar = allTabBars.removeAt(index);
// Dispose the controller when the tab is removed.
final controller =
tabBarControllerByViewId.remove(tarBar.viewId);
controller?.dispose();
}
if (index == state.selectedIndex) {
if (index > 0 && allTabBars.isNotEmpty) {
newSelectedIndex = index - 1;
}
}
}
emit(
state.copyWith(
tabBars: allTabBars,
selectedIndex: newSelectedIndex,
tabBarControllerByViewId: tabBarControllerByViewId,
),
);
}
},
viewDidUpdate: (ViewPB updatedView) {
final index = state.tabBars.indexWhere(
(element) => element.viewId == updatedView.id,
);
if (index != -1) {
final allTabBars = [...state.tabBars];
final updatedTabBar = TarBar(view: updatedView);
allTabBars[index] = updatedTabBar;
emit(state.copyWith(tabBars: allTabBars));
}
},
);
},
);
}
@override
Future<void> close() async {
for (final tabBar in state.tabBars) {
await state.tabBarControllerByViewId[tabBar.viewId]?.dispose();
}
return super.close();
}
void _listenInlineViewChanged() {
final controller = state.tabBarControllerByViewId[state.parentView.id];
controller?.onViewUpdated = (newView) {
add(GridTabBarEvent.viewDidUpdate(newView));
};
// Only listen the child view changes when the parent view is inline.
controller?.onViewChildViewChanged = (update) {
add(GridTabBarEvent.didUpdateChildViews(update));
};
}
/// Create tab bar controllers for the new views and return the updated map.
Map<String, DatabaseTarBarController> _extendsTabBarController(
List<ViewPB> newViews,
) {
final tabBarControllerByViewId = {...state.tabBarControllerByViewId};
for (final view in newViews) {
final controller = DatabaseTarBarController(view: view);
controller.onViewUpdated = (newView) {
add(GridTabBarEvent.viewDidUpdate(newView));
};
tabBarControllerByViewId[view.id] = controller;
}
return tabBarControllerByViewId;
}
Future<void> _createLinkedView(String name, ViewLayoutPB layoutType) async {
final viewId = state.parentView.id;
final databaseIdOrError =
await DatabaseViewBackendService(viewId: viewId).getDatabaseId();
databaseIdOrError.fold(
(databaseId) async {
final linkedViewOrError =
await ViewBackendService.createDatabaseLinkedView(
parentViewId: viewId,
databaseId: databaseId,
layoutType: layoutType,
name: name,
);
linkedViewOrError.fold(
(linkedView) {},
(err) => Log.error(err),
);
},
(r) => Log.error(r),
);
}
Future<void> _loadChildView() async {
ViewBackendService.getChildViews(viewId: state.parentView.id)
.then((viewsOrFail) {
if (isClosed) {
return;
}
viewsOrFail.fold(
(views) => add(GridTabBarEvent.didLoadChildViews(views)),
(err) => Log.error(err),
);
});
}
}
@freezed
class GridTabBarEvent with _$GridTabBarEvent {
const factory GridTabBarEvent.initial() = _Initial;
const factory GridTabBarEvent.didLoadChildViews(
List<ViewPB> childViews,
) = _DidLoadChildViews;
const factory GridTabBarEvent.selectView(String viewId) = _DidSelectView;
const factory GridTabBarEvent.createView(AddButtonAction action) =
_CreateView;
const factory GridTabBarEvent.renameView(String viewId, String newName) =
_RenameView;
const factory GridTabBarEvent.deleteView(String viewId) = _DeleteView;
const factory GridTabBarEvent.didUpdateChildViews(
ChildViewUpdatePB updatePB,
) = _DidUpdateChildViews;
const factory GridTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate;
}
@freezed
class GridTabBarState with _$GridTabBarState {
const factory GridTabBarState({
required ViewPB parentView,
required int selectedIndex,
required List<TarBar> tabBars,
required Map<String, DatabaseTarBarController> tabBarControllerByViewId,
}) = _GridTabBarState;
factory GridTabBarState.initial(ViewPB view) {
final tabBar = TarBar(view: view);
return GridTabBarState(
parentView: view,
selectedIndex: 0,
tabBars: [tabBar],
tabBarControllerByViewId: {
view.id: DatabaseTarBarController(
view: view,
)
},
);
}
}
class TarBar extends Equatable {
final ViewPB view;
final DatabaseTabBarItemBuilder _builder;
String get viewId => view.id;
DatabaseTabBarItemBuilder get builder => _builder;
ViewLayoutPB get layout => view.layout;
TarBar({
required this.view,
}) : _builder = view.tarBarItem();
@override
List<Object?> get props => [view.hashCode];
}
typedef OnViewUpdated = void Function(ViewPB newView);
typedef OnViewChildViewChanged = void Function(
ChildViewUpdatePB childViewUpdate,
);
class DatabaseTarBarController {
ViewPB view;
final DatabaseController controller;
final ViewListener viewListener;
OnViewUpdated? onViewUpdated;
OnViewChildViewChanged? onViewChildViewChanged;
DatabaseTarBarController({
required this.view,
}) : controller = DatabaseController(view: view),
viewListener = ViewListener(viewId: view.id) {
viewListener.start(
onViewChildViewsUpdated: (update) {
onViewChildViewChanged?.call(update);
},
onViewUpdated: (newView) {
view = newView;
onViewUpdated?.call(newView);
},
);
}
Future<void> dispose() async {
await viewListener.stop();
await controller.dispose();
}
}

View File

@ -35,7 +35,7 @@ class DatabaseViewCache {
final String viewId;
late RowCache _rowCache;
final DatabaseViewListener _databaseViewListener;
DatabaseViewCallbacks? _callbacks;
final List<DatabaseViewCallbacks> _callbacks = [];
UnmodifiableListView<RowInfo> get rowInfos => _rowCache.rowInfos;
RowCache get rowCache => _rowCache;
@ -61,22 +61,28 @@ class DatabaseViewCache {
_rowCache.applyRowsChanged(changeset);
if (changeset.deletedRows.isNotEmpty) {
_callbacks?.onRowsDeleted?.call(changeset.deletedRows);
for (final callback in _callbacks) {
callback.onRowsDeleted?.call(changeset.deletedRows);
}
}
if (changeset.updatedRows.isNotEmpty) {
_callbacks?.onRowsUpdated?.call(
changeset.updatedRows.map((e) => e.rowId).toList(),
_rowCache.changeReason,
);
for (final callback in _callbacks) {
callback.onRowsUpdated?.call(
changeset.updatedRows.map((e) => e.rowId).toList(),
_rowCache.changeReason,
);
}
}
if (changeset.insertedRows.isNotEmpty) {
_callbacks?.onRowsCreated?.call(
changeset.insertedRows
.map((insertedRow) => insertedRow.rowMeta.id)
.toList(),
);
for (final callback in _callbacks) {
callback.onRowsCreated?.call(
changeset.insertedRows
.map((insertedRow) => insertedRow.rowMeta.id)
.toList(),
);
}
}
},
(err) => Log.error(err),
@ -103,21 +109,25 @@ class DatabaseViewCache {
);
_rowCache.onRowsChanged(
(reason) => _callbacks?.onNumOfRowsChanged?.call(
rowInfos,
_rowCache.rowByRowId,
reason,
),
(reason) {
for (final callback in _callbacks) {
callback.onNumOfRowsChanged?.call(
rowInfos,
_rowCache.rowByRowId,
reason,
);
}
},
);
}
Future<void> dispose() async {
await _databaseViewListener.stop();
await _rowCache.dispose();
_callbacks = null;
_callbacks.clear();
}
void setListener(DatabaseViewCallbacks callbacks) {
_callbacks = callbacks;
void addListener(DatabaseViewCallbacks callbacks) {
_callbacks.add(callbacks);
}
}

View File

@ -28,9 +28,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
FieldController get fieldController => databaseController.fieldController;
String get viewId => databaseController.viewId;
BoardBloc({required ViewPB view})
: databaseController = DatabaseController(view: view),
super(BoardState.initial(view.id)) {
BoardBloc({
required ViewPB view,
required this.databaseController,
}) : super(BoardState.initial(view.id)) {
boardController = AppFlowyBoardController(
onMoveGroup: (
fromGroupId,
@ -166,7 +167,6 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
@override
Future<void> close() async {
await databaseController.dispose();
for (final controller in groupControllers.values) {
controller.dispose();
}
@ -233,7 +233,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
},
);
databaseController.setListener(
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onGroupChanged: onGroupChanged,
);

View File

@ -1,19 +1,14 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'presentation/board_page.dart';
class BoardPluginBuilder implements PluginBuilder {
@override
Plugin build(dynamic data) {
if (data is ViewPB) {
return BoardPlugin(pluginType: pluginType, view: data);
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
} else {
throw FlowyPluginException.invalidData;
}
@ -36,55 +31,3 @@ class BoardPluginConfig implements PluginConfig {
@override
bool get creatable => true;
}
class BoardPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
BoardPlugin({
required ViewPB view,
required PluginType pluginType,
bool listenOnViewChanged = false,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
);
@override
PluginWidgetBuilder get widgetBuilder =>
BoardPluginWidgetBuilder(notifier: notifier);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class BoardPluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
BoardPluginWidgetBuilder({required this.notifier, Key? key});
ViewPB get view => notifier.view;
@override
Widget get leftBarItem => ViewLeftBarItem(view: view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(view, deletedView.index);
}
});
});
return BoardPage(key: ValueKey(view.id), view: view);
}
@override
List<NavigationItem> get navigationItems => [this];
}

View File

@ -3,9 +3,11 @@
import 'dart:collection';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
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/tar_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@ -24,11 +26,48 @@ import '../../widgets/card/card_cell_builder.dart';
import '../../widgets/row/cell_builder.dart';
import '../application/board_bloc.dart';
import '../../widgets/card/card.dart';
import 'toolbar/board_toolbar.dart';
import 'toolbar/board_setting_bar.dart';
class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
@override
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
) {
return BoardPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return BoardSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
);
}
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return SizedBox.fromSize();
}
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
}
class BoardPage extends StatelessWidget {
final DatabaseController databaseController;
BoardPage({
required this.view,
required this.databaseController,
Key? key,
this.onEditStateChanged,
}) : super(key: ValueKey(view.id));
@ -41,8 +80,10 @@ class BoardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
BoardBloc(view: view)..add(const BoardEvent.initial()),
create: (context) => BoardBloc(
view: view,
databaseController: databaseController,
)..add(const BoardEvent.initial()),
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (p, c) => p.loadingState != c.loadingState,
builder: (context, state) {
@ -110,14 +151,9 @@ class _BoardContentState extends State<BoardContent> {
child: BlocBuilder<BoardBloc, BoardState>(
buildWhen: (previous, current) => previous.groupIds != current.groupIds,
builder: (context, state) {
final column = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: column,
child: _buildBoard(context),
);
},
),
@ -125,22 +161,20 @@ class _BoardContentState extends State<BoardContent> {
}
Widget _buildBoard(BuildContext context) {
return Expanded(
child: AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: ScrollController(),
controller: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
),
return AppFlowyBoard(
boardScrollController: scrollManager,
scrollController: ScrollController(),
controller: context.read<BoardBloc>().boardController,
headerBuilder: _buildHeader,
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context,
column,
columnItem,
),
groupConstraints: const BoxConstraints.tightFor(width: 300),
config: AppFlowyBoardConfig(
groupBackgroundColor: Theme.of(context).colorScheme.surfaceVariant,
),
);
}
@ -335,17 +369,6 @@ class _BoardContentState extends State<BoardContent> {
}
}
class _ToolbarBlocAdaptor extends StatelessWidget {
const _ToolbarBlocAdaptor({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<BoardBloc, BoardState>(
builder: (context, state) => const BoardToolbar(),
);
}
}
Widget? _buildHeaderIcon(GroupData customData) {
Widget? widget;
switch (customData.fieldType) {

View File

@ -1,10 +1,11 @@
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BoardToolbar extends StatelessWidget {
const BoardToolbar({
class BoardSettingBar extends StatelessWidget {
final DatabaseController databaseController;
const BoardSettingBar({
required this.databaseController,
Key? key,
}) : super(key: key);
@ -15,9 +16,7 @@ class BoardToolbar extends StatelessWidget {
child: Row(
children: [
const Spacer(),
SettingButton(
databaseController: context.read<BoardBloc>().databaseController,
),
SettingButton(databaseController: databaseController),
],
),
);

View File

@ -27,9 +27,8 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
CellCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
CalendarBloc({required ViewPB view})
: databaseController = DatabaseController(view: view),
super(CalendarState.initial()) {
CalendarBloc({required ViewPB view, required this.databaseController})
: super(CalendarState.initial()) {
on<CalendarEvent>(
(event, emit) async {
await event.when(
@ -39,6 +38,12 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
_loadAllEvents();
},
didReceiveCalendarSettings: (CalendarLayoutSettingPB settings) {
// If the field id changed, reload all events
state.settings.fold(() => null, (oldSetting) {
if (oldSetting.fieldId != settings.fieldId) {
_loadAllEvents();
}
});
emit(state.copyWith(settings: Some(settings)));
},
didReceiveDatabaseUpdate: (DatabasePB database) {
@ -53,10 +58,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
),
);
},
didReceiveNewLayoutField: (CalendarLayoutSettingPB layoutSettings) {
_loadAllEvents();
emit(state.copyWith(settings: Some(layoutSettings)));
},
createEvent: (DateTime date, String title) async {
await _createEvent(date, title);
},
@ -105,12 +106,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
);
}
@override
Future<void> close() async {
await databaseController.dispose();
return super.close();
}
FieldInfo? _getCalendarFieldInfo(String fieldId) {
final fieldInfos = databaseController.fieldController.fieldInfos;
final index = fieldInfos.indexWhere(
@ -149,7 +144,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
Future<void> _createEvent(DateTime date, String title) async {
return state.settings.fold(
() => null,
() {
Log.warn('Calendar settings not found');
},
(settings) async {
final dateField = _getCalendarFieldInfo(settings.fieldId);
final titleField = _getTitleFieldInfo();
@ -207,7 +204,7 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
Future<void> _updateCalendarLayoutSetting(
CalendarLayoutSettingPB layoutSetting,
) async {
return databaseController.updateCalenderLayoutSetting(layoutSetting);
return databaseController.updateLayoutSetting(layoutSetting);
}
Future<CalendarEventData<CalendarDayEvent>?> _loadEvent(RowId rowId) async {
@ -333,14 +330,9 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
onLoadLayout: _didReceiveLayoutSetting,
);
final onCalendarLayoutFieldChanged = CalendarLayoutCallbacks(
onCalendarLayoutChanged: _didReceiveNewLayoutField,
);
databaseController.setListener(
databaseController.addListener(
onDatabaseChanged: onDatabaseChanged,
onLayoutChanged: onLayoutChanged,
onCalendarLayoutChanged: onCalendarLayoutFieldChanged,
);
}
@ -353,13 +345,6 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
}
}
void _didReceiveNewLayoutField(DatabaseLayoutSettingPB layoutSetting) {
if (layoutSetting.hasCalendar()) {
if (isClosed) return;
add(CalendarEvent.didReceiveNewLayoutField(layoutSetting.calendar));
}
}
bool isEventDayChanged(CalendarEventData<CalendarDayEvent> event) {
final index = state.allEvents.indexWhere(
(element) => element.event!.eventId == event.event!.eventId,
@ -426,10 +411,6 @@ class CalendarEvent with _$CalendarEvent {
const factory CalendarEvent.didReceiveDatabaseUpdate(DatabasePB database) =
_ReceiveDatabaseUpdate;
const factory CalendarEvent.didReceiveNewLayoutField(
CalendarLayoutSettingPB layoutSettings,
) = _DidReceiveNewLayoutField;
}
@freezed

View File

@ -0,0 +1,167 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:dartz/dartz.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../application/database_controller.dart';
import '../../application/row/row_cache.dart';
part 'unschedule_event_bloc.freezed.dart';
class UnscheduleEventsBloc
extends Bloc<UnscheduleEventsEvent, UnscheduleEventsState> {
final DatabaseController databaseController;
Map<String, FieldInfo> fieldInfoByFieldId = {};
// Getters
String get viewId => databaseController.viewId;
FieldController get fieldController => databaseController.fieldController;
CellCache get cellCache => databaseController.rowCache.cellCache;
RowCache get rowCache => databaseController.rowCache;
UnscheduleEventsBloc({
required this.databaseController,
}) : super(UnscheduleEventsState.initial()) {
on<UnscheduleEventsEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
_loadAllEvents();
},
didLoadAllEvents: (events) {
emit(
state.copyWith(
allEvents: events,
unscheduleEvents:
events.where((element) => !element.isScheduled).toList(),
),
);
},
didDeleteEvents: (List<RowId> deletedRowIds) {
final events = [...state.allEvents];
events.retainWhere(
(element) => !deletedRowIds.contains(element.rowMeta.id),
);
emit(
state.copyWith(
allEvents: events,
unscheduleEvents:
events.where((element) => !element.isScheduled).toList(),
),
);
},
didReceiveEvent: (CalendarEventPB event) {
emit(
state.copyWith(
allEvents: [...state.allEvents, event],
),
);
},
);
},
);
}
Future<CalendarEventPB?> _loadEvent(
RowId rowId,
) async {
final payload = RowIdPB(viewId: viewId, rowId: rowId);
return DatabaseEventGetCalendarEvent(payload).send().then(
(result) => result.fold(
(eventPB) => eventPB,
(r) {
Log.error(r);
return null;
},
),
);
}
Future<void> _loadAllEvents() async {
final payload = CalendarEventRequestPB.create()..viewId = viewId;
DatabaseEventGetAllCalendarEvents(payload).send().then((result) {
result.fold(
(events) {
if (!isClosed) {
add(UnscheduleEventsEvent.didLoadAllEvents(events.items));
}
},
(r) => Log.error(r),
);
});
}
void _startListening() {
final onDatabaseChanged = DatabaseCallbacks(
onRowsCreated: (rowIds) async {
if (isClosed) {
return;
}
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null && !isClosed) {
add(UnscheduleEventsEvent.didReceiveEvent(event));
}
}
},
onRowsDeleted: (rowIds) {
if (isClosed) {
return;
}
add(UnscheduleEventsEvent.didDeleteEvents(rowIds));
},
onRowsUpdated: (rowIds, reason) async {
if (isClosed) {
return;
}
for (final id in rowIds) {
final event = await _loadEvent(id);
if (event != null) {
add(UnscheduleEventsEvent.didDeleteEvents([id]));
add(UnscheduleEventsEvent.didReceiveEvent(event));
}
}
},
);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
}
}
@freezed
class UnscheduleEventsEvent with _$UnscheduleEventsEvent {
const factory UnscheduleEventsEvent.initial() = _InitialCalendar;
// Called after loading all the current evnets
const factory UnscheduleEventsEvent.didLoadAllEvents(
List<CalendarEventPB> events,
) = _ReceiveUnscheduleEventsEvents;
const factory UnscheduleEventsEvent.didDeleteEvents(List<RowId> rowIds) =
_DidDeleteEvents;
const factory UnscheduleEventsEvent.didReceiveEvent(
CalendarEventPB event,
) = _DidReceiveEvent;
}
@freezed
class UnscheduleEventsState with _$UnscheduleEventsState {
const factory UnscheduleEventsState({
required Option<DatabasePB> database,
required List<CalendarEventPB> allEvents,
required List<CalendarEventPB> unscheduleEvents,
}) = _UnscheduleEventsState;
factory UnscheduleEventsState.initial() => UnscheduleEventsState(
database: none(),
allEvents: [],
unscheduleEvents: [],
);
}

View File

@ -1,19 +1,14 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import '../../util.dart';
import 'presentation/calendar_page.dart';
class CalendarPluginBuilder extends PluginBuilder {
@override
Plugin build(dynamic data) {
if (data is ViewPB) {
return CalendarPlugin(pluginType: pluginType, view: data);
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
} else {
throw FlowyPluginException.invalidData;
}
@ -36,55 +31,3 @@ class CalendarPluginConfig implements PluginConfig {
@override
bool get creatable => true;
}
class CalendarPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
CalendarPlugin({
required ViewPB view,
required PluginType pluginType,
bool listenOnViewChanged = false,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
);
@override
PluginWidgetBuilder get widgetBuilder =>
CalendarPluginWidgetBuilder(notifier: notifier);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class CalendarPluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
CalendarPluginWidgetBuilder({required this.notifier, Key? key});
ViewPB get view => notifier.view;
@override
Widget get leftBarItem => ViewLeftBarItem(view: view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(view, deletedView.index);
}
});
});
return CalendarPage(key: ValueKey(view.id), view: view);
}
@override
List<NavigationItem> get navigationItems => [this];
}

View File

@ -309,7 +309,7 @@ class _EventCard extends StatelessWidget {
cellBuilder: cellBuilder,
openCard: (context) => showEventDetails(
context: context,
event: event,
event: event.event,
viewId: viewId,
rowCache: rowCache,
),

View File

@ -1,5 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
@ -15,11 +18,51 @@ import '../../widgets/row/cell_builder.dart';
import '../../widgets/row/row_detail.dart';
import 'calendar_day.dart';
import 'layout/sizes.dart';
import 'toolbar/calendar_toolbar.dart';
import 'toolbar/calendar_setting_bar.dart';
class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
@override
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
) {
return CalendarPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return CalendarSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
);
}
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return SizedBox.fromSize();
}
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
}
class CalendarPage extends StatefulWidget {
final ViewPB view;
const CalendarPage({required this.view, super.key});
final DatabaseController databaseController;
const CalendarPage({
required this.view,
required this.databaseController,
super.key,
});
@override
State<CalendarPage> createState() => _CalendarPageState();
@ -33,8 +76,10 @@ class _CalendarPageState extends State<CalendarPage> {
@override
void initState() {
_calendarState = GlobalKey<MonthViewState>();
_calendarBloc = CalendarBloc(view: widget.view)
..add(const CalendarEvent.initial());
_calendarBloc = CalendarBloc(
view: widget.view,
databaseController: widget.databaseController,
)..add(const CalendarEvent.initial());
super.initState();
}
@ -79,7 +124,7 @@ class _CalendarPageState extends State<CalendarPage> {
if (state.editingEvent != null) {
showEventDetails(
context: context,
event: state.editingEvent!.event!,
event: state.editingEvent!.event!.event,
viewId: widget.view.id,
rowCache: _calendarBloc.rowCache,
);
@ -115,8 +160,6 @@ class _CalendarPageState extends State<CalendarPage> {
builder: (context, state) {
return Column(
children: [
// const _ToolbarBlocAdaptor(),
const CalendarToolbar(),
_buildCalendar(
_eventController,
state.settings
@ -238,12 +281,12 @@ class _CalendarPageState extends State<CalendarPage> {
void showEventDetails({
required BuildContext context,
required CalendarDayEvent event,
required CalendarEventPB event,
required String viewId,
required RowCache rowCache,
}) {
final dataController = RowController(
rowMeta: event.event.rowMeta,
rowMeta: event.rowMeta,
viewId: viewId,
rowCache: rowCache,
);

View File

@ -351,21 +351,16 @@ class FirstDayOfWeek extends StatelessWidget {
final symbols =
DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
// starts from sunday
final items = symbols.WEEKDAYS.asMap().entries.map((entry) {
final index = entry.key;
final string = entry.value;
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(string),
onTap: () {
onUpdated(index);
popoverMutex.close();
},
rightIcon: firstDayOfWeek == index
? const FlowySvg(name: 'grid/checkmark')
: null,
),
const len = 2;
final items = symbols.WEEKDAYS.take(len).indexed.map((entry) {
return StartFromButton(
title: entry.$2,
dayIndex: entry.$1,
isSelected: firstDayOfWeek == entry.$1,
onTap: (index) {
onUpdated(index);
popoverMutex.close();
},
);
}).toList();
@ -376,7 +371,7 @@ class FirstDayOfWeek extends StatelessWidget {
itemBuilder: (context, index) => items[index],
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
itemCount: 2,
itemCount: len,
),
);
},
@ -426,3 +421,29 @@ enum CalendarLayoutSettingAction {
showWeekNumber,
showTimeLine,
}
class StartFromButton extends StatelessWidget {
final int dayIndex;
final String title;
final bool isSelected;
final void Function(int) onTap;
const StartFromButton({
required this.title,
required this.dayIndex,
required this.onTap,
required this.isSelected,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(title),
onTap: () => onTap(dayIndex),
rightIcon: isSelected ? const FlowySvg(name: 'grid/checkmark') : null,
),
);
}
}

View File

@ -0,0 +1,178 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/calendar/application/unschedule_event_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CalendarSettingBar extends StatelessWidget {
final DatabaseController databaseController;
const CalendarSettingBar({
required this.databaseController,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
UnscheduleEventsButton(databaseController: databaseController),
SettingButton(
databaseController: databaseController,
),
],
),
);
}
}
class UnscheduleEventsButton extends StatefulWidget {
final DatabaseController databaseController;
const UnscheduleEventsButton({
required this.databaseController,
Key? key,
}) : super(key: key);
@override
State<UnscheduleEventsButton> createState() => _UnscheduleEventsButtonState();
}
class _UnscheduleEventsButtonState extends State<UnscheduleEventsButton> {
late final PopoverController _popoverController;
late final UnscheduleEventsBloc _bloc;
@override
void initState() {
super.initState();
_bloc = UnscheduleEventsBloc(databaseController: widget.databaseController)
..add(const UnscheduleEventsEvent.initial());
_popoverController = PopoverController();
}
@override
dispose() {
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _popoverController,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
child: BlocProvider.value(
value: _bloc,
child: BlocBuilder<UnscheduleEventsBloc, UnscheduleEventsState>(
buildWhen: (previous, current) =>
previous.unscheduleEvents.length !=
current.unscheduleEvents.length,
builder: (context, state) {
return FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
);
},
),
),
popupBuilder: (context) {
return UnscheduleEventsList(
viewId: _bloc.viewId,
rowCache: _bloc.rowCache,
controller: _popoverController,
unscheduleEvents: _bloc.state.unscheduleEvents,
);
},
);
}
}
class UnscheduleEventsList extends StatelessWidget {
final String viewId;
final RowCache rowCache;
final PopoverController controller;
final List<CalendarEventPB> unscheduleEvents;
const UnscheduleEventsList({
required this.viewId,
required this.controller,
required this.unscheduleEvents,
required this.rowCache,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
LocaleKeys.calendar_settings_clickToAdd.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
const VSpace(6),
...unscheduleEvents.map(
(e) => UnscheduledEventCell(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e,
viewId: viewId,
rowCache: rowCache,
);
controller.close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
}
}
class UnscheduledEventCell extends StatelessWidget {
final CalendarEventPB event;
final VoidCallback onPressed;
const UnscheduledEventCell({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
),
onTap: onPressed,
),
);
}
}

View File

@ -1,136 +0,0 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../application/calendar_bloc.dart';
class CalendarToolbar extends StatelessWidget {
const CalendarToolbar({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const _UnscheduleEventsButton(),
SettingButton(
databaseController: context.read<CalendarBloc>().databaseController,
),
],
),
);
}
}
class _UnscheduleEventsButton extends StatefulWidget {
const _UnscheduleEventsButton({Key? key}) : super(key: key);
@override
State<_UnscheduleEventsButton> createState() =>
_UnscheduleEventsButtonState();
}
class _UnscheduleEventsButtonState extends State<_UnscheduleEventsButton> {
late final PopoverController _controller;
@override
void initState() {
super.initState();
_controller = PopoverController();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
final unscheduledEvents = state.allEvents
.where((e) => e.date == DateTime.fromMillisecondsSinceEpoch(0))
.toList();
final viewId = context.read<CalendarBloc>().viewId;
final rowCache = context.read<CalendarBloc>().rowCache;
return AppFlowyPopover(
direction: PopoverDirection.bottomWithCenterAligned,
controller: _controller,
offset: const Offset(0, 8),
constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600),
child: FlowyTextButton(
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${unscheduledEvents.length})",
fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: GridSize.typeOptionContentInsets,
),
popupBuilder: (context) {
final cells = <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
// LocaleKeys.calendar_settings_noDateHint.tr(),
LocaleKeys.calendar_settings_clickToAdd.tr(),
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
),
),
const VSpace(6),
...unscheduledEvents.map(
(e) => _UnscheduledEventItem(
event: e,
onPressed: () {
showEventDetails(
context: context,
event: e.event!,
viewId: viewId,
rowCache: rowCache,
);
_controller.close();
},
),
)
];
return ListView.separated(
itemBuilder: (context, index) => cells[index],
itemCount: cells.length,
separatorBuilder: (context, index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
shrinkWrap: true,
);
},
);
},
);
}
}
class _UnscheduledEventItem extends StatelessWidget {
final CalendarEventData<CalendarDayEvent> event;
final VoidCallback onPressed;
const _UnscheduledEventItem({
required this.event,
required this.onPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
text: FlowyText.medium(
event.title.isEmpty
? LocaleKeys.calendar_defaultNewCalendarTitle.tr()
: event.title,
),
onTap: onPressed,
),
);
}
}

View File

@ -3,17 +3,17 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'grid_accessory_bloc.freezed.dart';
class GridAccessoryMenuBloc
extends Bloc<GridAccessoryMenuEvent, GridAccessoryMenuState> {
class DatabaseViewSettingExtensionBloc extends Bloc<
DatabaseViewSettingExtensionEvent, DatabaseViewSettingExtensionState> {
final String viewId;
GridAccessoryMenuBloc({required this.viewId})
DatabaseViewSettingExtensionBloc({required this.viewId})
: super(
GridAccessoryMenuState.initial(
DatabaseViewSettingExtensionState.initial(
viewId,
),
) {
on<GridAccessoryMenuEvent>(
on<DatabaseViewSettingExtensionEvent>(
(event, emit) async {
event.when(
initial: () {},
@ -27,22 +27,25 @@ class GridAccessoryMenuBloc
}
@freezed
class GridAccessoryMenuEvent with _$GridAccessoryMenuEvent {
const factory GridAccessoryMenuEvent.initial() = _Initial;
const factory GridAccessoryMenuEvent.toggleMenu() = _MenuVisibleChange;
class DatabaseViewSettingExtensionEvent
with _$DatabaseViewSettingExtensionEvent {
const factory DatabaseViewSettingExtensionEvent.initial() = _Initial;
const factory DatabaseViewSettingExtensionEvent.toggleMenu() =
_MenuVisibleChange;
}
@freezed
class GridAccessoryMenuState with _$GridAccessoryMenuState {
const factory GridAccessoryMenuState({
class DatabaseViewSettingExtensionState
with _$DatabaseViewSettingExtensionState {
const factory DatabaseViewSettingExtensionState({
required String viewId,
required bool isVisible,
}) = _GridAccessoryMenuState;
}) = _DatabaseViewSettingExtensionState;
factory GridAccessoryMenuState.initial(
factory DatabaseViewSettingExtensionState.initial(
String viewId,
) =>
GridAccessoryMenuState(
DatabaseViewSettingExtensionState(
viewId: viewId,
isVisible: false,
);

View File

@ -1,6 +1,8 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -65,17 +67,27 @@ class GridBloc extends Bloc<GridEvent, GridState> {
),
);
},
didReceveFilters: (List<FilterInfo> filters) {
emit(
state.copyWith(
reorderable: filters.isEmpty && state.sorts.isEmpty,
filters: filters,
),
);
},
didReceveSorts: (List<SortInfo> sorts) {
emit(
state.copyWith(
reorderable: sorts.isEmpty && state.filters.isEmpty,
sorts: sorts,
),
);
},
);
},
);
}
@override
Future<void> close() async {
await databaseController.dispose();
return super.close();
}
RowCache getRowCache(RowId rowId) {
return databaseController.rowCache;
}
@ -93,17 +105,29 @@ class GridBloc extends Bloc<GridEvent, GridState> {
}
},
onRowsUpdated: (rows, reason) {
add(
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
);
if (!isClosed) {
add(
GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason),
);
}
},
onFieldsChanged: (fields) {
if (!isClosed) {
add(GridEvent.didReceiveFieldUpdate(fields));
}
},
onFiltersChanged: (filters) {
if (!isClosed) {
add(GridEvent.didReceveFilters(filters));
}
},
onSortsChanged: (sorts) {
if (!isClosed) {
add(GridEvent.didReceveSorts(sorts));
}
},
);
databaseController.setListener(onDatabaseChanged: onDatabaseChanged);
databaseController.addListener(onDatabaseChanged: onDatabaseChanged);
}
Future<void> _openGrid(Emitter<GridState> emit) async {
@ -138,6 +162,11 @@ class GridEvent with _$GridEvent {
const factory GridEvent.didReceiveGridUpdate(
DatabasePB grid,
) = _DidReceiveGridUpdate;
const factory GridEvent.didReceveFilters(List<FilterInfo> filters) =
_DidReceiveFilters;
const factory GridEvent.didReceveSorts(List<SortInfo> sorts) =
_DidReceiveSorts;
}
@freezed
@ -149,7 +178,10 @@ class GridState with _$GridState {
required List<RowInfo> rowInfos,
required int rowCount,
required GridLoadingState loadingState,
required bool reorderable,
required RowsChangedReason reason,
required List<SortInfo> sorts,
required List<FilterInfo> filters,
}) = _GridState;
factory GridState.initial(String viewId) => GridState(
@ -158,8 +190,11 @@ class GridState with _$GridState {
rowCount: 0,
grid: none(),
viewId: viewId,
reorderable: true,
loadingState: const _Loading(),
reason: const InitialListState(),
filters: [],
sorts: [],
);
}

View File

@ -1,19 +1,14 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import 'presentation/grid_page.dart';
class GridPluginBuilder implements PluginBuilder {
@override
Plugin build(dynamic data) {
if (data is ViewPB) {
return GridPlugin(pluginType: pluginType, view: data);
return DatabaseTabBarViewPlugin(pluginType: pluginType, view: data);
} else {
throw FlowyPluginException.invalidData;
}
@ -36,55 +31,3 @@ class GridPluginConfig implements PluginConfig {
@override
bool get creatable => true;
}
class GridPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
GridPlugin({
required ViewPB view,
required PluginType pluginType,
bool listenOnViewChanged = false,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
);
@override
PluginWidgetBuilder get widgetBuilder =>
GridPluginWidgetBuilder(notifier: notifier);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class GridPluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
ViewPB get view => notifier.view;
GridPluginWidgetBuilder({required this.notifier, Key? key});
@override
Widget get leftBarItem => ViewLeftBarItem(view: view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(view, deletedView.index);
}
});
});
return GridPage(key: ValueKey(view.id), view: view);
}
@override
List<NavigationItem> get navigationItems => [this];
}

View File

@ -1,5 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/toolbar/grid_setting_bar.dart';
import 'package:appflowy/plugins/database_view/tar_bar/setting_menu.dart';
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
@ -15,25 +17,77 @@ import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../../application/field/field_controller.dart';
import '../../application/row/row_cache.dart';
import '../../application/row/row_data_controller.dart';
import '../../application/setting/setting_bloc.dart';
import '../application/filter/filter_menu_bloc.dart';
import '../application/grid_bloc.dart';
import '../../application/database_controller.dart';
import '../application/sort/sort_menu_bloc.dart';
import 'grid_scroll.dart';
import '../../tar_bar/tab_bar_view.dart';
import 'layout/layout.dart';
import 'layout/sizes.dart';
import 'widgets/accessory_menu.dart';
import 'widgets/row/row.dart';
import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart';
import '../../widgets/row/row_detail.dart';
import 'widgets/shortcuts.dart';
import 'widgets/toolbar/grid_toolbar.dart';
class ToggleExtensionNotifier extends ChangeNotifier {
bool _isToggled = false;
get isToggled => _isToggled;
void toggle() {
_isToggled = !_isToggled;
notifyListeners();
}
}
class GridPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder {
final _toggleExtension = ToggleExtensionNotifier();
@override
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
) {
return GridPage(
key: _makeValueKey(controller),
view: view,
databaseController: controller,
);
}
@override
Widget settingBar(BuildContext context, DatabaseController controller) {
return GridSettingBar(
key: _makeValueKey(controller),
controller: controller,
toggleExtension: _toggleExtension,
);
}
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
) {
return DatabaseViewSettingExtension(
key: _makeValueKey(controller),
viewId: controller.viewId,
databaseController: controller,
toggleExtension: _toggleExtension,
);
}
ValueKey _makeValueKey(DatabaseController controller) {
return ValueKey(controller.viewId);
}
}
class GridPage extends StatefulWidget {
final DatabaseController databaseController;
const GridPage({
required this.view,
required this.databaseController,
this.onDeleted,
Key? key,
}) : super(key: key);
@ -46,12 +100,9 @@ class GridPage extends StatefulWidget {
}
class _GridPageState extends State<GridPage> {
late DatabaseController databaseController;
@override
void initState() {
super.initState();
databaseController = DatabaseController(view: widget.view);
}
@override
@ -61,24 +112,9 @@ class _GridPageState extends State<GridPage> {
BlocProvider<GridBloc>(
create: (context) => GridBloc(
view: widget.view,
databaseController: databaseController,
databaseController: widget.databaseController,
)..add(const GridEvent.initial()),
),
BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: widget.view.id,
fieldController: databaseController.fieldController,
)..add(const GridFilterMenuEvent.initial()),
),
BlocProvider<SortMenuBloc>(
create: (context) => SortMenuBloc(
viewId: widget.view.id,
fieldController: databaseController.fieldController,
)..add(const SortMenuEvent.initial()),
),
BlocProvider<DatabaseSettingBloc>(
create: (context) => DatabaseSettingBloc(viewId: widget.view.id),
),
],
child: BlocBuilder<GridBloc, GridState>(
builder: (context, state) {
@ -87,9 +123,7 @@ class _GridPageState extends State<GridPage> {
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.successOrFail.fold(
(_) => GridShortcuts(
child: FlowyGrid(
viewId: widget.view.id,
),
child: GridPageContent(view: widget.view),
),
(err) => FlowyErrorPage(err.toString()),
),
@ -100,18 +134,18 @@ class _GridPageState extends State<GridPage> {
}
}
class FlowyGrid extends StatefulWidget {
final String viewId;
const FlowyGrid({
required this.viewId,
class GridPageContent extends StatefulWidget {
final ViewPB view;
const GridPageContent({
required this.view,
super.key,
});
@override
State<FlowyGrid> createState() => _FlowyGridState();
State<GridPageContent> createState() => _GridPageContentState();
}
class _FlowyGridState extends State<FlowyGrid> {
class _GridPageContentState extends State<GridPageContent> {
final _scrollController = GridScrollController(
scrollGroupController: LinkedScrollControllerGroup(),
);
@ -135,106 +169,114 @@ class _FlowyGridState extends State<FlowyGrid> {
buildWhen: (previous, current) => previous.fields != current.fields,
builder: (context, state) {
final contentWidth = GridLayout.headerWidth(state.fields.value);
final child = _WrapScrollView(
scrollController: _scrollController,
contentWidth: contentWidth,
child: _GridRows(
viewId: widget.viewId,
verticalScrollController: _scrollController.verticalController,
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const GridToolbar(),
GridAccessoryMenu(viewId: state.viewId),
_gridHeader(context, state.viewId),
Flexible(child: child),
const _RowCountBadge(),
_GridHeader(headerScrollController: headerScrollController),
_GridRows(
viewId: state.viewId,
contentWidth: contentWidth,
scrollController: _scrollController,
),
const _GridFooter(),
],
);
},
);
}
}
Widget _gridHeader(BuildContext context, String viewId) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
return GridHeaderSliverAdaptor(
viewId: viewId,
fieldController: fieldController,
anchorScrollController: headerScrollController,
class _GridHeader extends StatelessWidget {
final ScrollController headerScrollController;
const _GridHeader({required this.headerScrollController});
@override
Widget build(BuildContext context) {
return BlocBuilder<GridBloc, GridState>(
builder: (context, state) {
return GridHeaderSliverAdaptor(
viewId: state.viewId,
fieldController:
context.read<GridBloc>().databaseController.fieldController,
anchorScrollController: headerScrollController,
);
},
);
}
}
class _GridRows extends StatelessWidget {
final String viewId;
final double contentWidth;
final GridScrollController scrollController;
const _GridRows({
required this.viewId,
required this.verticalScrollController,
required this.contentWidth,
required this.scrollController,
});
final ScrollController verticalScrollController;
@override
Widget build(BuildContext context) {
final filterState = context.watch<GridFilterMenuBloc>().state;
final sortState = context.watch<SortMenuBloc>().state;
return BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => current.reason.maybeWhen(
reorderRows: () => true,
reorderSingleRow: (reorderRow, rowInfo) => true,
delete: (item) => true,
insert: (item) => true,
orElse: () => false,
),
builder: (context, state) {
final rowInfos = state.rowInfos;
final behavior = ScrollConfiguration.of(context).copyWith(
scrollbars: false,
);
return ScrollConfiguration(
behavior: behavior,
child: ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: verticalScrollController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
index: index,
isSortEnabled: sortState.sortInfos.isNotEmpty,
isFilterEnabled: filterState.filters.isNotEmpty,
);
}
return const _GridFooter(key: Key('gridFooter'));
},
return Flexible(
child: _WrapScrollView(
scrollController: scrollController,
contentWidth: contentWidth,
child: BlocBuilder<GridBloc, GridState>(
buildWhen: (previous, current) => current.reason.maybeWhen(
reorderRows: () => true,
reorderSingleRow: (reorderRow, rowInfo) => true,
delete: (item) => true,
insert: (item) => true,
orElse: () => false,
),
);
},
builder: (context, state) {
final rowInfos = state.rowInfos;
final behavior = ScrollConfiguration.of(context).copyWith(
scrollbars: false,
);
return ScrollConfiguration(
behavior: behavior,
child: ReorderableListView.builder(
/// TODO(Xazin): Resolve inconsistent scrollbar behavior
/// This is a workaround related to
/// https://github.com/flutter/flutter/issues/25652
cacheExtent: 5000,
scrollController: scrollController.verticalController,
buildDefaultDragHandles: false,
proxyDecorator: (child, index, animation) => Material(
color: Colors.white.withOpacity(.1),
child: Opacity(opacity: .5, child: child),
),
onReorder: (fromIndex, newIndex) {
final toIndex =
newIndex > fromIndex ? newIndex - 1 : newIndex;
if (fromIndex == toIndex) {
return;
}
context
.read<GridBloc>()
.add(GridEvent.moveRow(fromIndex, toIndex));
},
itemCount: rowInfos.length + 1, // the extra item is the footer
itemBuilder: (context, index) {
if (index < rowInfos.length) {
final rowInfo = rowInfos[index];
return _renderRow(
context,
rowInfo.rowId,
isDraggable: state.reorderable,
index: index,
);
}
return const GridRowBottomBar(key: Key('gridFooter'));
},
),
);
},
),
),
);
}
@ -242,8 +284,7 @@ class _GridRows extends StatelessWidget {
BuildContext context,
RowId rowId, {
int? index,
bool isSortEnabled = false,
bool isFilterEnabled = false,
required bool isDraggable,
Animation<double>? animation,
}) {
final rowCache = context.read<GridBloc>().getRowCache(rowId);
@ -265,7 +306,7 @@ class _GridRows extends StatelessWidget {
rowId: rowId,
viewId: viewId,
index: index,
isDraggable: !isSortEnabled && !isFilterEnabled,
isDraggable: isDraggable,
dataController: dataController,
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
openDetailPage: (context, cellBuilder) {
@ -320,22 +361,6 @@ class _GridRows extends StatelessWidget {
}
}
class _GridFooter extends StatelessWidget {
const _GridFooter({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: GridSize.footerContentInsets,
height: GridSize.footerHeight,
margin: const EdgeInsets.only(bottom: 200),
child: const GridAddRowButton(),
);
}
}
class _WrapScrollView extends StatelessWidget {
const _WrapScrollView({
required this.contentWidth,
@ -366,8 +391,8 @@ class _WrapScrollView extends StatelessWidget {
}
}
class _RowCountBadge extends StatelessWidget {
const _RowCountBadge();
class _GridFooter extends StatelessWidget {
const _GridFooter();
@override
Widget build(BuildContext context) {

View File

@ -1,90 +0,0 @@
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../layout/sizes.dart';
import 'filter/filter_menu.dart';
import 'sort/sort_menu.dart';
class GridAccessoryMenu extends StatelessWidget {
final String viewId;
const GridAccessoryMenu({required this.viewId, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => GridAccessoryMenuBloc(viewId: viewId),
child: MultiBlocListener(
listeners: [
BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => context
.read<GridAccessoryMenuBloc>()
.add(const GridAccessoryMenuEvent.toggleMenu()),
),
BlocListener<SortMenuBloc, SortMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => context
.read<GridAccessoryMenuBloc>()
.add(const GridAccessoryMenuEvent.toggleMenu()),
),
],
child: BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
builder: (context, state) {
if (state.isVisible) {
return const _AccessoryMenu();
} else {
return const SizedBox();
}
},
),
),
);
}
}
class _AccessoryMenu extends StatelessWidget {
const _AccessoryMenu({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
builder: (context, state) {
return _wrapPadding(
Column(
children: [
Divider(
height: 1.0,
color: AFThemeExtension.of(context).toggleOffFill,
),
const VSpace(6),
const IntrinsicHeight(
child: Row(
children: [
SortMenu(),
HSpace(6),
FilterMenu(),
],
),
),
],
),
);
},
);
}
Widget _wrapPadding(Widget child) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
vertical: 6,
),
child: child,
);
}
}

View File

@ -1,4 +1,5 @@
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/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
@ -12,37 +13,49 @@ import 'create_filter_list.dart';
import 'filter_menu_item.dart';
class FilterMenu extends StatelessWidget {
const FilterMenu({Key? key}) : super(key: key);
final FieldController fieldController;
const FilterMenu({
required this.fieldController,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
builder: (context, state) {
final List<Widget> children = [];
children.addAll(
state.filters
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
.toList(),
);
return BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: fieldController.viewId,
fieldController: fieldController,
)..add(
const GridFilterMenuEvent.initial(),
),
child: BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
builder: (context, state) {
final List<Widget> children = [];
children.addAll(
state.filters
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
.toList(),
);
if (state.creatableFields.isNotEmpty) {
children.add(AddFilterButton(viewId: state.viewId));
}
if (state.creatableFields.isNotEmpty) {
children.add(AddFilterButton(viewId: state.viewId));
}
return Expanded(
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 6,
runSpacing: 4,
children: children,
return Expanded(
child: Row(
children: [
Expanded(
child: Wrap(
spacing: 6,
runSpacing: 4,
children: children,
),
),
),
],
),
);
},
],
),
);
},
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/image.dart';
@ -27,3 +28,19 @@ class GridAddRowButton extends StatelessWidget {
);
}
}
class GridRowBottomBar extends StatelessWidget {
const GridRowBottomBar({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
padding: GridSize.footerContentInsets,
height: GridSize.footerHeight,
margin: const EdgeInsets.only(bottom: 200),
child: const GridAddRowButton(),
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -13,30 +14,40 @@ import 'sort_editor.dart';
import 'sort_info.dart';
class SortMenu extends StatelessWidget {
const SortMenu({Key? key}) : super(key: key);
final FieldController fieldController;
const SortMenu({
required this.fieldController,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<SortMenuBloc, SortMenuState>(
builder: (context, state) {
if (state.sortInfos.isNotEmpty) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(340, 200)),
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
return SortEditor(
viewId: state.viewId,
fieldController: context.read<SortMenuBloc>().fieldController,
sortInfos: state.sortInfos,
);
},
child: SortChoiceChip(sortInfos: state.sortInfos),
);
} else {
return const SizedBox();
}
},
return BlocProvider<SortMenuBloc>(
create: (context) => SortMenuBloc(
viewId: fieldController.viewId,
fieldController: fieldController,
)..add(const SortMenuEvent.initial()),
child: BlocBuilder<SortMenuBloc, SortMenuState>(
builder: (context, state) {
if (state.sortInfos.isNotEmpty) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(340, 200)),
direction: PopoverDirection.bottomWithLeftAligned,
popupBuilder: (BuildContext popoverContext) {
return SortEditor(
viewId: state.viewId,
fieldController: context.read<SortMenuBloc>().fieldController,
sortInfos: state.sortInfos,
);
},
child: SortChoiceChip(sortInfos: state.sortInfos),
);
} else {
return const SizedBox();
}
},
),
);
}
}

View File

@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/layout/layout_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -67,34 +66,6 @@ class _DatabaseLayoutListState extends State<DatabaseLayoutList> {
}
}
extension DatabaseLayoutExtension on DatabaseLayoutPB {
String layoutName() {
switch (this) {
case DatabaseLayoutPB.Board:
return LocaleKeys.board_menuName.tr();
case DatabaseLayoutPB.Calendar:
return LocaleKeys.calendar_menuName.tr();
case DatabaseLayoutPB.Grid:
return LocaleKeys.grid_menuName.tr();
default:
return "";
}
}
String iconName() {
switch (this) {
case DatabaseLayoutPB.Board:
return 'editor/board';
case DatabaseLayoutPB.Calendar:
return "editor/grid";
case DatabaseLayoutPB.Grid:
return "editor/grid";
default:
return "";
}
}
}
class DatabaseViewLayoutCell extends StatelessWidget {
final bool isSelected;
final DatabaseLayoutPB databaseLayout;

View File

@ -0,0 +1,68 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/filter/filter_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/application/sort/sort_menu_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/setting_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'filter_button.dart';
import 'sort_button.dart';
class GridSettingBar extends StatelessWidget {
final DatabaseController controller;
final ToggleExtensionNotifier toggleExtension;
const GridSettingBar({
required this.controller,
required this.toggleExtension,
super.key,
});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<GridFilterMenuBloc>(
create: (context) => GridFilterMenuBloc(
viewId: controller.viewId,
fieldController: controller.fieldController,
)..add(const GridFilterMenuEvent.initial()),
),
BlocProvider<SortMenuBloc>(
create: (context) => SortMenuBloc(
viewId: controller.viewId,
fieldController: controller.fieldController,
)..add(const SortMenuEvent.initial()),
),
],
child: MultiBlocListener(
listeners: [
BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
),
BlocListener<SortMenuBloc, SortMenuState>(
listenWhen: (p, c) => p.isVisible != c.isVisible,
listener: (context, state) => toggleExtension.toggle(),
),
],
child: SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
const Spacer(),
const FilterButton(),
const SortButton(),
SettingButton(
databaseController: controller,
),
],
),
),
),
);
}
}

View File

@ -1,41 +0,0 @@
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
import 'filter_button.dart';
import '../../../../widgets/setting/setting_button.dart';
import 'sort_button.dart';
class GridToolbarContext {
final String viewId;
final FieldController fieldController;
GridToolbarContext({
required this.viewId,
required this.fieldController,
});
}
class GridToolbar extends StatelessWidget {
const GridToolbar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
const Spacer(),
const FilterButton(),
const SortButton(),
SettingButton(
databaseController: context.read<GridBloc>().databaseController,
),
],
),
);
}
}

View File

@ -0,0 +1,98 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/grid/application/grid_accessory_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/grid_page.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import '../application/field/field_controller.dart';
import '../grid/presentation/layout/sizes.dart';
import '../grid/presentation/widgets/filter/filter_menu.dart';
import '../grid/presentation/widgets/sort/sort_menu.dart';
class DatabaseViewSettingExtension extends StatelessWidget {
final String viewId;
final DatabaseController databaseController;
final ToggleExtensionNotifier toggleExtension;
const DatabaseViewSettingExtension({
required this.viewId,
required this.databaseController,
required this.toggleExtension,
super.key,
});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: toggleExtension,
child: Consumer<ToggleExtensionNotifier>(
builder: (context, value, child) {
if (value.isToggled) {
return BlocProvider(
create: (context) =>
DatabaseViewSettingExtensionBloc(viewId: viewId),
child: _DatabaseViewSettingContent(
fieldController: databaseController.fieldController,
),
);
} else {
return const SizedBox();
}
},
),
);
}
}
class _DatabaseViewSettingContent extends StatelessWidget {
final FieldController fieldController;
const _DatabaseViewSettingContent({
required this.fieldController,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<DatabaseViewSettingExtensionBloc,
DatabaseViewSettingExtensionState>(
builder: (context, state) {
final children = <Widget>[
Divider(
height: 1.0,
color: AFThemeExtension.of(context).toggleOffFill,
),
const VSpace(6),
IntrinsicHeight(
child: Row(
children: [
SortMenu(
fieldController: fieldController,
),
const HSpace(6),
FilterMenu(
fieldController: fieldController,
),
],
),
)
];
return _wrapPadding(
Column(children: children),
);
},
);
}
Widget _wrapPadding(Widget child) {
return Padding(
padding: EdgeInsets.symmetric(
horizontal: GridSize.leadingHeaderPadding,
vertical: 6,
),
child: child,
);
}
}

View File

@ -0,0 +1,415 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/tar_bar_bloc.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/database_controller.dart';
import '../grid/presentation/layout/sizes.dart';
import 'tar_bar_add_button.dart';
abstract class DatabaseTabBarItemBuilder {
const DatabaseTabBarItemBuilder();
/// Returns the content of the tab bar item. The content is shown when the tab
/// bar item is selected. It can be any kind of database view.
Widget content(
BuildContext context,
ViewPB view,
DatabaseController controller,
);
/// Returns the setting bar of the tab bar item. The setting bar is shown on the
/// top right conner when the tab bar item is selected.
Widget settingBar(
BuildContext context,
DatabaseController controller,
);
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
);
}
class DatabaseTabBarView extends StatefulWidget {
final ViewPB view;
const DatabaseTabBarView({
required this.view,
super.key,
});
@override
State<DatabaseTabBarView> createState() => _DatabaseTabBarViewState();
}
class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
PageController? _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(
initialPage: 0,
);
}
@override
Widget build(BuildContext context) {
return BlocProvider<GridTabBarBloc>(
create: (context) => GridTabBarBloc(view: widget.view)
..add(
const GridTabBarEvent.initial(),
),
child: MultiBlocListener(
listeners: [
BlocListener<GridTabBarBloc, GridTabBarState>(
listenWhen: (p, c) => p.selectedIndex != c.selectedIndex,
listener: (context, state) {
_pageController?.animateToPage(
state.selectedIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.ease,
);
},
),
],
child: Column(
children: [
Row(
children: [
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return const Flexible(
child: Padding(
padding: EdgeInsets.only(left: 50),
child: DatabaseTabBar(),
),
);
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return SizedBox(
width: 300,
child: Padding(
padding: const EdgeInsets.only(right: 50),
child: pageSettingBarFromState(state),
),
);
},
),
],
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return pageSettingBarExtensionFromState(state);
},
),
Expanded(
child: BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return PageView(
pageSnapping: false,
physics: const NeverScrollableScrollPhysics(),
controller: _pageController,
children: pageContentFromState(state),
);
},
),
),
],
),
),
);
}
List<Widget> pageContentFromState(GridTabBarState state) {
return state.tabBars.map((tabBar) {
final controller =
state.tabBarControllerByViewId[tabBar.viewId]!.controller;
return tabBar.builder.content(
context,
tabBar.view,
controller,
);
}).toList();
}
Widget pageSettingBarFromState(GridTabBarState state) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBar(
context,
controller,
);
}
Widget pageSettingBarExtensionFromState(GridTabBarState state) {
if (state.tabBars.length < state.selectedIndex) {
return const SizedBox.shrink();
}
final tarBar = state.tabBars[state.selectedIndex];
final controller =
state.tabBarControllerByViewId[tarBar.viewId]!.controller;
return tarBar.builder.settingBarExtension(
context,
controller,
);
}
}
class DatabaseTabBarViewPlugin extends Plugin {
@override
final ViewPluginNotifier notifier;
final PluginType _pluginType;
DatabaseTabBarViewPlugin({
required ViewPB view,
required PluginType pluginType,
}) : _pluginType = pluginType,
notifier = ViewPluginNotifier(view: view);
@override
PluginWidgetBuilder get widgetBuilder => DatabasePluginWidgetBuilder(
notifier: notifier,
);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => _pluginType;
}
class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
final ViewPluginNotifier notifier;
DatabasePluginWidgetBuilder({
required this.notifier,
Key? key,
});
@override
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
@override
Widget buildWidget({PluginContext? context}) {
notifier.isDeleted.addListener(() {
notifier.isDeleted.value.fold(() => null, (deletedView) {
if (deletedView.hasIndex()) {
context?.onDeleted(notifier.view, deletedView.index);
}
});
});
return DatabaseTabBarView(
key: ValueKey(notifier.view.id),
view: notifier.view,
);
}
@override
List<NavigationItem> get navigationItems => [this];
}
class DatabaseTabBar extends StatefulWidget {
const DatabaseTabBar({super.key});
@override
State<DatabaseTabBar> createState() => _DatabaseTabBarState();
}
class _DatabaseTabBarState extends State<DatabaseTabBar> {
final _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
final children = state.tabBars.indexed.map((indexed) {
final isSelected = state.selectedIndex == indexed.$1;
final tabBar = indexed.$2;
return DatabaseTabBarItem(
key: ValueKey(tabBar.viewId),
view: tabBar.view,
isSelected: isSelected,
onTap: (selectedView) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.selectView(selectedView.id),
);
},
);
}).toList();
return Row(
children: [
Flexible(
child: SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: IntrinsicWidth(
child: Row(children: children),
),
),
),
AddDatabaseViewButton(
onTap: (action) async {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.createView(action),
);
},
),
],
);
},
);
}
}
class DatabaseTabBarItem extends StatelessWidget {
final bool isSelected;
final ViewPB view;
final Function(ViewPB) onTap;
const DatabaseTabBarItem({
required this.view,
required this.isSelected,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: 80, maxWidth: 160),
child: IntrinsicWidth(
child: Column(
children: [
TabBarItemButton(
view: view,
onTap: () => onTap(view),
),
if (isSelected)
Divider(
height: 1,
thickness: 2,
color: Theme.of(context).colorScheme.secondary,
),
],
),
),
);
}
}
class TabBarItemButton extends StatelessWidget {
final ViewPB view;
final VoidCallback onTap;
const TabBarItemButton({
required this.view,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return PopoverActionList<TabBarViewAction>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: TabBarViewAction.values,
buildChild: (controller) {
return FlowyButton(
radius: Corners.s5Border,
hoverColor: AFThemeExtension.of(context).greyHover,
onTap: onTap,
onSecondaryTap: () {
controller.show();
},
text: FlowyText.medium(
view.name,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
margin: GridSize.cellContentInsets,
leftIcon: svgWidget(
view.iconName,
color: Theme.of(context).iconTheme.color,
),
);
},
onSelected: (action, controller) {
switch (action) {
case TabBarViewAction.rename:
NavigatorTextFieldDialog(
title: LocaleKeys.menuAppHeader_renameDialog.tr(),
value: view.name,
confirm: (newValue) {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.renameView(view.id, newValue),
);
},
).show(context);
break;
case TabBarViewAction.delete:
NavigatorAlertDialog(
title: LocaleKeys.grid_deleteView.tr(),
confirm: () {
context.read<GridTabBarBloc>().add(
GridTabBarEvent.deleteView(view.id),
);
},
).show(context);
break;
}
controller.close();
},
);
}
}
enum TabBarViewAction implements ActionCell {
rename,
delete;
@override
String get name {
switch (this) {
case TabBarViewAction.rename:
return LocaleKeys.disclosureAction_rename.tr();
case TabBarViewAction.delete:
return LocaleKeys.disclosureAction_delete.tr();
}
}
Widget icon(Color iconColor) {
switch (this) {
case TabBarViewAction.rename:
return const FlowySvg(name: 'editor/edit');
case TabBarViewAction.delete:
return const FlowySvg(name: 'editor/delete');
}
}
@override
Widget? leftIcon(Color iconColor) => icon(iconColor);
@override
Widget? rightIcon(Color iconColor) => null;
}

View File

@ -0,0 +1,156 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/extension.dart';
import 'package:flutter/material.dart';
class AddDatabaseViewButton extends StatefulWidget {
final Function(AddButtonAction) onTap;
const AddDatabaseViewButton({
required this.onTap,
super.key,
});
@override
State<AddDatabaseViewButton> createState() => _AddDatabaseViewButtonState();
}
class _AddDatabaseViewButtonState extends State<AddDatabaseViewButton> {
final popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
controller: popoverController,
constraints: BoxConstraints.loose(const Size(200, 400)),
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 8),
margin: EdgeInsets.zero,
triggerActions: PopoverTriggerFlags.none,
child: FlowyIconButton(
iconPadding: const EdgeInsets.all(4),
hoverColor: AFThemeExtension.of(context).greyHover,
onPressed: () => popoverController.show(),
icon: svgWidget(
'home/add',
color: Theme.of(context).colorScheme.tertiary,
),
),
popupBuilder: (BuildContext context) {
return TarBarAddButtonAction(
onTap: (action) {
popoverController.close();
widget.onTap(action);
},
);
},
);
}
}
class TarBarAddButtonAction extends StatelessWidget {
final Function(AddButtonAction) onTap;
const TarBarAddButtonAction({
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
final cells = AddButtonAction.values.map((layout) {
return TarBarAddButtonActionCell(
action: layout,
onTap: onTap,
);
}).toList();
return ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: cells.length,
itemBuilder: (BuildContext context, int index) => cells[index],
separatorBuilder: (BuildContext context, int index) =>
VSpace(GridSize.typeOptionSeparatorHeight),
padding: const EdgeInsets.symmetric(vertical: 6.0),
);
}
}
class TarBarAddButtonActionCell extends StatelessWidget {
final AddButtonAction action;
final void Function(AddButtonAction) onTap;
const TarBarAddButtonActionCell({
required this.action,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.popoverItemHeight,
child: FlowyButton(
hoverColor: AFThemeExtension.of(context).lightGreyHover,
text: FlowyText.medium(
action.title,
color: AFThemeExtension.of(context).textColor,
),
leftIcon: svgWidget(
action.iconName,
color: Theme.of(context).iconTheme.color,
),
onTap: () => onTap(action),
).padding(horizontal: 6.0),
);
}
}
enum AddButtonAction {
grid,
calendar,
board;
String get title {
switch (this) {
case AddButtonAction.board:
return LocaleKeys.board_menuName.tr();
case AddButtonAction.calendar:
return LocaleKeys.calendar_menuName.tr();
case AddButtonAction.grid:
return LocaleKeys.grid_menuName.tr();
default:
return "";
}
}
ViewLayoutPB get layoutType {
switch (this) {
case AddButtonAction.board:
return ViewLayoutPB.Board;
case AddButtonAction.calendar:
return ViewLayoutPB.Calendar;
case AddButtonAction.grid:
return ViewLayoutPB.Grid;
default:
return ViewLayoutPB.Grid;
}
}
String get iconName {
switch (this) {
case AddButtonAction.board:
return 'editor/board';
case AddButtonAction.calendar:
return "editor/grid";
case AddButtonAction.grid:
return "editor/grid";
default:
return "";
}
}
}

View File

@ -0,0 +1,31 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
extension DatabaseLayoutExtension on DatabaseLayoutPB {
String layoutName() {
switch (this) {
case DatabaseLayoutPB.Board:
return LocaleKeys.board_menuName.tr();
case DatabaseLayoutPB.Calendar:
return LocaleKeys.calendar_menuName.tr();
case DatabaseLayoutPB.Grid:
return LocaleKeys.grid_menuName.tr();
default:
return "";
}
}
String iconName() {
switch (this) {
case DatabaseLayoutPB.Board:
return 'editor/board';
case DatabaseLayoutPB.Calendar:
return "editor/grid";
case DatabaseLayoutPB.Grid:
return "editor/grid";
default:
return "";
}
}
}

View File

@ -1,5 +1,4 @@
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
@ -9,6 +8,7 @@ import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import '../../grid/presentation/layout/sizes.dart';
import 'setting_button.dart';
class DatabaseSettingList extends StatelessWidget {
final DatabaseController databaseContoller;

View File

@ -1,9 +1,9 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/setting/setting_bloc.dart';
import 'package:appflowy/plugins/database_view/calendar/presentation/toolbar/calendar_layout_setting.dart';
import 'package:appflowy/plugins/database_view/widgets/group/database_group.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -97,7 +97,7 @@ class _DatabaseSettingListPopoverState
case DatabaseSettingAction.showLayout:
return DatabaseLayoutList(
viewId: widget.databaseController.viewId,
currentLayout: widget.databaseController.databaseLayout!,
currentLayout: widget.databaseController.databaseLayout,
);
case DatabaseSettingAction.showGroup:
return DatabaseGroupList(
@ -132,7 +132,7 @@ class ICalendarSettingImpl extends ICalendarSetting {
@override
void updateLayoutSettings(CalendarLayoutSettingPB layoutSettings) {
_databaseController.updateCalenderLayoutSetting(layoutSettings);
_databaseController.updateLayoutSetting(layoutSettings);
}
@override
@ -140,3 +140,63 @@ class ICalendarSettingImpl extends ICalendarSetting {
return _databaseController.databaseLayoutSetting?.calendar;
}
}
enum DatabaseSettingAction {
showProperties,
showLayout,
showGroup,
showCalendarLayout,
}
extension DatabaseSettingActionExtension on DatabaseSettingAction {
String iconName() {
switch (this) {
case DatabaseSettingAction.showProperties:
return 'grid/setting/properties';
case DatabaseSettingAction.showLayout:
return 'grid/setting/database_layout';
case DatabaseSettingAction.showGroup:
return 'grid/setting/group';
case DatabaseSettingAction.showCalendarLayout:
return 'grid/setting/calendar_layout';
}
}
String title() {
switch (this) {
case DatabaseSettingAction.showProperties:
return LocaleKeys.grid_settings_Properties.tr();
case DatabaseSettingAction.showLayout:
return LocaleKeys.grid_settings_databaseLayout.tr();
case DatabaseSettingAction.showGroup:
return LocaleKeys.grid_settings_group.tr();
case DatabaseSettingAction.showCalendarLayout:
return LocaleKeys.calendar_settings_name.tr();
}
}
}
/// Returns the list of actions that should be shown for the given database layout.
List<DatabaseSettingAction> actionsForDatabaseLayout(DatabaseLayoutPB? layout) {
switch (layout) {
case DatabaseLayoutPB.Board:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showGroup,
];
case DatabaseLayoutPB.Calendar:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
DatabaseSettingAction.showCalendarLayout,
];
case DatabaseLayoutPB.Grid:
return [
DatabaseSettingAction.showProperties,
DatabaseSettingAction.showLayout,
];
default:
return [];
}
}

View File

@ -50,10 +50,7 @@ class DocumentPlugin extends Plugin<int> {
required ViewPB view,
bool listenOnViewChanged = false,
Key? key,
}) : notifier = ViewPluginNotifier(
view: view,
listenOnViewChanged: listenOnViewChanged,
) {
}) : notifier = ViewPluginNotifier(view: view) {
_pluginType = pluginType;
_documentAppearanceCubit.fetch();
}

View File

@ -91,10 +91,12 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
onExit: (_) => widget.editorState.service.scrollService?.enable(),
child: SizedBox(
height: 400,
child: Stack(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMenu(context, viewPB),
_buildPage(context, viewPB),
Expanded(child: _buildPage(context, viewPB)),
],
),
),
@ -114,68 +116,58 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
}
Widget _buildMenu(BuildContext context, ViewPB viewPB) {
return Positioned(
top: 5,
left: 5,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// information
FlowyIconButton(
tooltipText: LocaleKeys.tooltip_referencePage.tr(
namedArgs: {'name': viewPB.layout.name},
),
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// information
FlowyIconButton(
tooltipText: LocaleKeys.tooltip_referencePage.tr(
namedArgs: {'name': viewPB.layout.name},
),
width: 24,
height: 24,
iconPadding: const EdgeInsets.all(3),
icon: svgWidget(
'common/information',
color: Theme.of(context).iconTheme.color,
),
),
// setting
const Space(7, 0),
PopoverActionList<_ActionWrapper>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: _ActionType.values
.map((action) => _ActionWrapper(action))
.toList(),
buildChild: (controller) => FlowyIconButton(
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
width: 24,
height: 24,
iconPadding: const EdgeInsets.all(3),
icon: svgWidget(
'common/information',
'common/settings',
color: Theme.of(context).iconTheme.color,
),
onPressed: () => controller.show(),
),
// Name
const Space(7, 0),
FlowyText.medium(
viewPB.name,
fontSize: 16.0,
),
// setting
const Space(7, 0),
PopoverActionList<_ActionWrapper>(
direction: PopoverDirection.bottomWithCenterAligned,
actions: _ActionType.values
.map((action) => _ActionWrapper(action))
.toList(),
buildChild: (controller) => FlowyIconButton(
tooltipText: LocaleKeys.tooltip_openMenu.tr(),
width: 24,
height: 24,
iconPadding: const EdgeInsets.all(3),
icon: svgWidget(
'common/settings',
color: Theme.of(context).iconTheme.color,
),
onPressed: () => controller.show(),
),
onSelected: (action, controller) async {
switch (action.inner) {
case _ActionType.viewDatabase:
getIt<MenuSharedState>().latestOpenView = viewPB;
onSelected: (action, controller) async {
switch (action.inner) {
case _ActionType.viewDatabase:
getIt<MenuSharedState>().latestOpenView = viewPB;
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
break;
case _ActionType.delete:
final transaction = widget.editorState.transaction;
transaction.deleteNode(widget.node);
widget.editorState.apply(transaction);
break;
}
controller.close();
},
)
],
),
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
break;
case _ActionType.delete:
final transaction = widget.editorState.transaction;
transaction.deleteNode(widget.node);
widget.editorState.apply(transaction);
break;
}
controller.close();
},
)
],
);
}

View File

@ -53,7 +53,7 @@ extension InsertDatabase on EditorState {
}
final prefix = _referencedDatabasePrefix(childView.layout);
final ref = await ViewBackendService.createDatabaseReferenceView(
final ref = await ViewBackendService.createDatabaseLinkedView(
parentViewId: childView.id,
name: "$prefix ${childView.name}",
layoutType: childView.layout,

View File

@ -1,4 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -212,7 +213,7 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
FlowyButton(
isSelected: index == _selectedIndex,
leftIcon: svgWidget(
_iconName(value),
value.iconName,
color: Theme.of(context).iconTheme.color,
),
text: FlowyText.regular(value.name),
@ -238,19 +239,6 @@ class _LinkToPageMenuState extends State<LinkToPageMenu> {
future: items,
);
}
String _iconName(ViewPB viewPB) {
switch (viewPB.layout) {
case ViewLayoutPB.Grid:
return 'editor/grid';
case ViewLayoutPB.Board:
return 'editor/board';
case ViewLayoutPB.Calendar:
return 'editor/calendar';
default:
throw Exception('Unknown layout type');
}
}
}
extension on ViewLayoutPB {

View File

@ -1,14 +1,10 @@
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart';
import '../workspace/presentation/home/home_stack.dart';
class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
final ViewListener? _viewListener;
ViewPB view;
@ -18,30 +14,18 @@ class ViewPluginNotifier extends PluginNotifier<Option<DeletedViewPB>> {
ViewPluginNotifier({
required this.view,
required bool listenOnViewChanged,
}) : _viewListener = ViewListener(viewId: view.id) {
if (listenOnViewChanged) {
_viewListener?.start(
onViewUpdated: (updatedView) {
// If the layout is changed, we need to create a new plugin for it.
if (view.layout != updatedView.layout) {
getIt<HomeStackManager>().setPlugin(
updatedView.plugin(
listenOnViewChanged: listenOnViewChanged,
),
);
} else {
view = updatedView;
}
},
onViewMoveToTrash: (result) {
result.fold(
(deletedView) => isDeleted.value = some(deletedView),
(err) => Log.error(err),
);
},
);
}
_viewListener?.start(
onViewUpdated: (updatedView) {
view = updatedView;
},
onViewMoveToTrash: (result) {
result.fold(
(deletedView) => isDeleted.value = some(deletedView),
(err) => Log.error(err),
);
},
);
}
@override