feat: mobile view editor improvements (#4186)

* feat: database view editor improvements

* feat: implement calendar settings

* fix: wrong view being opened
This commit is contained in:
Richard Shiue
2023-12-21 11:51:42 +08:00
committed by GitHub
parent 4fa08fda02
commit 13260e1db8
21 changed files with 792 additions and 1109 deletions

View File

@ -142,7 +142,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
showMobileBottomSheet(
context,
backgroundColor: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(top: 8, bottom: 36),
padding: const EdgeInsets.only(top: 8, bottom: 38),
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
children: [

View File

@ -1,4 +1,5 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
import 'package:flutter/material.dart';
@ -6,6 +7,7 @@ import 'package:go_router/go_router.dart';
import 'mobile_create_field_screen.dart';
import 'mobile_edit_field_screen.dart';
import 'mobile_field_picker_list.dart';
import 'mobile_field_type_grid.dart';
import 'mobile_field_type_option_editor.dart';
import 'mobile_quick_field_editor.dart';
@ -104,3 +106,22 @@ void showQuickEditField(
},
);
}
Future<String?> showFieldPicker(
BuildContext context,
String? selectedFieldId,
FieldController fieldController,
bool Function(FieldInfo fieldInfo) filterBy,
) {
return showMobileBottomSheet<String>(
context,
padding: EdgeInsets.zero,
builder: (context) {
return MobileFieldPickerList(
selectedFieldId: selectedFieldId,
fieldController: fieldController,
filterBy: filterBy,
);
},
);
}

View File

@ -0,0 +1,150 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileFieldPickerList extends StatefulWidget {
MobileFieldPickerList({
super.key,
required this.selectedFieldId,
required FieldController fieldController,
required bool Function(FieldInfo fieldInfo) filterBy,
}) : fields = fieldController.fieldInfos.where(filterBy).toList();
final String? selectedFieldId;
final List<FieldInfo> fields;
@override
State<MobileFieldPickerList> createState() => _MobileFieldPickerListState();
}
class _MobileFieldPickerListState extends State<MobileFieldPickerList> {
String? newFieldId;
@override
void initState() {
super.initState();
newFieldId = widget.selectedFieldId;
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const Center(child: DragHandler()),
_Header(newFieldId: newFieldId),
Expanded(
child: ListView.builder(
itemCount: widget.fields.length,
itemBuilder: (context, index) => _FieldButton(
field: widget.fields[index],
showTopBorder: index == 0,
isSelected: widget.fields[index].id == newFieldId,
onSelect: (fieldId) => setState(() => newFieldId = fieldId),
),
),
),
],
);
}
}
class _Header extends StatelessWidget {
const _Header({required this.newFieldId});
final String? newFieldId;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox.square(
dimension: 36,
child: IconButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.zero,
onPressed: () => context.pop(),
icon: const FlowySvg(
FlowySvgs.arrow_left_s,
size: Size.square(20),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
child: TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
enableFeedback: true,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: () => context.pop(newFieldId),
child: FlowyText.medium(
LocaleKeys.button_save.tr(),
fontSize: 16,
color: Theme.of(context).colorScheme.onPrimary,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
Center(
child: FlowyText.medium(
LocaleKeys.calendar_settings_changeLayoutDateField.tr(),
fontSize: 16,
),
),
],
);
}
}
class _FieldButton extends StatelessWidget {
const _FieldButton({
required this.field,
required this.isSelected,
required this.onSelect,
required this.showTopBorder,
});
final FieldInfo field;
final bool isSelected;
final void Function(String fieldId) onSelect;
final bool showTopBorder;
@override
Widget build(BuildContext context) {
return FlowyOptionTile.checkbox(
text: field.name,
isSelected: isSelected,
leftIcon: FlowySvg(
field.fieldType.icon(),
size: const Size.square(20),
),
showTopBorder: showTopBorder,
onTap: () => onSelect(field.id),
);
}
}

View File

@ -134,7 +134,7 @@ class _QuickEditFieldState extends State<QuickEditField> {
},
),
],
const VSpace(12),
const VSpace(38),
],
);
}

View File

