mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: mobile card detail screen (#3935)
* feat: add CardDetailScreen and CardPropertyEditScreen - add basic UI layout for these two screens - add MobileTextCell as the GridCellWidget adapts to mobile * feat: add MobileNumberCell and MobileTimestampCell * feat: Add MobileDateCell and MobileCheckboxCell - Add MobileDateCellEditScreen - Add dateStr and endDateStr in DateCellCalendarState * feat: add MobileFieldTypeOptionEditor - Add placeholder for different TypeOptionMobileWidgetBuilders - Add _MobileSwitchFieldButton * feat: add property delete feature in CardPropertyEditScreen * fix: fix VisibilitySwitch didn't update * feat: add MobileCreateRowFieldScreen - Refactor MobileFieldEditor to used in CardPropertyEditScreen and MobileCreateRowFieldScreen - Add MobileCreateRowFieldScreen * chore: localization and improve spacing * feat: add TimestampTypeOptionMobileWidget - Refactor TimeFormatListTile to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody - Add IncludeTimeSwitch to be used in TimestampTypeOptionMobileWidget and _DateCellEditBody * feat: add checkbox and DateTypeOptionMobileWidget * chore: improve UI * chore: improve spacing * fix: fix end time shown issue * fix: minor issues * fix: flutter analyze * chore: delete unused TextEditingController * fix: prevent group field from deleting * feat: add NumberTypeOptionMobileWidget * chore: rename and clean code * chore: clean code * chore: extract class * chore: refactor reorder cells * chore: improve super.key * chore: refactor MobileFieldTypeList * chore: reorginize code * chore: remove unused import file * chore: clean code * chore: add commas due to flutter upgrade * feat: add MobileURLCell * fix: close keyboard when user tap outside of textfield * chore: update go_router version * fix: add missing GridCellStyle --------- Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
parent
b9ecc7ceb6
commit
acc951c5eb
@ -0,0 +1 @@
|
|||||||
|
export 'card_detail/mobile_card_detail_screen.dart';
|
@ -0,0 +1,186 @@
|
|||||||
|
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/bottom_sheet/bottom_sheet_action_widget.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_banner_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_action_sheet_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_document.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 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class MobileCardDetailScreen extends StatefulWidget {
|
||||||
|
const MobileCardDetailScreen({
|
||||||
|
super.key,
|
||||||
|
required this.rowController,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const routeName = '/MobileCardDetailScreen';
|
||||||
|
static const argRowController = 'rowController';
|
||||||
|
static const argCellBuilder = 'cellBuilder';
|
||||||
|
final RowController rowController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileCardDetailScreen> createState() => _MobileCardDetailScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
||||||
|
late final ScrollController _scrollController;
|
||||||
|
late final GridCellBuilder _cellBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController = ScrollController();
|
||||||
|
_cellBuilder = GridCellBuilder(
|
||||||
|
cellCache: widget.rowController.cellCache,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// TODO(yijing): fix context issue when navigating in bottom navigation bar
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => RowDetailBloc(rowController: widget.rowController)
|
||||||
|
..add(const RowDetailEvent.initial()),
|
||||||
|
child: Scaffold(
|
||||||
|
// appbar with duplicate and delete card features
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(LocaleKeys.board_cardDetail.tr()),
|
||||||
|
actions: [
|
||||||
|
BlocProvider<RowActionSheetBloc>(
|
||||||
|
create: (context) => RowActionSheetBloc(
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
rowId: widget.rowController.rowId,
|
||||||
|
groupId: widget.rowController.groupId,
|
||||||
|
),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFlowyMobileBottomSheet(
|
||||||
|
context,
|
||||||
|
title: LocaleKeys.board_cardActions.tr(),
|
||||||
|
builder: (_) => Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: BottomSheetActionWidget(
|
||||||
|
svg: FlowySvgs.copy_s,
|
||||||
|
text: LocaleKeys.button_duplicate.tr(),
|
||||||
|
onTap: () {
|
||||||
|
context.read<RowActionSheetBloc>().add(
|
||||||
|
const RowActionSheetEvent
|
||||||
|
.duplicateRow(),
|
||||||
|
);
|
||||||
|
context
|
||||||
|
..pop()
|
||||||
|
..pop();
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: LocaleKeys.board_cardDuplicated.tr(),
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
Expanded(
|
||||||
|
child: BottomSheetActionWidget(
|
||||||
|
svg: FlowySvgs.m_delete_m,
|
||||||
|
text: LocaleKeys.button_delete.tr(),
|
||||||
|
onTap: () {
|
||||||
|
context.read<RowActionSheetBloc>().add(
|
||||||
|
const RowActionSheetEvent.deleteRow(),
|
||||||
|
);
|
||||||
|
context
|
||||||
|
..pop()
|
||||||
|
..pop();
|
||||||
|
Fluttertoast.showToast(
|
||||||
|
msg: LocaleKeys.board_cardDeleted.tr(),
|
||||||
|
gravity: ToastGravity.CENTER,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ListView(
|
||||||
|
controller: _scrollController,
|
||||||
|
children: [
|
||||||
|
BlocProvider<RowBannerBloc>(
|
||||||
|
create: (context) => RowBannerBloc(
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
rowMeta: widget.rowController.rowMeta,
|
||||||
|
)..add(const RowBannerEvent.initial()),
|
||||||
|
child: BlocBuilder<RowBannerBloc, RowBannerState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// primaryField is the property cannot be deleted like card title
|
||||||
|
if (state.primaryField != null) {
|
||||||
|
final mobileStyle = GridTextCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_titlePlaceholder.tr(),
|
||||||
|
textStyle: Theme.of(context).textTheme.titleLarge,
|
||||||
|
);
|
||||||
|
|
||||||
|
// get the cell context for the card title
|
||||||
|
final cellContext = DatabaseCellContext(
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
rowMeta: widget.rowController.rowMeta,
|
||||||
|
fieldInfo: FieldInfo.initial(state.primaryField!),
|
||||||
|
);
|
||||||
|
|
||||||
|
return _cellBuilder.build(
|
||||||
|
cellContext,
|
||||||
|
style: mobileStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VSpace(8),
|
||||||
|
// Card Properties
|
||||||
|
MobileRowPropertyList(
|
||||||
|
cellBuilder: _cellBuilder,
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
const VSpace(16),
|
||||||
|
RowDocument(
|
||||||
|
viewId: widget.rowController.viewId,
|
||||||
|
rowId: widget.rowController.rowId,
|
||||||
|
scrollController: _scrollController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class MobileCreateRowFieldScreen extends StatefulWidget {
|
||||||
|
static const routeName = '/MobileCreateRowFieldScreen';
|
||||||
|
static const argViewId = 'viewId';
|
||||||
|
static const argTypeOption = 'typeOption';
|
||||||
|
|
||||||
|
const MobileCreateRowFieldScreen({
|
||||||
|
required this.viewId,
|
||||||
|
required this.typeOption,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
final TypeOptionPB typeOption;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileCreateRowFieldScreen> createState() =>
|
||||||
|
_MobileCreateRowFieldScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileCreateRowFieldScreenState
|
||||||
|
extends State<MobileCreateRowFieldScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(LocaleKeys.grid_field_newProperty.tr()),
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: Text(
|
||||||
|
LocaleKeys.button_done.tr(),
|
||||||
|
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: MobileFieldEditor(
|
||||||
|
viewId: widget.viewId,
|
||||||
|
typeOptionLoader: FieldTypeOptionLoader(
|
||||||
|
viewId: widget.viewId,
|
||||||
|
field: widget.typeOption.field_2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class MobileCreateRowFieldButton extends StatelessWidget {
|
||||||
|
const MobileCreateRowFieldButton({super.key, required this.viewId});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextButton.icon(
|
||||||
|
label: Text(
|
||||||
|
LocaleKeys.grid_field_newProperty.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||||
|
viewId: viewId,
|
||||||
|
);
|
||||||
|
result.fold(
|
||||||
|
(typeOption) {
|
||||||
|
context.push(
|
||||||
|
MobileCreateRowFieldScreen.routeName,
|
||||||
|
extra: {
|
||||||
|
MobileCreateRowFieldScreen.argViewId: viewId,
|
||||||
|
MobileCreateRowFieldScreen.argTypeOption: typeOption,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(r) => Log.error("Failed to create field type option: $r"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: FlowySvg(
|
||||||
|
FlowySvgs.add_m,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class MobileFieldNameTextField extends StatefulWidget {
|
||||||
|
const MobileFieldNameTextField({
|
||||||
|
super.key,
|
||||||
|
this.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? text;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileFieldNameTextField> createState() =>
|
||||||
|
_MobileFieldNameTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileFieldNameTextFieldState extends State<MobileFieldNameTextField> {
|
||||||
|
final controller = TextEditingController();
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.text != null) {
|
||||||
|
controller.text = widget.text!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
onChanged: (newName) {
|
||||||
|
context
|
||||||
|
.read<FieldEditorBloc>()
|
||||||
|
.add(FieldEditorEvent.updateName(newName));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,221 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/mobile_create_row_field_button.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.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 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
/// Display the row properties in a list. Only use this widget in the
|
||||||
|
/// [MobileCardDetailScreen].
|
||||||
|
class MobileRowPropertyList extends StatelessWidget {
|
||||||
|
const MobileRowPropertyList({
|
||||||
|
super.key,
|
||||||
|
required this.viewId,
|
||||||
|
required this.cellBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
final GridCellBuilder cellBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final List<DatabaseCellContext> visibleCells = state.visibleCells
|
||||||
|
.where((element) => !element.fieldInfo.field.isPrimary)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return ReorderableListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: visibleCells.length,
|
||||||
|
itemBuilder: (context, index) => _PropertyCell(
|
||||||
|
key: ValueKey('row_detail_${visibleCells[index].fieldId}'),
|
||||||
|
cellContext: visibleCells[index],
|
||||||
|
cellBuilder: cellBuilder,
|
||||||
|
index: index,
|
||||||
|
),
|
||||||
|
onReorder: (oldIndex, newIndex) {
|
||||||
|
// when reorderiing downwards, need to update index
|
||||||
|
if (oldIndex < newIndex) {
|
||||||
|
newIndex--;
|
||||||
|
}
|
||||||
|
final reorderedFieldId = visibleCells[oldIndex].fieldId;
|
||||||
|
final targetFieldId = visibleCells[newIndex].fieldId;
|
||||||
|
|
||||||
|
context.read<RowDetailBloc>().add(
|
||||||
|
RowDetailEvent.reorderField(
|
||||||
|
reorderedFieldId,
|
||||||
|
targetFieldId,
|
||||||
|
oldIndex,
|
||||||
|
newIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
footer: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (context.read<RowDetailBloc>().state.numHiddenFields != 0)
|
||||||
|
const ToggleHiddenFieldsVisibilityButton(),
|
||||||
|
const VSpace(8),
|
||||||
|
// add new field
|
||||||
|
MobileCreateRowFieldButton(
|
||||||
|
viewId: viewId,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(yijing): temperary locate here
|
||||||
|
// It may need to be share with other widgets
|
||||||
|
const cellHeight = 32.0;
|
||||||
|
|
||||||
|
class _PropertyCell extends StatefulWidget {
|
||||||
|
const _PropertyCell({
|
||||||
|
super.key,
|
||||||
|
required this.cellContext,
|
||||||
|
required this.cellBuilder,
|
||||||
|
required this.index,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DatabaseCellContext cellContext;
|
||||||
|
final GridCellBuilder cellBuilder;
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _PropertyCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PropertyCellState extends State<_PropertyCell> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final style = _customCellStyle(widget.cellContext.fieldType);
|
||||||
|
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
horizontalTitleGap: 4,
|
||||||
|
// FieldCellButton in Desktop
|
||||||
|
// TODO(yijing): adjust width with sreen size
|
||||||
|
leading: SizedBox(
|
||||||
|
width: 150,
|
||||||
|
height: cellHeight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
icon: FlowySvg(
|
||||||
|
widget.cellContext.fieldInfo.field.fieldType.icon(),
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
widget.cellContext.fieldInfo.field.name,
|
||||||
|
// TODO(yijing): update text style
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
// naivgator to field editor
|
||||||
|
onPressed: () => context.push(
|
||||||
|
CardPropertyEditScreen.routeName,
|
||||||
|
extra: {
|
||||||
|
CardPropertyEditScreen.argCellContext: widget.cellContext,
|
||||||
|
CardPropertyEditScreen.argRowDetailBloc:
|
||||||
|
context.read<RowDetailBloc>(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => cell.requestFocus.notify(),
|
||||||
|
//property values
|
||||||
|
child: AccessoryHover(
|
||||||
|
fieldType: widget.cellContext.fieldType,
|
||||||
|
child: cell,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.Checkbox:
|
||||||
|
return GridCheckboxCellStyle(
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
|
);
|
||||||
|
case FieldType.DateTime:
|
||||||
|
return DateCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
);
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return TimestampCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
);
|
||||||
|
case FieldType.MultiSelect:
|
||||||
|
return SelectOptionCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
|
);
|
||||||
|
case FieldType.Checklist:
|
||||||
|
return ChecklistCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
cellPadding: EdgeInsets.zero,
|
||||||
|
showTasksInline: true,
|
||||||
|
);
|
||||||
|
case FieldType.Number:
|
||||||
|
return GridNumberCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
);
|
||||||
|
case FieldType.RichText:
|
||||||
|
return GridTextCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
);
|
||||||
|
case FieldType.SingleSelect:
|
||||||
|
return SelectOptionCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||||
|
);
|
||||||
|
|
||||||
|
case FieldType.URL:
|
||||||
|
return GridURLCellStyle(
|
||||||
|
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||||
|
accessoryTypes: [
|
||||||
|
GridURLCellAccessoryType.copyURL,
|
||||||
|
GridURLCellAccessoryType.visitURL,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw UnimplementedError;
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
export 'mobile_field_name_text_field.dart';
|
||||||
|
export 'mobile_create_row_field_button.dart';
|
||||||
|
export 'mobile_row_property_list.dart';
|
@ -0,0 +1,64 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_editor.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class CardPropertyEditScreen extends StatelessWidget {
|
||||||
|
const CardPropertyEditScreen({
|
||||||
|
super.key,
|
||||||
|
required this.cellContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const routeName = '/CardPropertyEditScreen';
|
||||||
|
static const argCellContext = 'cellContext';
|
||||||
|
static const argRowDetailBloc = 'rowDetailBloc';
|
||||||
|
|
||||||
|
final DatabaseCellContext cellContext;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(LocaleKeys.grid_field_editProperty.tr()),
|
||||||
|
actions: [
|
||||||
|
// show delete button when this field is not used to group cards
|
||||||
|
if (!cellContext.fieldInfo.isGroupField)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showFlowyMobileConfirmDialog(
|
||||||
|
context,
|
||||||
|
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||||
|
actionButtonTitle: LocaleKeys.button_delete.tr(),
|
||||||
|
actionButtonColor: Theme.of(context).colorScheme.error,
|
||||||
|
onActionButtonPressed: () {
|
||||||
|
context.read<RowDetailBloc>().add(
|
||||||
|
RowDetailEvent.deleteField(
|
||||||
|
cellContext.fieldInfo.field.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const FlowySvg(FlowySvgs.m_delete_m),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: MobileFieldEditor(
|
||||||
|
viewId: cellContext.viewId,
|
||||||
|
typeOptionLoader: FieldTypeOptionLoader(
|
||||||
|
viewId: cellContext.viewId,
|
||||||
|
field: cellContext.fieldInfo.field,
|
||||||
|
),
|
||||||
|
fieldInfo: cellContext.fieldInfo,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,130 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/mobile_field_type_option_editor.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_editor_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
// Used in [CardPropertyEditScreen] and [MobileCreateRowFieldScreen]
|
||||||
|
class MobileFieldEditor extends StatelessWidget {
|
||||||
|
const MobileFieldEditor({
|
||||||
|
super.key,
|
||||||
|
required this.viewId,
|
||||||
|
required this.typeOptionLoader,
|
||||||
|
this.isGroupingField = false,
|
||||||
|
this.fieldInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String viewId;
|
||||||
|
final bool isGroupingField;
|
||||||
|
final FieldTypeOptionLoader typeOptionLoader;
|
||||||
|
final FieldInfo? fieldInfo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) {
|
||||||
|
return FieldEditorBloc(
|
||||||
|
// group field is the field to be used to group cards in database view, it can not be deleted
|
||||||
|
isGroupField: isGroupingField,
|
||||||
|
loader: typeOptionLoader,
|
||||||
|
field: typeOptionLoader.field,
|
||||||
|
)..add(const FieldEditorEvent.initial());
|
||||||
|
},
|
||||||
|
child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// for field type edit option
|
||||||
|
final dataController = context.read<FieldEditorBloc>().dataController;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// property name
|
||||||
|
// TODO(yijing): improve hint text
|
||||||
|
PropertyTitle(LocaleKeys.settings_user_name.tr()),
|
||||||
|
BlocSelector<FieldEditorBloc, FieldEditorState, String>(
|
||||||
|
selector: (state) {
|
||||||
|
return state.name;
|
||||||
|
},
|
||||||
|
builder: (context, propertyName) {
|
||||||
|
return MobileFieldNameTextField(
|
||||||
|
text: propertyName,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
PropertyTitle(LocaleKeys.grid_field_visibility.tr()),
|
||||||
|
const Spacer(),
|
||||||
|
VisibilitySwitch(
|
||||||
|
isFieldHidden:
|
||||||
|
fieldInfo?.visibility == FieldVisibility.AlwaysHidden,
|
||||||
|
onChanged: () {
|
||||||
|
state.field.fold(
|
||||||
|
() => Log.error('Can not hidden the field'),
|
||||||
|
(field) => context.read<RowDetailBloc>().add(
|
||||||
|
RowDetailEvent.toggleFieldVisibility(
|
||||||
|
field.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const VSpace(8),
|
||||||
|
// edit property type and settings
|
||||||
|
if (!typeOptionLoader.field.isPrimary)
|
||||||
|
MobileFieldTypeOptionEditor(
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VisibilitySwitch extends StatefulWidget {
|
||||||
|
const VisibilitySwitch({
|
||||||
|
super.key,
|
||||||
|
required this.isFieldHidden,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isFieldHidden;
|
||||||
|
final Function? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VisibilitySwitch> createState() => _VisibilitySwitchState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VisibilitySwitchState extends State<VisibilitySwitch> {
|
||||||
|
late bool _isFieldHidden = widget.isFieldHidden;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Switch.adaptive(
|
||||||
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
value: !_isFieldHidden,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
setState(() {
|
||||||
|
_isFieldHidden = !_isFieldHidden;
|
||||||
|
widget.onChanged?.call();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_type_option_edit_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||||
|
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
typedef SelectFieldCallback = void Function(FieldType);
|
||||||
|
|
||||||
|
class MobileFieldTypeList extends StatelessWidget {
|
||||||
|
const MobileFieldTypeList({
|
||||||
|
super.key,
|
||||||
|
required this.onSelectField,
|
||||||
|
required this.bloc,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FieldTypeOptionEditBloc bloc;
|
||||||
|
final SelectFieldCallback onSelectField;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const allFieldTypes = FieldType.values;
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: bloc,
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: allFieldTypes.length,
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
return MobileFieldTypeCell(
|
||||||
|
fieldType: allFieldTypes[index],
|
||||||
|
onSelectField: onSelectField,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MobileFieldTypeCell extends StatelessWidget {
|
||||||
|
const MobileFieldTypeCell({
|
||||||
|
super.key,
|
||||||
|
required this.fieldType,
|
||||||
|
required this.onSelectField,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FieldType fieldType;
|
||||||
|
final SelectFieldCallback onSelectField;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RadioListTile<FieldType>(
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
value: fieldType,
|
||||||
|
groupValue: context.select(
|
||||||
|
(FieldTypeOptionEditBloc bloc) => bloc.state.field.fieldType,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
onSelectField(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
FlowySvg(
|
||||||
|
fieldType.icon(),
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
Text(
|
||||||
|
fieldType.title(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/type_option_widget_builder/type_option_widget_builder.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_type_option_edit_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_data_controller.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.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 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'mobile_field_type_list.dart';
|
||||||
|
|
||||||
|
class MobileFieldTypeOptionEditor extends StatelessWidget {
|
||||||
|
const MobileFieldTypeOptionEditor({
|
||||||
|
super.key,
|
||||||
|
required TypeOptionController dataController,
|
||||||
|
}) : _dataController = dataController;
|
||||||
|
|
||||||
|
final TypeOptionController _dataController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<FieldTypeOptionEditBloc>(
|
||||||
|
create: (_) => FieldTypeOptionEditBloc(_dataController)
|
||||||
|
..add(const FieldTypeOptionEditEvent.initial()),
|
||||||
|
child: BlocBuilder<FieldTypeOptionEditBloc, FieldTypeOptionEditState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final typeOptionWidget = _makeTypeOptionMobileWidget(
|
||||||
|
context: context,
|
||||||
|
dataController: _dataController,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const _MobileSwitchFieldButton(),
|
||||||
|
if (typeOptionWidget != null) ...[
|
||||||
|
const VSpace(8),
|
||||||
|
typeOptionWidget,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileSwitchFieldButton extends StatelessWidget {
|
||||||
|
const _MobileSwitchFieldButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final fieldType = context.select(
|
||||||
|
(FieldTypeOptionEditBloc bloc) => bloc.state.field.fieldType,
|
||||||
|
);
|
||||||
|
return GestureDetector(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
LocaleKeys.grid_field_propertyType.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
FlowySvg(fieldType.icon()),
|
||||||
|
const HSpace(4),
|
||||||
|
Text(
|
||||||
|
fieldType.title(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios_sharp,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => showFlowyMobileBottomSheet(
|
||||||
|
context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
title: LocaleKeys.grid_field_propertyType.tr(),
|
||||||
|
builder: (_) => MobileFieldTypeList(
|
||||||
|
bloc: context.read<FieldTypeOptionEditBloc>(),
|
||||||
|
onSelectField: (newFieldType) {
|
||||||
|
context.read<FieldTypeOptionEditBloc>().add(
|
||||||
|
FieldTypeOptionEditEvent.switchToField(newFieldType),
|
||||||
|
);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _makeTypeOptionMobileWidget({
|
||||||
|
required BuildContext context,
|
||||||
|
required TypeOptionController dataController,
|
||||||
|
}) =>
|
||||||
|
_makeTypeOptionMobileWidgetBuilder(
|
||||||
|
dataController: dataController,
|
||||||
|
).build(context);
|
||||||
|
|
||||||
|
TypeOptionWidgetBuilder _makeTypeOptionMobileWidgetBuilder({
|
||||||
|
required TypeOptionController dataController,
|
||||||
|
}) {
|
||||||
|
final viewId = dataController.loader.viewId;
|
||||||
|
final fieldType = dataController.field.fieldType;
|
||||||
|
|
||||||
|
switch (dataController.field.fieldType) {
|
||||||
|
case FieldType.Checkbox:
|
||||||
|
return CheckboxTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<CheckboxTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case FieldType.DateTime:
|
||||||
|
return DateTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<DateTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return TimestampTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<TimestampTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case FieldType.SingleSelect:
|
||||||
|
return SingleSelectTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<SingleSelectTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case FieldType.MultiSelect:
|
||||||
|
return MultiSelectTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<MultiSelectTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case FieldType.Number:
|
||||||
|
return NumberTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<NumberTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case FieldType.RichText:
|
||||||
|
return RichTextTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<RichTextTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case FieldType.URL:
|
||||||
|
return URLTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<URLTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case FieldType.Checklist:
|
||||||
|
return ChecklistTypeOptionMobileWidgetBuilder(
|
||||||
|
makeTypeOptionContextWithDataController<ChecklistTypeOptionPB>(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldType: fieldType,
|
||||||
|
dataController: dataController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw UnimplementedError;
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CheckboxTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
CheckboxTypeOptionMobileWidgetBuilder(
|
||||||
|
CheckboxTypeOptionContext typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => null;
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ChecklistTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
ChecklistTypeOptionMobileWidgetBuilder(
|
||||||
|
ChecklistTypeOptionContext typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => const Text('Under Construction');
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import 'package:appflowy/mobile/presentation/database/card/row/cells/date_cell/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/date_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class DateTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
final DateTypeOptionMobileWidget _widget;
|
||||||
|
DateTypeOptionMobileWidgetBuilder(
|
||||||
|
DateTypeOptionContext typeOptionContext,
|
||||||
|
) : _widget = DateTypeOptionMobileWidget(
|
||||||
|
typeOptionContext: typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => _widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateTypeOptionMobileWidget extends TypeOptionWidget {
|
||||||
|
const DateTypeOptionMobileWidget({
|
||||||
|
super.key,
|
||||||
|
required this.typeOptionContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTypeOptionContext typeOptionContext;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
DateTypeOptionBloc(typeOptionContext: typeOptionContext),
|
||||||
|
child: BlocConsumer<DateTypeOptionBloc, DateTypeOptionState>(
|
||||||
|
listener: (context, state) =>
|
||||||
|
typeOptionContext.typeOption = state.typeOption,
|
||||||
|
builder: (context, state) {
|
||||||
|
final List<Widget> children = [
|
||||||
|
DateFormatListTile(
|
||||||
|
currentFormatStr: state.typeOption.dateFormat.title(),
|
||||||
|
groupValue: context
|
||||||
|
.watch<DateTypeOptionBloc>()
|
||||||
|
.state
|
||||||
|
.typeOption
|
||||||
|
.dateFormat,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
if (newFormat != null) {
|
||||||
|
context.read<DateTypeOptionBloc>().add(
|
||||||
|
DateTypeOptionEvent.didSelectDateFormat(
|
||||||
|
newFormat,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TimeFormatListTile(
|
||||||
|
currentFormatStr: state.typeOption.timeFormat.title(),
|
||||||
|
groupValue: context
|
||||||
|
.watch<DateTypeOptionBloc>()
|
||||||
|
.state
|
||||||
|
.typeOption
|
||||||
|
.timeFormat,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
if (newFormat != null) {
|
||||||
|
context.read<DateTypeOptionBloc>().add(
|
||||||
|
DateTypeOptionEvent.didSelectTimeFormat(
|
||||||
|
newFormat,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
separatorBuilder: (_, index) => const VSpace(8),
|
||||||
|
itemCount: children.length,
|
||||||
|
itemBuilder: (_, index) => children[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MultiSelectTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
MultiSelectTypeOptionMobileWidgetBuilder(
|
||||||
|
MultiSelectTypeOptionContext typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => const Text('Under Construction');
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/widgets/property_title.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/number_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/number_format_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pbenum.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class NumberTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
final NumberTypeOptionMobileWidget _widget;
|
||||||
|
|
||||||
|
NumberTypeOptionMobileWidgetBuilder(
|
||||||
|
NumberTypeOptionContext typeOptionContext,
|
||||||
|
) : _widget = NumberTypeOptionMobileWidget(
|
||||||
|
typeOptionContext: typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) {
|
||||||
|
return _widget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumberTypeOptionMobileWidget extends TypeOptionWidget {
|
||||||
|
final NumberTypeOptionContext typeOptionContext;
|
||||||
|
|
||||||
|
const NumberTypeOptionMobileWidget({
|
||||||
|
super.key,
|
||||||
|
required this.typeOptionContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
NumberTypeOptionBloc(typeOptionContext: typeOptionContext),
|
||||||
|
child: BlocConsumer<NumberTypeOptionBloc, NumberTypeOptionState>(
|
||||||
|
listener: (context, state) =>
|
||||||
|
typeOptionContext.typeOption = state.typeOption,
|
||||||
|
builder: (context, state) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
PropertyTitle(LocaleKeys.grid_field_numberFormat.tr()),
|
||||||
|
const Spacer(),
|
||||||
|
const HSpace(4),
|
||||||
|
Text(
|
||||||
|
state.typeOption.format.title(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => showFlowyMobileBottomSheet(
|
||||||
|
context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
title: LocaleKeys.grid_field_numberFormat.tr(),
|
||||||
|
builder: (bottomsheetContext) => GestureDetector(
|
||||||
|
child: NumberFormatList(
|
||||||
|
onSelected: (format) {
|
||||||
|
context
|
||||||
|
.read<NumberTypeOptionBloc>()
|
||||||
|
.add(NumberTypeOptionEvent.didSelectFormat(format));
|
||||||
|
bottomsheetContext.pop();
|
||||||
|
},
|
||||||
|
selectedFormat: state.typeOption.format,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef SelectNumberFormatCallback = Function(NumberFormatPB format);
|
||||||
|
|
||||||
|
class NumberFormatList extends StatelessWidget {
|
||||||
|
const NumberFormatList({
|
||||||
|
super.key,
|
||||||
|
required this.selectedFormat,
|
||||||
|
required this.onSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SelectNumberFormatCallback onSelected;
|
||||||
|
final NumberFormatPB selectedFormat;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => NumberFormatBloc(),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const _FilterTextField(),
|
||||||
|
const VSpace(16),
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: BlocBuilder<NumberFormatBloc, NumberFormatState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final List<NumberFormatPB> formatList = state.formats;
|
||||||
|
return ListView.builder(
|
||||||
|
itemCount: formatList.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final format = formatList[index];
|
||||||
|
return RadioListTile<NumberFormatPB>(
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
value: format,
|
||||||
|
groupValue: selectedFormat,
|
||||||
|
onChanged: (format) => onSelected(format!),
|
||||||
|
title: Text(format.title()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FilterTextField extends StatelessWidget {
|
||||||
|
const _FilterTextField();
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (text) => context
|
||||||
|
.read<NumberFormatBloc>()
|
||||||
|
.add(NumberFormatEvent.setFilter(text)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RichTextTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
RichTextTypeOptionMobileWidgetBuilder(
|
||||||
|
RichTextTypeOptionContext typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => null;
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SingleSelectTypeOptionMobileWidgetBuilder
|
||||||
|
extends TypeOptionWidgetBuilder {
|
||||||
|
SingleSelectTypeOptionMobileWidgetBuilder(
|
||||||
|
SingleSelectTypeOptionContext typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => const Text('Under Construction');
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
import 'package:appflowy/mobile/presentation/database/card/row/cells/date_cell/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class TimestampTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
final TimestampTypeOptionMobileWidget _widget;
|
||||||
|
|
||||||
|
TimestampTypeOptionMobileWidgetBuilder(
|
||||||
|
TimestampTypeOptionContext typeOptionContext,
|
||||||
|
) : _widget = TimestampTypeOptionMobileWidget(
|
||||||
|
typeOptionContext: typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => _widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimestampTypeOptionMobileWidget extends TypeOptionWidget {
|
||||||
|
const TimestampTypeOptionMobileWidget({
|
||||||
|
super.key,
|
||||||
|
required this.typeOptionContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TimestampTypeOptionContext typeOptionContext;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
TimestampTypeOptionBloc(typeOptionContext: typeOptionContext),
|
||||||
|
child: BlocConsumer<TimestampTypeOptionBloc, TimestampTypeOptionState>(
|
||||||
|
listener: (context, state) =>
|
||||||
|
typeOptionContext.typeOption = state.typeOption,
|
||||||
|
builder: (context, state) {
|
||||||
|
final List<Widget> children = [
|
||||||
|
// used to add a separator padding at the top
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
DateFormatListTile(
|
||||||
|
currentFormatStr: state.typeOption.dateFormat.title(),
|
||||||
|
groupValue: context
|
||||||
|
.watch<TimestampTypeOptionBloc>()
|
||||||
|
.state
|
||||||
|
.typeOption
|
||||||
|
.dateFormat,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
if (newFormat != null) {
|
||||||
|
context.read<TimestampTypeOptionBloc>().add(
|
||||||
|
TimestampTypeOptionEvent.didSelectDateFormat(
|
||||||
|
newFormat,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IncludeTimeSwitch(
|
||||||
|
switchValue: state.typeOption.includeTime,
|
||||||
|
onChanged: (value) => context
|
||||||
|
.read<TimestampTypeOptionBloc>()
|
||||||
|
.add(TimestampTypeOptionEvent.includeTime(value)),
|
||||||
|
),
|
||||||
|
if (state.typeOption.includeTime)
|
||||||
|
TimeFormatListTile(
|
||||||
|
currentFormatStr: state.typeOption.timeFormat.title(),
|
||||||
|
groupValue: context
|
||||||
|
.watch<TimestampTypeOptionBloc>()
|
||||||
|
.state
|
||||||
|
.typeOption
|
||||||
|
.timeFormat,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
if (newFormat != null) {
|
||||||
|
context.read<TimestampTypeOptionBloc>().add(
|
||||||
|
TimestampTypeOptionEvent.didSelectTimeFormat(
|
||||||
|
newFormat,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
separatorBuilder: (context, index) => const VSpace(8),
|
||||||
|
itemCount: children.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) => children[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
export 'checklist.dart';
|
||||||
|
export 'checkbox.dart';
|
||||||
|
export 'date.dart';
|
||||||
|
export 'multi_select.dart';
|
||||||
|
export 'number.dart';
|
||||||
|
export 'rich_text.dart';
|
||||||
|
export 'single_select.dart';
|
||||||
|
export 'timestamp.dart';
|
||||||
|
export 'url.dart';
|
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class URLTypeOptionMobileWidgetBuilder extends TypeOptionWidgetBuilder {
|
||||||
|
URLTypeOptionMobileWidgetBuilder(
|
||||||
|
URLTypeOptionContext typeOptionContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? build(BuildContext context) => null;
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class PropertyTitle extends StatelessWidget {
|
||||||
|
const PropertyTitle(this.name, {super.key});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(
|
||||||
|
name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
export 'mobile_text_cell.dart';
|
||||||
|
export 'mobile_number_cell.dart';
|
||||||
|
export 'mobile_timestamp_cell.dart';
|
||||||
|
export 'mobile_checkbox_cell.dart';
|
||||||
|
export 'mobile_url_cell.dart';
|
||||||
|
export 'date_cell/mobile_date_cell.dart';
|
||||||
|
export 'date_cell/mobile_date_cell_edit_screen.dart';
|
@ -0,0 +1,103 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import 'mobile_date_cell_edit_screen.dart';
|
||||||
|
|
||||||
|
class MobileDateCell extends GridCellWidget {
|
||||||
|
MobileDateCell({
|
||||||
|
super.key,
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
required this.hintText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
final String? hintText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridCellState<MobileDateCell> createState() => _DateCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateCellState extends GridCellState<MobileDateCell> {
|
||||||
|
late final DateCellBloc _cellBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as DateCellController;
|
||||||
|
_cellBloc = DateCellBloc(cellController: cellController)
|
||||||
|
..add(const DateCellEvent.initial());
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// full screen show the date edit screen
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => context.push(
|
||||||
|
MobileDateCellEditScreen.routeName,
|
||||||
|
extra: {
|
||||||
|
MobileDateCellEditScreen.argCellController:
|
||||||
|
widget.cellControllerBuilder.build() as DateCellController,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: MobileDateCellText(
|
||||||
|
dateStr: state.dateStr,
|
||||||
|
placeholder: widget.hintText ?? "",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void requestBeginFocus() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? onCopy() => _cellBloc.state.dateStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MobileDateCellText extends StatelessWidget {
|
||||||
|
const MobileDateCellText({
|
||||||
|
super.key,
|
||||||
|
required this.dateStr,
|
||||||
|
required this.placeholder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String dateStr;
|
||||||
|
final String placeholder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isPlaceholder = dateStr.isEmpty;
|
||||||
|
final text = isPlaceholder ? placeholder : dateStr;
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
color: isPlaceholder
|
||||||
|
? Theme.of(context).hintColor
|
||||||
|
: Theme.of(context).colorScheme.onBackground,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,372 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
|
||||||
|
import 'package:appflowy_backend/log.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
|
import 'package:dartz/dartz.dart' hide State;
|
||||||
|
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 'widgets/widgets.dart';
|
||||||
|
|
||||||
|
class MobileDateCellEditScreen extends StatefulWidget {
|
||||||
|
static const routeName = '/MobileDateCellEditScreen';
|
||||||
|
static const argCellController = 'cellController';
|
||||||
|
|
||||||
|
const MobileDateCellEditScreen(
|
||||||
|
this.cellController, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateCellController cellController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MobileDateCellEditScreen> createState() =>
|
||||||
|
_MobileDateCellEditScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileDateCellEditScreenState extends State<MobileDateCellEditScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(LocaleKeys.button_edit.tr()),
|
||||||
|
),
|
||||||
|
body: FutureBuilder<Either<dynamic, FlowyError>>(
|
||||||
|
future: widget.cellController.getTypeOption(
|
||||||
|
DateTypeOptionDataParser(),
|
||||||
|
),
|
||||||
|
builder: (BuildContext context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return snapshot.data!.fold(
|
||||||
|
(dateTypeOptionPB) {
|
||||||
|
return _DateCellEditBody(
|
||||||
|
dateCellController: widget.cellController,
|
||||||
|
dateTypeOptionPB: dateTypeOptionPB,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(err) {
|
||||||
|
Log.error(err);
|
||||||
|
return FlowyMobileStateContainer.error(
|
||||||
|
title: LocaleKeys.grid_field_failedToLoadDate.tr(),
|
||||||
|
errorMsg: err.toString(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Center(child: CircularProgressIndicator.adaptive());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateCellEditBody extends StatefulWidget {
|
||||||
|
const _DateCellEditBody({
|
||||||
|
required this.dateCellController,
|
||||||
|
required this.dateTypeOptionPB,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateCellController dateCellController;
|
||||||
|
final DateTypeOptionPB dateTypeOptionPB;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DateCellEditBody> createState() => _DateCellEditBodyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateCellEditBodyState extends State<_DateCellEditBody> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => DateCellCalendarBloc(
|
||||||
|
dateTypeOptionPB: widget.dateTypeOptionPB,
|
||||||
|
cellData: widget.dateCellController.getCellData(),
|
||||||
|
cellController: widget.dateCellController,
|
||||||
|
)..add(const DateCellCalendarEvent.initial()),
|
||||||
|
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final widgetsList = [
|
||||||
|
DateAndTimeDisplay(state),
|
||||||
|
const DatePicker(),
|
||||||
|
const _EndDateSwitch(),
|
||||||
|
const _IncludeTimeSwitch(),
|
||||||
|
const _StartDayTime(),
|
||||||
|
const _EndDayTime(),
|
||||||
|
const Divider(),
|
||||||
|
const _DateFormatOption(),
|
||||||
|
const _TimeFormatOption(),
|
||||||
|
const Divider(),
|
||||||
|
const _ClearDateButton(),
|
||||||
|
];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: ListView.separated(
|
||||||
|
itemBuilder: (context, index) => widgetsList[index],
|
||||||
|
separatorBuilder: (_, __) => const VSpace(8),
|
||||||
|
itemCount: widgetsList.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EndDateSwitch extends StatelessWidget {
|
||||||
|
const _EndDateSwitch();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
|
||||||
|
selector: (state) => state.isRange,
|
||||||
|
builder: (context, isRange) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
LocaleKeys.grid_field_isRange.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: isRange,
|
||||||
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
onChanged: (value) {
|
||||||
|
context
|
||||||
|
.read<DateCellCalendarBloc>()
|
||||||
|
.add(DateCellCalendarEvent.setIsRange(value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IncludeTimeSwitch extends StatelessWidget {
|
||||||
|
const _IncludeTimeSwitch();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
|
||||||
|
selector: (state) => state.includeTime,
|
||||||
|
builder: (context, includeTime) {
|
||||||
|
return IncludeTimeSwitch(
|
||||||
|
switchValue: includeTime,
|
||||||
|
onChanged: (value) {
|
||||||
|
context
|
||||||
|
.read<DateCellCalendarBloc>()
|
||||||
|
.add(DateCellCalendarEvent.setIncludeTime(value));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StartDayTime extends StatelessWidget {
|
||||||
|
const _StartDayTime();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: state.includeTime
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
state.isRange
|
||||||
|
? LocaleKeys.grid_field_startDateTime.tr()
|
||||||
|
: LocaleKeys.grid_field_dateTime.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// TODO(yijing): improve width
|
||||||
|
SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: _TimeTextField(
|
||||||
|
timeStr: state.timeStr,
|
||||||
|
isEndTime: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EndDayTime extends StatelessWidget {
|
||||||
|
const _EndDayTime();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: state.includeTime && state.endTimeStr != null
|
||||||
|
? Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
LocaleKeys.grid_field_endDateTime.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// TODO(yijing): improve width
|
||||||
|
SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: _TimeTextField(
|
||||||
|
timeStr: state.timeStr,
|
||||||
|
isEndTime: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeTextField extends StatefulWidget {
|
||||||
|
const _TimeTextField({
|
||||||
|
required this.timeStr,
|
||||||
|
required this.isEndTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? timeStr;
|
||||||
|
final bool isEndTime;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TimeTextField> createState() => _TimeTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeTextFieldState extends State<_TimeTextField> {
|
||||||
|
late final TextEditingController _textController =
|
||||||
|
TextEditingController(text: widget.timeStr);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
_textController.text =
|
||||||
|
widget.isEndTime ? state.endTimeStr ?? "" : state.timeStr ?? "";
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
return TextFormField(
|
||||||
|
controller: _textController,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: state.timeHintText,
|
||||||
|
errorText: widget.isEndTime
|
||||||
|
? state.parseEndTimeError
|
||||||
|
: state.parseTimeError,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.datetime,
|
||||||
|
onFieldSubmitted: (timeStr) {
|
||||||
|
context.read<DateCellCalendarBloc>().add(
|
||||||
|
widget.isEndTime
|
||||||
|
? DateCellCalendarEvent.setEndTime(timeStr)
|
||||||
|
: DateCellCalendarEvent.setTime(timeStr),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_textController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ClearDateButton extends StatelessWidget {
|
||||||
|
const _ClearDateButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
LocaleKeys.grid_field_clearDate.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => context
|
||||||
|
.read<DateCellCalendarBloc>()
|
||||||
|
.add(const DateCellCalendarEvent.clearDate()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeFormatOption extends StatelessWidget {
|
||||||
|
const _TimeFormatOption();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState,
|
||||||
|
TimeFormatPB>(
|
||||||
|
selector: (state) => state.dateTypeOptionPB.timeFormat,
|
||||||
|
builder: (context, state) {
|
||||||
|
return TimeFormatListTile(
|
||||||
|
currentFormatStr: state.title(),
|
||||||
|
groupValue: context
|
||||||
|
.watch<DateCellCalendarBloc>()
|
||||||
|
.state
|
||||||
|
.dateTypeOptionPB
|
||||||
|
.timeFormat,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
if (newFormat == null) return;
|
||||||
|
context
|
||||||
|
.read<DateCellCalendarBloc>()
|
||||||
|
.add(DateCellCalendarEvent.setTimeFormat(newFormat));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateFormatOption extends StatelessWidget {
|
||||||
|
const _DateFormatOption();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState,
|
||||||
|
DateFormatPB>(
|
||||||
|
selector: (state) => state.dateTypeOptionPB.dateFormat,
|
||||||
|
builder: (context, state) {
|
||||||
|
return DateFormatListTile(
|
||||||
|
currentFormatStr: state.title(),
|
||||||
|
groupValue: context
|
||||||
|
.watch<DateCellCalendarBloc>()
|
||||||
|
.state
|
||||||
|
.dateTypeOptionPB
|
||||||
|
.dateFormat,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
if (newFormat == null) return;
|
||||||
|
context
|
||||||
|
.read<DateCellCalendarBloc>()
|
||||||
|
.add(DateCellCalendarEvent.setDateFormat(newFormat));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class DateAndTimeDisplay extends StatelessWidget {
|
||||||
|
const DateAndTimeDisplay(this.state, {super.key});
|
||||||
|
final DateCellCalendarState state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// date/start date and time
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_DateEditButton(
|
||||||
|
dateStr: state.isRange
|
||||||
|
? _formatDateByDateFormatPB(
|
||||||
|
state.startDay,
|
||||||
|
state.dateTypeOptionPB.dateFormat,
|
||||||
|
)
|
||||||
|
: state.dateStr,
|
||||||
|
initialDate: state.isRange ? state.startDay : state.dateTime,
|
||||||
|
onDaySelected: (newDate) {
|
||||||
|
context.read<DateCellCalendarBloc>().add(
|
||||||
|
state.isRange
|
||||||
|
? DateCellCalendarEvent.setStartDay(newDate)
|
||||||
|
: DateCellCalendarEvent.selectDay(newDate),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
if (state.includeTime)
|
||||||
|
Expanded(child: _TimeEditButton(state.timeStr)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const VSpace(8),
|
||||||
|
// end date and time
|
||||||
|
if (state.isRange) ...[
|
||||||
|
_DateEditButton(
|
||||||
|
dateStr: state.endDay != null ? state.endDateStr : null,
|
||||||
|
initialDate: state.endDay,
|
||||||
|
onDaySelected: (newDate) {
|
||||||
|
context.read<DateCellCalendarBloc>().add(
|
||||||
|
DateCellCalendarEvent.setEndDay(newDate),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
if (state.includeTime)
|
||||||
|
Expanded(child: _TimeEditButton(state.endTimeStr)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateEditButton extends StatelessWidget {
|
||||||
|
const _DateEditButton({
|
||||||
|
required this.dateStr,
|
||||||
|
required this.initialDate,
|
||||||
|
required this.onDaySelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? dateStr;
|
||||||
|
|
||||||
|
/// initial date for date picker, if null, use DateTime.now()
|
||||||
|
final DateTime? initialDate;
|
||||||
|
final void Function(DateTime)? onDaySelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
// share space with TimeEditButton
|
||||||
|
width: (size.width - 8) / 2,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final DateTime? newDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(1900),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
);
|
||||||
|
if (newDate != null) {
|
||||||
|
onDaySelected?.call(newDate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
dateStr ?? LocaleKeys.grid_field_selectDate.tr(),
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeEditButton extends StatelessWidget {
|
||||||
|
const _TimeEditButton(
|
||||||
|
this.timeStr,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String? timeStr;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return OutlinedButton(
|
||||||
|
// TODO(yijing): implement time picker
|
||||||
|
onPressed: null,
|
||||||
|
child: Text(
|
||||||
|
timeStr ?? LocaleKeys.grid_field_selectTime.tr(),
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _formatDateByDateFormatPB(DateTime? date, DateFormatPB dateFormatPB) {
|
||||||
|
if (date == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
switch (dateFormatPB) {
|
||||||
|
case DateFormatPB.Local:
|
||||||
|
return DateFormat('MM/dd/yyyy').format(date);
|
||||||
|
case DateFormatPB.US:
|
||||||
|
return DateFormat('yyyy/MM/dd').format(date);
|
||||||
|
case DateFormatPB.ISO:
|
||||||
|
return DateFormat('yyyy-MM-dd').format(date);
|
||||||
|
case DateFormatPB.Friendly:
|
||||||
|
return DateFormat('MMM dd, yyyy').format(date);
|
||||||
|
case DateFormatPB.DayMonthYear:
|
||||||
|
return DateFormat('dd/MM/yyyy').format(date);
|
||||||
|
default:
|
||||||
|
return 'Unavailable date format';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class DateFormatListTile extends StatelessWidget {
|
||||||
|
const DateFormatListTile({
|
||||||
|
super.key,
|
||||||
|
required this.currentFormatStr,
|
||||||
|
this.groupValue,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String currentFormatStr;
|
||||||
|
|
||||||
|
/// The group value for the radio list tile.
|
||||||
|
final DateFormatPB? groupValue;
|
||||||
|
|
||||||
|
/// The Function for the radio list tile.
|
||||||
|
final void Function(DateFormatPB?)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final style = Theme.of(context);
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
LocaleKeys.grid_field_dateFormat.tr(),
|
||||||
|
style: style.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
currentFormatStr,
|
||||||
|
style: style.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios_sharp,
|
||||||
|
color: style.hintColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => showFlowyMobileBottomSheet(
|
||||||
|
context,
|
||||||
|
title: LocaleKeys.grid_field_dateFormat.tr(),
|
||||||
|
builder: (_) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_DateFormatRadioListTile(
|
||||||
|
title: LocaleKeys.grid_field_dateFormatLocal.tr(),
|
||||||
|
dateFormatPB: DateFormatPB.Local,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
onChanged?.call(newFormat);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_DateFormatRadioListTile(
|
||||||
|
title: LocaleKeys.grid_field_dateFormatUS.tr(),
|
||||||
|
dateFormatPB: DateFormatPB.US,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
onChanged?.call(newFormat);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_DateFormatRadioListTile(
|
||||||
|
title: LocaleKeys.grid_field_dateFormatISO.tr(),
|
||||||
|
dateFormatPB: DateFormatPB.ISO,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
onChanged?.call(newFormat);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_DateFormatRadioListTile(
|
||||||
|
title: LocaleKeys.grid_field_dateFormatFriendly.tr(),
|
||||||
|
dateFormatPB: DateFormatPB.Friendly,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
onChanged?.call(newFormat);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_DateFormatRadioListTile(
|
||||||
|
title: LocaleKeys.grid_field_dateFormatDayMonthYear.tr(),
|
||||||
|
dateFormatPB: DateFormatPB.DayMonthYear,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
onChanged?.call(newFormat);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DateFormatRadioListTile extends StatelessWidget {
|
||||||
|
const _DateFormatRadioListTile({
|
||||||
|
required this.title,
|
||||||
|
required this.dateFormatPB,
|
||||||
|
required this.groupValue,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final DateFormatPB dateFormatPB;
|
||||||
|
final DateFormatPB? groupValue;
|
||||||
|
final void Function(DateFormatPB?)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final style = Theme.of(context);
|
||||||
|
return RadioListTile<DateFormatPB>(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: style.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: style.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
groupValue: groupValue,
|
||||||
|
value: dateFormatPB,
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class IncludeTimeSwitch extends StatelessWidget {
|
||||||
|
const IncludeTimeSwitch({
|
||||||
|
super.key,
|
||||||
|
required this.switchValue,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool switchValue;
|
||||||
|
final void Function(bool)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
LocaleKeys.grid_field_includeTime.tr(),
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: switchValue,
|
||||||
|
activeColor: Theme.of(context).colorScheme.primary,
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
class TimeFormatListTile extends StatelessWidget {
|
||||||
|
const TimeFormatListTile({
|
||||||
|
super.key,
|
||||||
|
required this.currentFormatStr,
|
||||||
|
required this.groupValue,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String currentFormatStr;
|
||||||
|
|
||||||
|
/// The group value for the radio list tile.
|
||||||
|
final TimeFormatPB? groupValue;
|
||||||
|
|
||||||
|
/// The Function for the radio list tile.
|
||||||
|
final void Function(TimeFormatPB?)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final style = Theme.of(context);
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
LocaleKeys.grid_field_timeFormat.tr(),
|
||||||
|
style: style.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
currentFormatStr,
|
||||||
|
style: style.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
|
Icon(
|
||||||
|
Icons.arrow_forward_ios_sharp,
|
||||||
|
color: style.hintColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () => showFlowyMobileBottomSheet(
|
||||||
|
context,
|
||||||
|
title: LocaleKeys.grid_field_timeFormat.tr(),
|
||||||
|
builder: (context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_TimeFormatRadioListTile(
|
||||||
|
title: LocaleKeys.grid_field_timeFormatTwelveHour.tr(),
|
||||||
|
timeFormatPB: TimeFormatPB.TwelveHour,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
onChanged?.call(newFormat);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_TimeFormatRadioListTile(
|
||||||
|
title: LocaleKeys.grid_field_timeFormatTwentyFourHour.tr(),
|
||||||
|
timeFormatPB: TimeFormatPB.TwentyFourHour,
|
||||||
|
groupValue: groupValue,
|
||||||
|
onChanged: (newFormat) {
|
||||||
|
onChanged?.call(newFormat);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimeFormatRadioListTile extends StatelessWidget {
|
||||||
|
const _TimeFormatRadioListTile({
|
||||||
|
required this.title,
|
||||||
|
required this.timeFormatPB,
|
||||||
|
required this.groupValue,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final TimeFormatPB timeFormatPB;
|
||||||
|
final TimeFormatPB? groupValue;
|
||||||
|
final void Function(TimeFormatPB?)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final style = Theme.of(context);
|
||||||
|
return RadioListTile<TimeFormatPB>(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: const EdgeInsets.fromLTRB(0, 0, 4, 0),
|
||||||
|
controlAffinity: ListTileControlAffinity.trailing,
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: style.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: style.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
groupValue: groupValue,
|
||||||
|
value: timeFormatPB,
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export 'date_and_time_display.dart';
|
||||||
|
export 'date_format_list_tile.dart';
|
||||||
|
export 'time_format_list_tile.dart';
|
||||||
|
export 'include_time_switch.dart';
|
@ -0,0 +1,66 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class MobileCheckboxCell extends GridCellWidget {
|
||||||
|
MobileCheckboxCell({
|
||||||
|
super.key,
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
GridCellStyle? style,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridCellState<MobileCheckboxCell> createState() => _CheckboxCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CheckboxCellState extends GridCellState<MobileCheckboxCell> {
|
||||||
|
late final CheckboxCellBloc _cellBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as CheckboxCellController;
|
||||||
|
_cellBloc = CheckboxCellBloc(cellController: cellController)
|
||||||
|
..add(const CheckboxCellEvent.initial());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
// TODO(yijing): improve icon here
|
||||||
|
child: FlowySvg(
|
||||||
|
state.isSelected ? FlowySvgs.checkbox_s : FlowySvgs.uncheck_s,
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
|
size: const Size.square(24),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void requestBeginFocus() {
|
||||||
|
_cellBloc.add(const CheckboxCellEvent.select());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? onCopy() => _cellBloc.state.isSelected ? "Yes" : "No";
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class MobileNumberCell extends GridCellWidget {
|
||||||
|
MobileNumberCell({
|
||||||
|
super.key,
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
this.hintText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
final String? hintText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridEditableTextCell<MobileNumberCell> createState() => _NumberCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NumberCellState extends GridEditableTextCell<MobileNumberCell> {
|
||||||
|
late final NumberCellBloc _cellBloc;
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as NumberCellController;
|
||||||
|
_cellBloc = NumberCellBloc(cellController: cellController)
|
||||||
|
..add(const NumberCellEvent.initial());
|
||||||
|
_controller = TextEditingController(text: _cellBloc.state.cellContent);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: MultiBlocListener(
|
||||||
|
listeners: [
|
||||||
|
BlocListener<NumberCellBloc, NumberCellState>(
|
||||||
|
listenWhen: (p, c) => p.cellContent != c.cellContent,
|
||||||
|
listener: (context, state) => _controller.text = state.cellContent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
hintText: widget.hintText,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
isCollapsed: true,
|
||||||
|
),
|
||||||
|
// close keyboard when tapping outside of the text field
|
||||||
|
onTapOutside: (event) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> focusChanged() async {
|
||||||
|
if (mounted &&
|
||||||
|
_cellBloc.isClosed == false &&
|
||||||
|
_controller.text != _cellBloc.state.cellContent) {
|
||||||
|
_cellBloc.add(NumberCellEvent.updateCell(_controller.text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? onCopy() => _cellBloc.state.cellContent;
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class MobileTextCell extends GridCellWidget {
|
||||||
|
MobileTextCell({
|
||||||
|
super.key,
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
this.hintText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
final String? hintText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridEditableTextCell<MobileTextCell> createState() => _MobileTextCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MobileTextCellState extends GridEditableTextCell<MobileTextCell> {
|
||||||
|
late final TextCellBloc _cellBloc;
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as TextCellController;
|
||||||
|
_cellBloc = TextCellBloc(cellController: cellController)
|
||||||
|
..add(const TextCellEvent.initial());
|
||||||
|
_controller = TextEditingController(text: _cellBloc.state.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: BlocListener<TextCellBloc, TextCellState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (_controller.text != state.content) {
|
||||||
|
_controller.text = state.content;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
// TODO(yijing): update text style
|
||||||
|
decoration: InputDecoration(
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
hintText: widget.hintText,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
isCollapsed: true,
|
||||||
|
),
|
||||||
|
// close keyboard when tapping outside of the text field
|
||||||
|
onTapOutside: (event) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? onCopy() => _cellBloc.state.content;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> focusChanged() {
|
||||||
|
_cellBloc.add(
|
||||||
|
TextCellEvent.updateText(_controller.text),
|
||||||
|
);
|
||||||
|
return super.focusChanged();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class MobileTimestampCell extends GridCellWidget {
|
||||||
|
MobileTimestampCell({
|
||||||
|
super.key,
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridCellState<MobileTimestampCell> createState() => _TimestampCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimestampCellState extends GridCellState<MobileTimestampCell> {
|
||||||
|
late final TimestampCellBloc _cellBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as TimestampCellController;
|
||||||
|
_cellBloc = TimestampCellBloc(cellController: cellController)
|
||||||
|
..add(const TimestampCellEvent.initial());
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(state.dateStr),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? onCopy() => _cellBloc.state.dateStr;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void requestBeginFocus() {}
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class MobileURLCell extends GridCellWidget {
|
||||||
|
MobileURLCell({
|
||||||
|
super.key,
|
||||||
|
required this.cellControllerBuilder,
|
||||||
|
this.hintText,
|
||||||
|
});
|
||||||
|
|
||||||
|
final CellControllerBuilder cellControllerBuilder;
|
||||||
|
final String? hintText;
|
||||||
|
|
||||||
|
@override
|
||||||
|
GridCellState<MobileURLCell> createState() => _GridURLCellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GridURLCellState extends GridCellState<MobileURLCell> {
|
||||||
|
late final URLCellBloc _cellBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final cellController =
|
||||||
|
widget.cellControllerBuilder.build() as URLCellController;
|
||||||
|
_cellBloc = URLCellBloc(cellController: cellController)
|
||||||
|
..add(const URLCellEvent.initial());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_cellBloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _cellBloc,
|
||||||
|
child: BlocSelector<URLCellBloc, URLCellState, String>(
|
||||||
|
selector: (state) => state.content,
|
||||||
|
builder: (context, content) {
|
||||||
|
if (content.isEmpty) {
|
||||||
|
return TextField(
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
hintText: widget.hintText,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
isCollapsed: true,
|
||||||
|
),
|
||||||
|
// close keyboard when tapping outside of the text field
|
||||||
|
onTapOutside: (event) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
_cellBloc.add(
|
||||||
|
URLCellEvent.updateURL(value),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (content.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final shouldAddScheme = !['http', 'https']
|
||||||
|
.any((pattern) => content.startsWith(pattern));
|
||||||
|
final url = shouldAddScheme ? 'http://$content' : content;
|
||||||
|
canLaunchUrlString(url).then((value) => launchUrlString(url));
|
||||||
|
},
|
||||||
|
onLongPress: () => showFlowyMobileBottomSheet(
|
||||||
|
context,
|
||||||
|
title: LocaleKeys.board_mobile_editURL.tr(),
|
||||||
|
builder: (_) {
|
||||||
|
final controller = TextEditingController(text: content);
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
autofocus: true,
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
onEditingComplete: () {
|
||||||
|
_cellBloc.add(
|
||||||
|
URLCellEvent.updateURL(controller.text),
|
||||||
|
);
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
content,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void requestBeginFocus() {}
|
||||||
|
}
|
@ -39,6 +39,7 @@ class FlowyMobileStateContainer extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 32),
|
||||||
@ -47,7 +48,10 @@ class FlowyMobileStateContainer extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
emoji ?? '',
|
emoji ??
|
||||||
|
(_stateType == _FlowyMobileStateContainerType.error
|
||||||
|
? '🛸'
|
||||||
|
: ''),
|
||||||
style: const TextStyle(fontSize: 40),
|
style: const TextStyle(fontSize: 40),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
@ -5,10 +5,11 @@ Future<T?> showFlowyMobileBottomSheet<T>(
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String title,
|
required String title,
|
||||||
required Widget Function(BuildContext) builder,
|
required Widget Function(BuildContext) builder,
|
||||||
|
bool isScrollControlled = false,
|
||||||
}) async {
|
}) async {
|
||||||
return showModalBottomSheet(
|
return showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: isScrollControlled,
|
||||||
builder: (context) => Padding(
|
builder: (context) => Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/database_controller.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/field/field_controller.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||||
@ -10,6 +10,7 @@ import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_
|
|||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||||
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
|
import 'package:appflowy/plugins/database_view/tab_bar/tab_bar_view.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
|
||||||
|
import 'package:appflowy/util/platform_extension.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||||
import 'package:appflowy_board/appflowy_board.dart';
|
import 'package:appflowy_board/appflowy_board.dart';
|
||||||
@ -21,6 +22,7 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
|||||||
import 'package:flutter/material.dart' hide Card;
|
import 'package:flutter/material.dart' hide Card;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../widgets/card/cells/card_cell.dart';
|
import '../../widgets/card/cells/card_cell.dart';
|
||||||
import '../../widgets/card/card_cell_builder.dart';
|
import '../../widgets/card/card_cell_builder.dart';
|
||||||
@ -319,6 +321,15 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// navigate to card detail screen when it is in mobile
|
||||||
|
if (PlatformExtension.isMobile) {
|
||||||
|
context.push(
|
||||||
|
MobileCardDetailScreen.routeName,
|
||||||
|
extra: {
|
||||||
|
'rowController': dataController,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
FlowyOverlay.show(
|
FlowyOverlay.show(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => RowDetailPage(
|
builder: (_) => RowDetailPage(
|
||||||
@ -327,6 +338,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BoardTrailing extends StatefulWidget {
|
class BoardTrailing extends StatefulWidget {
|
||||||
|
@ -123,5 +123,5 @@ class SwitchFieldButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class TypeOptionWidget extends StatelessWidget {
|
abstract class TypeOptionWidget extends StatelessWidget {
|
||||||
const TypeOptionWidget({Key? key}) : super(key: key);
|
const TypeOptionWidget({super.key});
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database_view/application/row/row_controller.da
|
|||||||
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/row/row_property.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
@ -273,7 +274,8 @@ class RowContent extends StatelessWidget {
|
|||||||
) {
|
) {
|
||||||
return cellByFieldId.values.map(
|
return cellByFieldId.values.map(
|
||||||
(cellId) {
|
(cellId) {
|
||||||
final GridCellWidget child = builder.build(cellId);
|
final cellStyle = customCellStyle(cellId.fieldType);
|
||||||
|
final GridCellWidget child = builder.build(cellId, style: cellStyle);
|
||||||
|
|
||||||
return CellContainer(
|
return CellContainer(
|
||||||
width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140,
|
width: cellId.fieldInfo.fieldSettings?.width.toDouble() ?? 140,
|
||||||
|
@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
|
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
|
||||||
|
import 'package:appflowy/util/platform_extension.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
|
|
||||||
@ -122,6 +123,27 @@ class _RowCardState<T> extends State<RowCard<T>> {
|
|||||||
return !listEquals(previous.cells, current.cells);
|
return !listEquals(previous.cells, current.cells);
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
// mobile
|
||||||
|
if (PlatformExtension.isMobile) {
|
||||||
|
// TODO(yijing): refactor it in mobile to display card in database view
|
||||||
|
return RowCardContainer(
|
||||||
|
buildAccessoryWhen: () => state.isEditing == false,
|
||||||
|
accessoryBuilder: (context) {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
openAccessory: (p0) {},
|
||||||
|
openCard: (context) => widget.openCard(context),
|
||||||
|
child: _CardContent<T>(
|
||||||
|
rowNotifier: rowNotifier,
|
||||||
|
cellBuilder: widget.cellBuilder,
|
||||||
|
styleConfiguration: widget.styleConfiguration,
|
||||||
|
cells: state.cells,
|
||||||
|
renderHook: widget.renderHook,
|
||||||
|
cardData: widget.cardData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// desktop
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||||
|
import 'package:appflowy/util/platform_extension.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -23,7 +25,7 @@ class GridCellBuilder {
|
|||||||
|
|
||||||
GridCellWidget build(
|
GridCellWidget build(
|
||||||
DatabaseCellContext cellContext, {
|
DatabaseCellContext cellContext, {
|
||||||
GridCellStyle? style,
|
required GridCellStyle? style,
|
||||||
}) {
|
}) {
|
||||||
final cellControllerBuilder = CellControllerBuilder(
|
final cellControllerBuilder = CellControllerBuilder(
|
||||||
cellContext: cellContext,
|
cellContext: cellContext,
|
||||||
@ -31,6 +33,30 @@ class GridCellBuilder {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final key = cellContext.key();
|
final key = cellContext.key();
|
||||||
|
|
||||||
|
if (PlatformExtension.isMobile) {
|
||||||
|
return _getMobileCardCellWidget(
|
||||||
|
key,
|
||||||
|
cellContext,
|
||||||
|
cellControllerBuilder,
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _getDesktopGridCellWidget(
|
||||||
|
key,
|
||||||
|
cellContext,
|
||||||
|
cellControllerBuilder,
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GridCellWidget _getDesktopGridCellWidget(
|
||||||
|
ValueKey key,
|
||||||
|
DatabaseCellContext cellContext,
|
||||||
|
CellControllerBuilder cellControllerBuilder,
|
||||||
|
GridCellStyle? style,
|
||||||
|
) {
|
||||||
switch (cellContext.fieldType) {
|
switch (cellContext.fieldType) {
|
||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
return GridCheckboxCell(
|
return GridCheckboxCell(
|
||||||
@ -94,6 +120,74 @@ class GridCellBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editable cell/(card's propery value) widget
|
||||||
|
GridCellWidget _getMobileCardCellWidget(
|
||||||
|
ValueKey key,
|
||||||
|
DatabaseCellContext cellContext,
|
||||||
|
CellControllerBuilder cellControllerBuilder,
|
||||||
|
GridCellStyle? style,
|
||||||
|
) {
|
||||||
|
switch (cellContext.fieldType) {
|
||||||
|
case FieldType.RichText:
|
||||||
|
style as GridTextCellStyle;
|
||||||
|
return MobileTextCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
hintText: style.placeholder,
|
||||||
|
);
|
||||||
|
case FieldType.Number:
|
||||||
|
style as GridNumberCellStyle;
|
||||||
|
return MobileNumberCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
hintText: style.placeholder,
|
||||||
|
);
|
||||||
|
case FieldType.LastEditedTime:
|
||||||
|
case FieldType.CreatedTime:
|
||||||
|
return MobileTimestampCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
case FieldType.Checkbox:
|
||||||
|
return MobileCheckboxCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
case FieldType.DateTime:
|
||||||
|
style as DateCellStyle;
|
||||||
|
return MobileDateCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
hintText: style.placeholder,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
case FieldType.URL:
|
||||||
|
style as GridURLCellStyle;
|
||||||
|
return MobileURLCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
hintText: style.placeholder,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
// TODO(yijing): implement the following mobile select option cell
|
||||||
|
case FieldType.SingleSelect:
|
||||||
|
return GridSingleSelectCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
style: style,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
case FieldType.MultiSelect:
|
||||||
|
return GridMultiSelectCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
style: style,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
case FieldType.Checklist:
|
||||||
|
return GridChecklistCell(
|
||||||
|
cellControllerBuilder: cellControllerBuilder,
|
||||||
|
style: style,
|
||||||
|
key: key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw UnimplementedError;
|
||||||
|
}
|
||||||
|
|
||||||
class BlankCell extends StatelessWidget {
|
class BlankCell extends StatelessWidget {
|
||||||
const BlankCell({Key? key}) : super(key: key);
|
const BlankCell({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@ -38,20 +38,23 @@ class DateCellCalendarBloc
|
|||||||
await event.when(
|
await event.when(
|
||||||
initial: () async => _startListening(),
|
initial: () async => _startListening(),
|
||||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||||
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
|
final dateCellData = _dateDataFromCellData(cellData);
|
||||||
_dateDataFromCellData(cellData);
|
|
||||||
final endDay =
|
final endDay =
|
||||||
isRange == state.isRange && isRange ? endDateTime : null;
|
dateCellData.isRange == state.isRange && dateCellData.isRange
|
||||||
|
? dateCellData.endDateTime
|
||||||
|
: null;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
dateTime: dateTime,
|
dateTime: dateCellData.dateTime,
|
||||||
time: time,
|
timeStr: dateCellData.timeStr,
|
||||||
endDateTime: endDateTime,
|
endDateTime: dateCellData.endDateTime,
|
||||||
endTime: endTime,
|
endTimeStr: dateCellData.endTimeStr,
|
||||||
includeTime: includeTime,
|
includeTime: dateCellData.includeTime,
|
||||||
isRange: isRange,
|
isRange: dateCellData.isRange,
|
||||||
startDay: isRange ? dateTime : null,
|
startDay: dateCellData.isRange ? dateCellData.dateTime : null,
|
||||||
endDay: endDay,
|
endDay: endDay,
|
||||||
|
dateStr: dateCellData.dateStr,
|
||||||
|
endDateStr: dateCellData.endDateStr,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -76,21 +79,31 @@ class DateCellCalendarBloc
|
|||||||
setIsRange: (isRange) async {
|
setIsRange: (isRange) async {
|
||||||
await _updateDateData(isRange: isRange);
|
await _updateDateData(isRange: isRange);
|
||||||
},
|
},
|
||||||
setTime: (time) async {
|
setTime: (timeStr) async {
|
||||||
await _updateDateData(time: time);
|
await _updateDateData(timeStr: timeStr);
|
||||||
},
|
},
|
||||||
selectDateRange: (DateTime? start, DateTime? end) async {
|
selectDateRange: (DateTime? start, DateTime? end) async {
|
||||||
if (end == null && state.startDay != null && state.endDay == null) {
|
if (end == null && state.startDay != null && state.endDay == null) {
|
||||||
final (newStart, newEnd) = state.startDay!.isBefore(start!)
|
final (newStart, newEnd) = state.startDay!.isBefore(start!)
|
||||||
? (state.startDay!, start)
|
? (state.startDay!, start)
|
||||||
: (start, state.startDay!);
|
: (start, state.startDay!);
|
||||||
emit(state.copyWith(startDay: null, endDay: null));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
startDay: null,
|
||||||
|
endDay: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
await _updateDateData(
|
await _updateDateData(
|
||||||
date: newStart.date,
|
date: newStart.date,
|
||||||
endDate: newEnd.date,
|
endDate: newEnd.date,
|
||||||
);
|
);
|
||||||
} else if (end == null) {
|
} else if (end == null) {
|
||||||
emit(state.copyWith(startDay: start, endDay: null));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
startDay: start,
|
||||||
|
endDay: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await _updateDateData(
|
await _updateDateData(
|
||||||
date: start!.date,
|
date: start!.date,
|
||||||
@ -98,8 +111,54 @@ class DateCellCalendarBloc
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setStartDay: (DateTime startDay) async {
|
||||||
|
if (state.endDay == null) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
startDay: startDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (startDay.isAfter(state.endDay!)) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
startDay: startDay,
|
||||||
|
endDay: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
startDay: startDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_updateDateData(date: startDay.date, endDate: state.endDay!.date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setEndDay: (DateTime endDay) async {
|
||||||
|
if (state.startDay == null) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
endDay: endDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (endDay.isBefore(state.startDay!)) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
startDay: null,
|
||||||
|
endDay: endDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
endDay: endDay,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_updateDateData(date: state.startDay!.date, endDate: endDay.date);
|
||||||
|
}
|
||||||
|
},
|
||||||
setEndTime: (String endTime) async {
|
setEndTime: (String endTime) async {
|
||||||
await _updateDateData(endTime: endTime);
|
await _updateDateData(endTimeStr: endTime);
|
||||||
},
|
},
|
||||||
setDateFormat: (dateFormat) async {
|
setDateFormat: (dateFormat) async {
|
||||||
await _updateTypeOption(emit, dateFormat: dateFormat);
|
await _updateTypeOption(emit, dateFormat: dateFormat);
|
||||||
@ -117,30 +176,31 @@ class DateCellCalendarBloc
|
|||||||
|
|
||||||
Future<void> _updateDateData({
|
Future<void> _updateDateData({
|
||||||
DateTime? date,
|
DateTime? date,
|
||||||
String? time,
|
String? timeStr,
|
||||||
DateTime? endDate,
|
DateTime? endDate,
|
||||||
String? endTime,
|
String? endTimeStr,
|
||||||
bool? includeTime,
|
bool? includeTime,
|
||||||
bool? isRange,
|
bool? isRange,
|
||||||
}) async {
|
}) async {
|
||||||
// make sure that not both date and time are updated at the same time
|
// make sure that not both date and time are updated at the same time
|
||||||
assert(
|
assert(
|
||||||
!(date != null && time != null) || !(endDate != null && endTime != null),
|
!(date != null && timeStr != null) ||
|
||||||
|
!(endDate != null && endTimeStr != null),
|
||||||
);
|
);
|
||||||
|
|
||||||
// if not updating the time, use the old time in the state
|
// if not updating the time, use the old time in the state
|
||||||
final String? newTime = time ?? state.time;
|
final String? newTime = timeStr ?? state.timeStr;
|
||||||
DateTime? newDate;
|
DateTime? newDate;
|
||||||
if (time != null && time.isNotEmpty) {
|
if (timeStr != null && timeStr.isNotEmpty) {
|
||||||
newDate = state.dateTime ?? DateTime.now();
|
newDate = state.dateTime ?? DateTime.now();
|
||||||
} else {
|
} else {
|
||||||
newDate = _utcToLocalAndAddCurrentTime(date);
|
newDate = _utcToLocalAndAddCurrentTime(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if not updating the time, use the old time in the state
|
// if not updating the time, use the old time in the state
|
||||||
final String? newEndTime = endTime ?? state.endTime;
|
final String? newEndTime = endTimeStr ?? state.endTimeStr;
|
||||||
DateTime? newEndDate;
|
DateTime? newEndDate;
|
||||||
if (endTime != null && endTime.isNotEmpty) {
|
if (endTimeStr != null && endTimeStr.isNotEmpty) {
|
||||||
newEndDate = state.endDateTime ?? DateTime.now();
|
newEndDate = state.endDateTime ?? DateTime.now();
|
||||||
} else {
|
} else {
|
||||||
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
|
newEndDate = _utcToLocalAndAddCurrentTime(endDate);
|
||||||
@ -306,6 +366,12 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
|
|||||||
DateTime? start,
|
DateTime? start,
|
||||||
DateTime? end,
|
DateTime? end,
|
||||||
) = _SelectDateRange;
|
) = _SelectDateRange;
|
||||||
|
const factory DateCellCalendarEvent.setStartDay(
|
||||||
|
DateTime startDay,
|
||||||
|
) = _SetStartDay;
|
||||||
|
const factory DateCellCalendarEvent.setEndDay(
|
||||||
|
DateTime endDay,
|
||||||
|
) = _SetEndDay;
|
||||||
const factory DateCellCalendarEvent.setTime(String time) = _Time;
|
const factory DateCellCalendarEvent.setTime(String time) = _Time;
|
||||||
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime;
|
const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime;
|
||||||
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
|
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
|
||||||
@ -334,10 +400,12 @@ class DateCellCalendarState with _$DateCellCalendarState {
|
|||||||
// cell data from the backend
|
// cell data from the backend
|
||||||
required DateTime? dateTime,
|
required DateTime? dateTime,
|
||||||
required DateTime? endDateTime,
|
required DateTime? endDateTime,
|
||||||
required String? time,
|
required String? timeStr,
|
||||||
required String? endTime,
|
required String? endTimeStr,
|
||||||
required bool includeTime,
|
required bool includeTime,
|
||||||
required bool isRange,
|
required bool isRange,
|
||||||
|
required String? dateStr,
|
||||||
|
required String? endDateStr,
|
||||||
|
|
||||||
// error and hint text
|
// error and hint text
|
||||||
required String? parseTimeError,
|
required String? parseTimeError,
|
||||||
@ -349,18 +417,19 @@ class DateCellCalendarState with _$DateCellCalendarState {
|
|||||||
DateTypeOptionPB dateTypeOptionPB,
|
DateTypeOptionPB dateTypeOptionPB,
|
||||||
DateCellDataPB? cellData,
|
DateCellDataPB? cellData,
|
||||||
) {
|
) {
|
||||||
final (dateTime, endDateTime, time, endTime, includeTime, isRange) =
|
final dateCellData = _dateDataFromCellData(cellData);
|
||||||
_dateDataFromCellData(cellData);
|
|
||||||
return DateCellCalendarState(
|
return DateCellCalendarState(
|
||||||
dateTypeOptionPB: dateTypeOptionPB,
|
dateTypeOptionPB: dateTypeOptionPB,
|
||||||
startDay: isRange ? dateTime : null,
|
startDay: dateCellData.isRange ? dateCellData.dateTime : null,
|
||||||
endDay: isRange ? endDateTime : null,
|
endDay: dateCellData.isRange ? dateCellData.endDateTime : null,
|
||||||
dateTime: dateTime,
|
dateTime: dateCellData.dateTime,
|
||||||
endDateTime: endDateTime,
|
endDateTime: dateCellData.endDateTime,
|
||||||
time: time,
|
timeStr: dateCellData.timeStr,
|
||||||
endTime: endTime,
|
endTimeStr: dateCellData.endTimeStr,
|
||||||
includeTime: includeTime,
|
dateStr: dateCellData.dateStr,
|
||||||
isRange: isRange,
|
endDateStr: dateCellData.endDateStr,
|
||||||
|
includeTime: dateCellData.includeTime,
|
||||||
|
isRange: dateCellData.isRange,
|
||||||
parseTimeError: null,
|
parseTimeError: null,
|
||||||
parseEndTimeError: null,
|
parseEndTimeError: null,
|
||||||
timeHintText: _timeHintText(dateTypeOptionPB),
|
timeHintText: _timeHintText(dateTypeOptionPB),
|
||||||
@ -379,31 +448,78 @@ String _timeHintText(DateTypeOptionPB typeOption) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(DateTime?, DateTime?, String?, String?, bool, bool) _dateDataFromCellData(
|
DateCellData _dateDataFromCellData(
|
||||||
DateCellDataPB? cellData,
|
DateCellDataPB? cellData,
|
||||||
) {
|
) {
|
||||||
// a null DateCellDataPB may be returned, indicating that all the fields are
|
// a null DateCellDataPB may be returned, indicating that all the fields are
|
||||||
// their default values: empty strings and false booleans
|
// their default values: empty strings and false booleans
|
||||||
if (cellData == null) {
|
if (cellData == null) {
|
||||||
return (null, null, null, null, false, false);
|
return DateCellData(
|
||||||
|
dateTime: null,
|
||||||
|
endDateTime: null,
|
||||||
|
timeStr: null,
|
||||||
|
endTimeStr: null,
|
||||||
|
includeTime: false,
|
||||||
|
isRange: false,
|
||||||
|
dateStr: null,
|
||||||
|
endDateStr: null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime? dateTime;
|
DateTime? dateTime;
|
||||||
String? time;
|
String? timeStr;
|
||||||
DateTime? endDateTime;
|
DateTime? endDateTime;
|
||||||
String? endTime;
|
String? endTimeStr;
|
||||||
|
|
||||||
|
String? endDateStr;
|
||||||
if (cellData.hasTimestamp()) {
|
if (cellData.hasTimestamp()) {
|
||||||
final timestamp = cellData.timestamp * 1000;
|
final timestamp = cellData.timestamp * 1000;
|
||||||
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
||||||
time = cellData.time;
|
timeStr = cellData.time;
|
||||||
if (cellData.hasEndTimestamp()) {
|
if (cellData.hasEndTimestamp()) {
|
||||||
final endTimestamp = cellData.endTimestamp * 1000;
|
final endTimestamp = cellData.endTimestamp * 1000;
|
||||||
endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt());
|
endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt());
|
||||||
endTime = cellData.endTime;
|
endTimeStr = cellData.endTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final bool includeTime = cellData.includeTime;
|
final bool includeTime = cellData.includeTime;
|
||||||
final bool isRange = cellData.isRange;
|
final bool isRange = cellData.isRange;
|
||||||
|
|
||||||
return (dateTime, endDateTime, time, endTime, includeTime, isRange);
|
if (cellData.isRange) {
|
||||||
|
endDateStr = cellData.endDate;
|
||||||
|
}
|
||||||
|
final String dateStr = cellData.date;
|
||||||
|
|
||||||
|
return DateCellData(
|
||||||
|
dateTime: dateTime,
|
||||||
|
endDateTime: endDateTime,
|
||||||
|
timeStr: timeStr,
|
||||||
|
endTimeStr: endTimeStr,
|
||||||
|
includeTime: includeTime,
|
||||||
|
isRange: isRange,
|
||||||
|
dateStr: dateStr,
|
||||||
|
endDateStr: endDateStr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DateCellData {
|
||||||
|
final DateTime? dateTime;
|
||||||
|
final DateTime? endDateTime;
|
||||||
|
final String? timeStr;
|
||||||
|
final String? endTimeStr;
|
||||||
|
final bool includeTime;
|
||||||
|
final bool isRange;
|
||||||
|
final String? dateStr;
|
||||||
|
final String? endDateStr;
|
||||||
|
|
||||||
|
DateCellData({
|
||||||
|
required this.dateTime,
|
||||||
|
required this.endDateTime,
|
||||||
|
required this.timeStr,
|
||||||
|
required this.endTimeStr,
|
||||||
|
required this.includeTime,
|
||||||
|
required this.isRange,
|
||||||
|
required this.dateStr,
|
||||||
|
required this.endDateStr,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,7 @@ class StartTextField extends StatelessWidget {
|
|||||||
child: state.includeTime
|
child: state.includeTime
|
||||||
? _TimeTextField(
|
? _TimeTextField(
|
||||||
isEndTime: false,
|
isEndTime: false,
|
||||||
timeStr: state.time,
|
timeStr: state.timeStr,
|
||||||
popoverMutex: popoverMutex,
|
popoverMutex: popoverMutex,
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
@ -161,7 +161,7 @@ class EndTextField extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: _TimeTextField(
|
child: _TimeTextField(
|
||||||
isEndTime: true,
|
isEndTime: true,
|
||||||
timeStr: state.endTime,
|
timeStr: state.endTimeStr,
|
||||||
popoverMutex: popoverMutex,
|
popoverMutex: popoverMutex,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -413,17 +413,17 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
|||||||
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
|
return BlocConsumer<DateCellCalendarBloc, DateCellCalendarState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (widget.isEndTime) {
|
if (widget.isEndTime) {
|
||||||
_textController.text = state.endTime ?? "";
|
_textController.text = state.endTimeStr ?? "";
|
||||||
} else {
|
} else {
|
||||||
_textController.text = state.time ?? "";
|
_textController.text = state.timeStr ?? "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
String text = "";
|
String text = "";
|
||||||
if (!widget.isEndTime && state.time != null) {
|
if (!widget.isEndTime && state.timeStr != null) {
|
||||||
text = state.time!;
|
text = state.timeStr!;
|
||||||
} else if (state.endTime != null) {
|
} else if (state.endTimeStr != null) {
|
||||||
text = state.endTime!;
|
text = state.endTimeStr!;
|
||||||
}
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||||
|
@ -134,7 +134,7 @@ class _PropertyCellState extends State<_PropertyCell> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final style = _customCellStyle(widget.cellContext.fieldType);
|
final style = customCellStyle(widget.cellContext.fieldType);
|
||||||
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
final cell = widget.cellBuilder.build(widget.cellContext, style: style);
|
||||||
|
|
||||||
final dragThumb = MouseRegion(
|
final dragThumb = MouseRegion(
|
||||||
@ -247,7 +247,7 @@ class _PropertyCellState extends State<_PropertyCell> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GridCellStyle? _customCellStyle(FieldType fieldType) {
|
GridCellStyle? customCellStyle(FieldType fieldType) {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
return GridCheckboxCellStyle(
|
return GridCheckboxCellStyle(
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart';
|
||||||
|
import 'package:appflowy/mobile/presentation/database/card/row/cells/cells.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
|
import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
||||||
@ -7,6 +11,7 @@ import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dar
|
|||||||
import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart';
|
import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart';
|
||||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/startup/tasks/app_widget.dart';
|
import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||||
@ -16,6 +21,7 @@ import 'package:appflowy/util/platform_extension.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';
|
import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart';
|
||||||
import 'package:flowy_infra/time/duration.dart';
|
import 'package:flowy_infra/time/duration.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
GoRouter generateRouter(Widget child) {
|
GoRouter generateRouter(Widget child) {
|
||||||
@ -45,6 +51,11 @@ GoRouter generateRouter(Widget child) {
|
|||||||
_mobileGridScreenRoute(),
|
_mobileGridScreenRoute(),
|
||||||
_mobileBoardScreenRoute(),
|
_mobileBoardScreenRoute(),
|
||||||
_mobileCalendarScreenRoute(),
|
_mobileCalendarScreenRoute(),
|
||||||
|
// card detail page
|
||||||
|
_mobileCardDetailScreenRoute(),
|
||||||
|
_mobileCardPropertyEditScreenRoute(),
|
||||||
|
_mobileDateCellEditScreenRoute(),
|
||||||
|
_mobileCreateRowFieldScreenRoute(),
|
||||||
|
|
||||||
// home
|
// home
|
||||||
// MobileHomeSettingPage is outside the bottom navigation bar, thus it is not in the StatefulShellRoute.
|
// MobileHomeSettingPage is outside the bottom navigation bar, thus it is not in the StatefulShellRoute.
|
||||||
@ -415,6 +426,77 @@ GoRoute _mobileCalendarScreenRoute() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GoRoute _mobileCardDetailScreenRoute() {
|
||||||
|
return GoRoute(
|
||||||
|
path: MobileCardDetailScreen.routeName,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final args = state.extra as Map<String, dynamic>;
|
||||||
|
final rowController = args[MobileCardDetailScreen.argRowController];
|
||||||
|
|
||||||
|
return MaterialPage(
|
||||||
|
child: MobileCardDetailScreen(
|
||||||
|
rowController: rowController,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GoRoute _mobileCardPropertyEditScreenRoute() {
|
||||||
|
return GoRoute(
|
||||||
|
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||||
|
path: CardPropertyEditScreen.routeName,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final args = state.extra as Map<String, dynamic>;
|
||||||
|
final cellContext = args[CardPropertyEditScreen.argCellContext];
|
||||||
|
final rowDetailBloc = args[CardPropertyEditScreen.argRowDetailBloc];
|
||||||
|
|
||||||
|
return MaterialPage(
|
||||||
|
child: BlocProvider.value(
|
||||||
|
value: rowDetailBloc as RowDetailBloc,
|
||||||
|
child: CardPropertyEditScreen(
|
||||||
|
cellContext: cellContext,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GoRoute _mobileDateCellEditScreenRoute() {
|
||||||
|
return GoRoute(
|
||||||
|
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||||
|
path: MobileDateCellEditScreen.routeName,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final args = state.extra as Map<String, dynamic>;
|
||||||
|
final cellController = args[MobileDateCellEditScreen.argCellController];
|
||||||
|
|
||||||
|
return MaterialPage(
|
||||||
|
child: MobileDateCellEditScreen(cellController),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GoRoute _mobileCreateRowFieldScreenRoute() {
|
||||||
|
return GoRoute(
|
||||||
|
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||||
|
path: MobileCreateRowFieldScreen.routeName,
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
final args = state.extra as Map<String, dynamic>;
|
||||||
|
final viewId = args[MobileCreateRowFieldScreen.argViewId];
|
||||||
|
final typeOption = args[MobileCreateRowFieldScreen.argTypeOption];
|
||||||
|
|
||||||
|
return MaterialPage(
|
||||||
|
child:
|
||||||
|
MobileCreateRowFieldScreen(viewId: viewId, typeOption: typeOption),
|
||||||
|
fullscreenDialog: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
GoRoute _rootRoute(Widget child) {
|
GoRoute _rootRoute(Widget child) {
|
||||||
return GoRoute(
|
return GoRoute(
|
||||||
path: '/',
|
path: '/',
|
||||||
|
@ -239,6 +239,9 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
),
|
),
|
||||||
colorScheme: colorTheme,
|
colorScheme: colorTheme,
|
||||||
indicatorColor: Colors.blue,
|
indicatorColor: Colors.blue,
|
||||||
|
textSelectionTheme: TextSelectionThemeData(
|
||||||
|
cursorColor: colorTheme.onBackground,
|
||||||
|
),
|
||||||
extensions: [
|
extensions: [
|
||||||
AFThemeExtension(
|
AFThemeExtension(
|
||||||
warning: theme.yellow,
|
warning: theme.yellow,
|
||||||
|
@ -108,7 +108,7 @@ dependencies:
|
|||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
super_clipboard: ^0.6.3
|
super_clipboard: ^0.6.3
|
||||||
go_router: ^10.1.2
|
go_router: ^12.1.1
|
||||||
string_validator: ^1.0.0
|
string_validator: ^1.0.0
|
||||||
unsplash_client: ^2.1.1
|
unsplash_client: ^2.1.1
|
||||||
flutter_emoji_mart:
|
flutter_emoji_mart:
|
||||||
|
@ -497,6 +497,14 @@
|
|||||||
"timeFormatTwelveHour": "12 hour",
|
"timeFormatTwelveHour": "12 hour",
|
||||||
"timeFormatTwentyFourHour": "24 hour",
|
"timeFormatTwentyFourHour": "24 hour",
|
||||||
"clearDate": "Clear date",
|
"clearDate": "Clear date",
|
||||||
|
"dateTime": "Date time",
|
||||||
|
"startDateTime": "Start date time",
|
||||||
|
"endDateTime": "End date time",
|
||||||
|
"failedToLoadDate": "Failed to load date value",
|
||||||
|
"selectTime": "Select time",
|
||||||
|
"selectDate": "Select date",
|
||||||
|
"visibility": "Visibility",
|
||||||
|
"propertyType": "Property type",
|
||||||
"addSelectOption": "Add an option",
|
"addSelectOption": "Add an option",
|
||||||
"optionTitle": "Options",
|
"optionTitle": "Options",
|
||||||
"addOption": "Add option",
|
"addOption": "Add option",
|
||||||
@ -786,13 +794,20 @@
|
|||||||
"collapseTooltip": "Hide the hidden groups",
|
"collapseTooltip": "Hide the hidden groups",
|
||||||
"expandTooltip": "View the hidden groups"
|
"expandTooltip": "View the hidden groups"
|
||||||
},
|
},
|
||||||
|
"cardDetail": "Card Detail",
|
||||||
|
"cardActions": "Card Actions",
|
||||||
|
"cardDuplicated": "Card has been duplicated",
|
||||||
|
"cardDeleted": "Card has been deleted",
|
||||||
"menuName": "Board",
|
"menuName": "Board",
|
||||||
"showUngrouped": "Show ungrouped items",
|
"showUngrouped": "Show ungrouped items",
|
||||||
"ungroupedButtonText": "Ungrouped",
|
"ungroupedButtonText": "Ungrouped",
|
||||||
"ungroupedButtonTooltip": "Contains cards that don't belong in any group",
|
"ungroupedButtonTooltip": "Contains cards that don't belong in any group",
|
||||||
"ungroupedItemsTitle": "Click to add to the board",
|
"ungroupedItemsTitle": "Click to add to the board",
|
||||||
"groupBy": "Group by",
|
"groupBy": "Group by",
|
||||||
"referencedBoardPrefix": "View of"
|
"referencedBoardPrefix": "View of",
|
||||||
|
"mobile": {
|
||||||
|
"editURL": "Edit URL"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"menuName": "Calendar",
|
"menuName": "Calendar",
|
||||||
|
Loading…
Reference in New Issue
Block a user