@ -10,6 +10,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_info.dart
import 'package:appflowy/plugins/database_view/application/setting/property_bloc.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database_view/widgets/setting/field_visibility_extension.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
@ -23,11 +24,9 @@ class MobileDatabaseFieldList extends StatelessWidget {
const MobileDatabaseFieldList({
super.key,
required this.databaseController,
required this.viewPB,
});
final DatabaseController databaseController;
final ViewPB viewPB;
@override
Widget build(BuildContext context) {
@ -46,7 +45,7 @@ class MobileDatabaseFieldList extends StatelessWidget {
child: SingleChildScrollView(
child: _MobileDatabaseFieldListBody(
databaseController: databaseController,
view: viewPB,
view: context.read<ViewBloc>().state.view,
),
),
),

View File

@ -2,11 +2,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
import 'package:appflowy/plugins/database_view/calendar/application/calendar_setting_bloc.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../field/mobile_field_bottom_sheets.dart';
/// [DatabaseViewLayoutPicker] is seen when changing the layout type of a
/// database view or creating a new database view.
@ -46,6 +51,9 @@ class DatabaseViewLayoutPicker extends StatelessWidget {
}
}
/// [MobileCalendarViewLayoutSettings] is used when the database layout is
/// calendar. It allows changing the field being used to layout the events,
/// and which day of the week the calendar starts on.
class MobileCalendarViewLayoutSettings extends StatelessWidget {
const MobileCalendarViewLayoutSettings({
super.key,
@ -56,26 +64,36 @@ class MobileCalendarViewLayoutSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_layoutField(),
_divider(),
..._startWeek(context),
],
return BlocProvider<CalendarSettingBloc>(
create: (context) {
return CalendarSettingBloc(
databaseController: databaseController,
)..add(const CalendarSettingEvent.initial());
},
child: BlocBuilder<CalendarSettingBloc, CalendarSettingState>(
builder: (context, state) {
if (state.layoutSetting == null) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_CalendarLayoutField(
context: context,
databaseController: databaseController,
selectedFieldId: state.layoutSetting?.fieldId,
),
_divider(),
..._startWeek(context, state.layoutSetting?.firstDayOfWeek),
],
);
},
),
);
}
Widget _layoutField() {
return FlowyOptionTile.text(
text: LocaleKeys.calendar_settings_layoutDateField.tr(),
);
}
Widget _divider() => const VSpace(20);
List<Widget> _startWeek(BuildContext context) {
List<Widget> _startWeek(BuildContext context, int? firstDayOfWeek) {
final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols;
return [
Padding(
@ -87,18 +105,85 @@ class MobileCalendarViewLayoutSettings extends StatelessWidget {
),
),
FlowyOptionTile.checkbox(
text: symbols.WEEKDAYS[-1],
isSelected: true,
onTap: () {},
text: symbols.WEEKDAYS[0],
isSelected: firstDayOfWeek! == 0,
onTap: () {
context.read<CalendarSettingBloc>().add(
const CalendarSettingEvent.updateLayoutSetting(
firstDayOfWeek: 0,
),
);
},
),
FlowyOptionTile.checkbox(
text: symbols.WEEKDAYS[0],
isSelected: false,
text: symbols.WEEKDAYS[1],
isSelected: firstDayOfWeek == 1,
showTopBorder: false,
onTap: () {},
onTap: () {
context.read<CalendarSettingBloc>().add(
const CalendarSettingEvent.updateLayoutSetting(
firstDayOfWeek: 1,
),
);
},
),
];
}
Widget _divider() => const VSpace(20);
}
class _CalendarLayoutField extends StatelessWidget {
const _CalendarLayoutField({
required this.context,
required this.databaseController,
required this.selectedFieldId,
});
final BuildContext context;
final DatabaseController databaseController;
final String? selectedFieldId;
@override
Widget build(BuildContext context) {
FieldInfo? selectedField;
if (selectedFieldId != null) {
selectedField =
databaseController.fieldController.getField(selectedFieldId!);
}
return FlowyOptionTile.text(
text: LocaleKeys.calendar_settings_layoutDateField.tr(),
trailing: selectedFieldId == null
? null
: Row(
children: [
FlowyText(
selectedField!.name,
color: Theme.of(context).hintColor,
),
const HSpace(8),
const FlowySvg(FlowySvgs.arrow_right_s),
],
),
onTap: () async {
final newFieldId = await showFieldPicker(
context,
selectedFieldId,
databaseController.fieldController,
(field) => field.fieldType == FieldType.DateTime,
);
if (context.mounted &&
newFieldId != null &&
newFieldId != selectedFieldId) {
context.read<CalendarSettingBloc>().add(
CalendarSettingEvent.updateLayoutSetting(
layoutFieldId: newFieldId,
),
);
}
},
);
}
}
class MobileBoardViewLayoutSettings extends StatelessWidget {

View File

@ -1,30 +1,46 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/tab_bar_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'database_view_layout.dart';
import 'database_view_quick_actions.dart';
/// [MobileDatabaseViewList] shows a list of all the views in the database and
/// adds a button to create a new database view.
class MobileDatabaseViewList extends StatelessWidget {
const MobileDatabaseViewList({super.key, required this.views});
const MobileDatabaseViewList({super.key, required this.databaseController});
final List<ViewPB> views;
final DatabaseController databaseController;
@override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
return BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final views = [state.view, ...state.view.childViews];
final children = [
...views.map((view) => MobileDatabaseViewListButton(view: view)),
const Center(child: DragHandler()),
const _Header(),
...views.mapIndexed(
(index, view) => MobileDatabaseViewListButton(
view: view,
databaseController: databaseController,
showTopBorder: index == 0,
),
),
const VSpace(20),
const MobileNewDatabaseViewButton(),
];
@ -37,36 +53,123 @@ class MobileDatabaseViewList extends StatelessWidget {
}
}
@visibleForTesting
class MobileDatabaseViewListButton extends StatelessWidget {
const MobileDatabaseViewListButton({super.key, required this.view});
final ViewPB view;
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
return FlowyOptionTile.text(
text: view.name,
onTap: () {},
leftIcon: _buildViewIconButton(context, view),
trailing: FlowySvg(
FlowySvgs.three_dots_s,
size: const Size.square(20),
color: Theme.of(context).hintColor,
const iconWidth = 30.0;
return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: FlowyIconButton(
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(iconWidth),
),
width: iconWidth,
iconPadding: EdgeInsets.zero,
onPressed: () => context.pop(),
),
),
Align(
alignment: Alignment.center,
child: FlowyText.medium(
LocaleKeys.grid_settings_viewList.tr(),
fontSize: 16,
),
),
],
),
);
}
}
@visibleForTesting
class MobileDatabaseViewListButton extends StatelessWidget {
const MobileDatabaseViewListButton({
super.key,
required this.view,
required this.databaseController,
required this.showTopBorder,
});
final ViewPB view;
final DatabaseController databaseController;
final bool showTopBorder;
@override
Widget build(BuildContext context) {
return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (context, state) {
final index =
state.tabBars.indexWhere((tabBar) => tabBar.viewId == view.id);
final isSelected = index == state.selectedIndex;
return FlowyOptionTile.text(
text: view.name,
onTap: () {
context
.read<DatabaseTabBarBloc>()
.add(DatabaseTabBarEvent.selectView(view.id));
},
leftIcon: _buildViewIconButton(context, view),
trailing: _trailing(context, isSelected),
showTopBorder: showTopBorder,
);
},
);
}
Widget _buildViewIconButton(BuildContext context, ViewPB view) {
return view.icon.value.isNotEmpty
? EmojiText(
emoji: view.icon.value,
fontSize: 16.0,
)
: SizedBox.square(
dimension: 20.0,
child: view.defaultIcon(),
);
return SizedBox.square(
dimension: 20.0,
child: view.defaultIcon(),
);
}
Widget _trailing(BuildContext context, bool isSelected) {
final more = FlowyIconButton(
icon: FlowySvg(
FlowySvgs.three_dots_s,
size: const Size.square(20),
color: Theme.of(context).hintColor,
),
onPressed: () {
showMobileBottomSheet(
context,
padding: EdgeInsets.zero,
builder: (_) {
return BlocProvider<ViewBloc>(
create: (_) =>
ViewBloc(view: view)..add(const ViewEvent.initial()),
child: MobileDatabaseViewQuickActions(
view: view,
databaseController: databaseController,
),
);
},
);
},
);
if (isSelected) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.blue_check_s,
size: Size.square(20),
blendMode: BlendMode.dst,
),
const HSpace(8),
more,
],
);
} else {
return more;
}
}
}
@ -83,12 +186,20 @@ class MobileNewDatabaseViewButton extends StatelessWidget {
size: const Size.square(20),
color: Theme.of(context).hintColor,
),
trailing: FlowySvg(
FlowySvgs.three_dots_s,
size: const Size.square(20),
color: Theme.of(context).hintColor,
),
onTap: () {},
onTap: () async {
final result = await showMobileBottomSheet<(DatabaseLayoutPB, String)>(
context,
padding: EdgeInsets.zero,
builder: (_) {
return const MobileCreateDatabaseView();
},
);
if (context.mounted && result != null) {
context
.read<DatabaseTabBarBloc>()
.add(DatabaseTabBarEvent.createView(result.$1, result.$2));
}
},
);
}
}
@ -104,13 +215,13 @@ class MobileCreateDatabaseView extends StatefulWidget {
class _MobileCreateDatabaseViewState extends State<MobileCreateDatabaseView> {
late final TextEditingController controller;
DatabaseLayoutPB layoutType = DatabaseLayoutPB.Grid;
String icon = "";
@override
void initState() {
super.initState();
controller =
TextEditingController(text: LocaleKeys.grid_title_placeholder.tr());
controller = TextEditingController(
text: LocaleKeys.grid_title_placeholder.tr(),
);
}
@override
@ -123,42 +234,93 @@ class _MobileCreateDatabaseViewState extends State<MobileCreateDatabaseView> {
Widget build(BuildContext context) {
return Column(
children: [
const Center(child: DragHandler()),
_CreateViewHeader(
textController: controller,
selectedLayout: layoutType,
),
FlowyOptionTile.textField(
controller: controller,
leftIcon: _buildViewIcon(),
),
const VSpace(20),
DatabaseViewLayoutPicker(
selectedLayout: DatabaseLayoutPB.Grid,
onSelect: (layout) {},
selectedLayout: layoutType,
onSelect: (layout) {
setState(() => layoutType = layout);
},
),
],
);
}
}
Widget _buildViewIcon() {
final viewIcon = icon.isNotEmpty
? EmojiText(
emoji: icon,
fontSize: 16.0,
)
: SizedBox.square(
dimension: 18.0,
child: FlowySvg(layoutType.icon),
);
return InkWell(
onTap: () {},
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
class _CreateViewHeader extends StatelessWidget {
const _CreateViewHeader({
required this.textController,
required this.selectedLayout,
});
final TextEditingController textController;
final DatabaseLayoutPB selectedLayout;
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox.square(
dimension: 36,
child: IconButton(
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
padding: EdgeInsets.zero,
onPressed: () => context.pop(),
icon: const FlowySvg(
FlowySvgs.arrow_left_s,
size: Size.square(20),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 8),
child: TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 0,
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
enableFeedback: true,
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: () {
context.pop((selectedLayout, textController.text.trim()));
},
child: FlowyText.medium(
LocaleKeys.button_done.tr(),
fontSize: 16,
color: Theme.of(context).colorScheme.onPrimary,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
Center(
child: FlowyText.medium(
LocaleKeys.grid_settings_createView.tr(),
fontSize: 16,
),
),
width: 36,
height: 36,
child: Center(child: viewIcon),
),
],
);
}
}

View File

@ -1,34 +1,69 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'edit_database_view_screen.dart';
/// [MobileDatabaseViewQuickActions] is gives users to quickly edit a database
/// view from the [MobileDatabaseViewList]
class MobileDatabaseViewQuickActions extends StatelessWidget {
const MobileDatabaseViewQuickActions({super.key});
class MobileDatabaseViewQuickActions extends StatefulWidget {
const MobileDatabaseViewQuickActions({
super.key,
required this.view,
required this.databaseController,
});
final ViewPB view;
final DatabaseController databaseController;
@override
State<MobileDatabaseViewQuickActions> createState() =>
_MobileDatabaseViewQuickActionsState();
}
class _MobileDatabaseViewQuickActionsState
extends State<MobileDatabaseViewQuickActions> {
bool isEditing = false;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_actionButton(_Action.edit),
_divider(),
_actionButton(_Action.duplicate),
_divider(),
_actionButton(_Action.delete),
_divider(),
],
);
return isEditing
? MobileEditDatabaseViewScreen(
databaseController: widget.databaseController,
)
: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 38),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_actionButton(context, _Action.edit),
_divider(),
_actionButton(context, _Action.duplicate),
_divider(),
_actionButton(context, _Action.delete),
_divider(),
],
),
);
}
Widget _actionButton(_Action action) {
return MobileQuickActionButton(
icon: action.icon,
text: action.label,
onTap: () {},
Widget _actionButton(BuildContext context, _Action action) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: MobileQuickActionButton(
icon: action.icon,
text: action.label,
color: action.color(context),
onTap: () {
if (action == _Action.edit) {
setState(() => isEditing = true);
}
},
),
);
}
@ -50,7 +85,7 @@ enum _Action {
FlowySvgData get icon {
return switch (this) {
edit => FlowySvgs.grid_s,
edit => FlowySvgs.edit_s,
duplicate => FlowySvgs.copy_s,
delete => FlowySvgs.delete_s,
};

View File

@ -3,13 +3,11 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
import 'package:appflowy/plugins/database_view/application/database_view_service.dart';
import 'package:appflowy/plugins/database_view/application/layout/layout_service.dart';
import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
@ -29,11 +27,9 @@ class MobileEditDatabaseViewScreen extends StatefulWidget {
const MobileEditDatabaseViewScreen({
super.key,
required this.databaseController,
required this.viewPB,
});
final DatabaseController databaseController;
final ViewPB viewPB;
@override
State<MobileEditDatabaseViewScreen> createState() =>
@ -52,13 +48,11 @@ class _MobileEditDatabaseViewScreenState
return switch (state.currentPage) {
MobileEditDatabaseViewPageEnum.main => _EditDatabaseViewMainPage(
databaseController: widget.databaseController,
viewPB: widget.viewPB,
),
MobileEditDatabaseViewPageEnum.fields => _wrapSubPage(
context,
MobileDatabaseFieldList(
databaseController: widget.databaseController,
viewPB: widget.viewPB,
),
),
_ => const SizedBox.shrink(),
@ -84,11 +78,9 @@ class _MobileEditDatabaseViewScreenState
class _EditDatabaseViewMainPage extends StatelessWidget {
const _EditDatabaseViewMainPage({
required this.databaseController,
required this.viewPB,
});
final DatabaseController databaseController;
final ViewPB viewPB;
@override
Widget build(BuildContext context) {
@ -107,7 +99,6 @@ class _EditDatabaseViewMainPage extends StatelessWidget {
child: SingleChildScrollView(
child: _EditDatabaseViewBody(
databaseController: databaseController,
view: viewPB,
),
),
),
@ -157,41 +148,40 @@ class _EditDatabaseViewHeader extends StatelessWidget {
class _EditDatabaseViewBody extends StatelessWidget {
const _EditDatabaseViewBody({
required this.databaseController,
required this.view,
});
final DatabaseController databaseController;
final ViewPB view;
@override
Widget build(BuildContext context) {
return BlocProvider<ViewBloc>(
create: (context) {
return ViewBloc(view: view)..add(const ViewEvent.initial());
return BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_NameAndIcon(view: state.view),
_divider(),
DatabaseViewSettingTile(
setting: DatabaseViewSettings.layout,
databaseController: databaseController,
view: state.view,
showTopBorder: true,
),
if (databaseController.databaseLayout == DatabaseLayoutPB.Calendar)
DatabaseViewSettingTile(
setting: DatabaseViewSettings.calendar,
databaseController: databaseController,
view: state.view,
),
DatabaseViewSettingTile(
setting: DatabaseViewSettings.fields,
databaseController: databaseController,
view: state.view,
),
_divider(),
],
);
},
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_NameAndIcon(view: state.view),
_divider(),
DatabaseViewSettingTile(
setting: DatabaseViewSettings.layout,
databaseController: databaseController,
view: state.view,
showTopBorder: true,
),
DatabaseViewSettingTile(
setting: DatabaseViewSettings.fields,
databaseController: databaseController,
view: state.view,
),
_divider(),
],
);
},
),
);
}
@ -220,37 +210,9 @@ class _NameAndIconState extends State<_NameAndIcon> {
Widget build(BuildContext context) {
return FlowyOptionTile.textField(
controller: textEditingController,
textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0),
onTextChanged: (text) {
context.read<ViewBloc>().add(ViewEvent.rename(text));
},
leftIcon: _buildViewIcon(),
);
}
Widget _buildViewIcon() {
final icon = widget.view.icon.value.isNotEmpty
? EmojiText(
emoji: widget.view.icon.value,
fontSize: 16.0,
)
: SizedBox.square(
dimension: 18.0,
child: widget.view.defaultIcon(),
);
return InkWell(
onTap: () {},
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border.fromBorderSide(
BorderSide(color: Theme.of(context).dividerColor),
),
),
width: 36,
height: 36,
child: Center(child: icon),
),
);
}
}
@ -351,7 +313,8 @@ class DatabaseViewSettingTile extends StatelessWidget {
return Row(
children: [
FlowyText(
"$numVisible shown",
LocaleKeys.grid_settings_numberOfVisibleFields
.tr(args: [numVisible.toString()]),
color: Theme.of(context).hintColor,
),
const HSpace(8),

View File

@ -1,227 +0,0 @@
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class SheetPage {
const SheetPage({
required this.title,
required this.body,
});
final String title;
final Widget body;
}
void showPaginatedBottomSheet(BuildContext context, {required SheetPage page}) {
showMobileBottomSheet(
context,
// Workaround for not causing drag to rebuild
isDragEnabled: false,
builder: (context) => FlowyBottomSheet(root: page),
);
}
typedef SheetNotifier = ValueNotifier<(SheetPage, bool)>;
class FlowyBottomSheet extends StatelessWidget {
FlowyBottomSheet({
super.key,
required this.root,
}) : _notifier = ValueNotifier((root, true));
final SheetPage root;
final SheetNotifier _notifier;
@override
Widget build(BuildContext context) {
return FlowyBottomSheetController(
key: UniqueKey(),
root: root,
onPageChanged: (page, isRoot) => _notifier.value = (page, isRoot),
child: _FlowyBottomSheetHandler(
root: root,
notifier: _notifier,
),
);
}
}
class _FlowyBottomSheetHandler extends StatefulWidget {
const _FlowyBottomSheetHandler({
required this.root,
required this.notifier,
});
final SheetPage root;
final ValueNotifier<(SheetPage, bool)> notifier;
@override
State<_FlowyBottomSheetHandler> createState() =>
_FlowyBottomSheetHandlerState();
}
class _FlowyBottomSheetHandlerState extends State<_FlowyBottomSheetHandler> {
late SheetPage currentPage;
late bool isRoot;
@override
void initState() {
super.initState();
widget.notifier.addListener(_onPageChanged);
isRoot = true;
currentPage = widget.root;
WidgetsBinding.instance.addPostFrameCallback((_) {
currentPage = FlowyBottomSheetController.of(context)!.currentPage;
});
}
@override
void dispose() {
widget.notifier.removeListener(_onPageChanged);
super.dispose();
}
void _onPageChanged() {
final (page, root) = widget.notifier.value;
if (mounted) {
setState(() {
currentPage = page;
isRoot = root;
});
}
}
@override
Widget build(BuildContext context) {
return AnimatedSize(
duration: const Duration(milliseconds: 150),
child: FlowyBottomSheetPage(
isRoot: isRoot,
title: currentPage.title,
child: currentPage.body,
),
);
}
}
class FlowyBottomSheetPage extends StatelessWidget {
const FlowyBottomSheetPage({
super.key,
required this.title,
required this.child,
this.isRoot = false,
});
final String title;
final Widget child;
final bool isRoot;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_SheetTopBar(title: title, isRoot: isRoot),
child,
],
);
}
}
class _SheetTopBar extends StatelessWidget {
const _SheetTopBar({
required this.title,
this.isRoot = false,
});
final String title;
final bool isRoot;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
if (!isRoot) ...[
IconButton(
onPressed: () => FlowyBottomSheetController.of(context)!.pop(),
icon: const Icon(Icons.arrow_back_ios),
),
const HSpace(6),
],
Text(
title,
style: theme.textTheme.labelSmall,
),
const Spacer(),
IconButton(
icon: Icon(
Icons.close,
color: theme.hintColor,
),
onPressed: () => context.pop(),
),
],
);
}
}
class FlowyBottomSheetController extends InheritedWidget {
FlowyBottomSheetController({
super.key,
required SheetPage root,
this.onPageChanged,
required super.child,
FlowyBottomSheetControllerImpl? controller,
}) : _controller = controller ?? FlowyBottomSheetControllerImpl(root: root);
final Function(SheetPage page, bool isRoot)? onPageChanged;
final FlowyBottomSheetControllerImpl _controller;
SheetPage get currentPage => _controller.page;
@override
bool updateShouldNotify(covariant FlowyBottomSheetController oldWidget) {
return child != oldWidget.child ||
_controller.length != oldWidget._controller.length;
}
static FlowyBottomSheetController? of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<FlowyBottomSheetController>();
}
void push(SheetPage page) {
_controller.push(page);
onPageChanged?.call(_controller.page, _controller.isRoot);
}
void pop() {
_controller.pop();
onPageChanged?.call(_controller.page, _controller.isRoot);
}
}
class FlowyBottomSheetControllerImpl {
FlowyBottomSheetControllerImpl({
required SheetPage root,
}) : _pages = [root];
final List<SheetPage> _pages;
SheetPage get page => _pages.last;
bool get isRoot => _pages.length == 1;
int get length => _pages.length;
void push(SheetPage page) {
_pages.add(page);
}
void pop() {
_pages.remove(page);
}
}