mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: insert a new field to the left or right of an existing one (#4022)
* feat: allow inserting fields before or after a certain field * fix: tauri build * chore: implement frontend * test: rust-lib tests * test: integration test * chore: point to temp collab rev * chore: bump collab rev * chore: fix tauri build * chore: fix the tauri build, for real this time * fix: new field editor show detail not general --------- Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field_settings/field_settings_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
@ -25,11 +26,13 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
final FieldBackendService fieldService;
|
||||
final FieldSettingsBackendService fieldSettingsService;
|
||||
final TypeOptionController typeOptionController;
|
||||
final void Function(String newFieldId)? onFieldInserted;
|
||||
|
||||
FieldEditorBloc({
|
||||
required this.viewId,
|
||||
required this.field,
|
||||
required this.fieldController,
|
||||
this.onFieldInserted,
|
||||
required FieldTypeOptionLoader loader,
|
||||
}) : typeOptionController = TypeOptionController(
|
||||
field: field,
|
||||
@ -73,6 +76,28 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
final result = await fieldService.updateField(name: newName);
|
||||
_logIfError(result);
|
||||
},
|
||||
insertLeft: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: viewId,
|
||||
position: CreateFieldPosition.Before,
|
||||
targetFieldId: field.id,
|
||||
);
|
||||
result.fold(
|
||||
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id),
|
||||
(err) => Log.error("Failed creating field $err"),
|
||||
);
|
||||
},
|
||||
insertRight: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: viewId,
|
||||
position: CreateFieldPosition.After,
|
||||
targetFieldId: field.id,
|
||||
);
|
||||
result.fold(
|
||||
(typeOptionPB) => onFieldInserted?.call(typeOptionPB.field_2.id),
|
||||
(err) => Log.error("Failed creating field $err"),
|
||||
);
|
||||
},
|
||||
toggleFieldVisibility: () async {
|
||||
final currentVisibility =
|
||||
state.field.visibility ?? FieldVisibility.AlwaysShown;
|
||||
@ -122,6 +147,8 @@ class FieldEditorEvent with _$FieldEditorEvent {
|
||||
const factory FieldEditorEvent.switchFieldType(final FieldType fieldType) =
|
||||
_SwitchFieldType;
|
||||
const factory FieldEditorEvent.renameField(final String name) = _RenameField;
|
||||
const factory FieldEditorEvent.insertLeft() = _InsertLeft;
|
||||
const factory FieldEditorEvent.insertRight() = _InsertRight;
|
||||
const factory FieldEditorEvent.toggleFieldVisibility() =
|
||||
_ToggleFieldVisiblity;
|
||||
const factory FieldEditorEvent.deleteField() = _DeleteField;
|
||||
|
@ -26,11 +26,20 @@ class TypeOptionBackendService {
|
||||
static Future<Either<TypeOptionPB, FlowyError>> createFieldTypeOption({
|
||||
required String viewId,
|
||||
FieldType fieldType = FieldType.RichText,
|
||||
CreateFieldPosition position = CreateFieldPosition.End,
|
||||
String? targetFieldId,
|
||||
}) {
|
||||
final payload = CreateFieldPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..fieldType = FieldType.RichText;
|
||||
|
||||
return DatabaseEventCreateTypeOption(payload).send();
|
||||
if (position == CreateFieldPosition.Before ||
|
||||
position == CreateFieldPosition.After && targetFieldId != null) {
|
||||
payload.targetFieldId = targetFieldId!;
|
||||
}
|
||||
|
||||
payload.fieldPosition = position;
|
||||
|
||||
return DatabaseEventCreateField(payload).send();
|
||||
}
|
||||
}
|
||||
|
@ -20,17 +20,17 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
}) : super(GridHeaderState.initial()) {
|
||||
on<GridHeaderEvent>(
|
||||
(event, emit) async {
|
||||
await event.map(
|
||||
initial: (_InitialHeader value) {
|
||||
await event.when(
|
||||
initial: () {
|
||||
_startListening();
|
||||
add(
|
||||
GridHeaderEvent.didReceiveFieldUpdate(fieldController.fieldInfos),
|
||||
);
|
||||
},
|
||||
didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) {
|
||||
didReceiveFieldUpdate: (List<FieldInfo> fields) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
fields: value.fields
|
||||
fields: fields
|
||||
.where(
|
||||
(element) =>
|
||||
element.visibility != null &&
|
||||
@ -40,8 +40,17 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
moveField: (_MoveField value) async {
|
||||
await _moveField(value, emit);
|
||||
startEditingField: (fieldId) {
|
||||
emit(state.copyWith(editingFieldId: fieldId));
|
||||
},
|
||||
startEditingNewField: (fieldId) {
|
||||
emit(state.copyWith(editingFieldId: fieldId, newFieldId: fieldId));
|
||||
},
|
||||
endEditingField: () {
|
||||
emit(state.copyWith(editingFieldId: null, newFieldId: null));
|
||||
},
|
||||
moveField: (field, fromIndex, toIndex) async {
|
||||
await _moveField(field, fromIndex, toIndex, emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -49,19 +58,17 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
|
||||
}
|
||||
|
||||
Future<void> _moveField(
|
||||
_MoveField value,
|
||||
FieldPB field,
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
Emitter<GridHeaderState> emit,
|
||||
) async {
|
||||
final fields = List<FieldInfo>.from(state.fields);
|
||||
fields.insert(value.toIndex, fields.removeAt(value.fromIndex));
|
||||
fields.insert(toIndex, fields.removeAt(fromIndex));
|
||||
emit(state.copyWith(fields: fields));
|
||||
|
||||
final fieldService =
|
||||
FieldBackendService(viewId: viewId, fieldId: value.field.id);
|
||||
final result = await fieldService.moveField(
|
||||
value.fromIndex,
|
||||
value.toIndex,
|
||||
);
|
||||
final fieldService = FieldBackendService(viewId: viewId, fieldId: field.id);
|
||||
final result = await fieldService.moveField(fromIndex, toIndex);
|
||||
result.fold((l) {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
@ -79,6 +86,11 @@ class GridHeaderEvent with _$GridHeaderEvent {
|
||||
const factory GridHeaderEvent.initial() = _InitialHeader;
|
||||
const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =
|
||||
_DidReceiveFieldUpdate;
|
||||
const factory GridHeaderEvent.startEditingField(String fieldId) =
|
||||
_StartEditingField;
|
||||
const factory GridHeaderEvent.startEditingNewField(String fieldId) =
|
||||
_StartEditingNewField;
|
||||
const factory GridHeaderEvent.endEditingField() = _EndEditingField;
|
||||
const factory GridHeaderEvent.moveField(
|
||||
FieldPB field,
|
||||
int fromIndex,
|
||||
@ -88,8 +100,12 @@ class GridHeaderEvent with _$GridHeaderEvent {
|
||||
|
||||
@freezed
|
||||
class GridHeaderState with _$GridHeaderState {
|
||||
const factory GridHeaderState({required List<FieldInfo> fields}) =
|
||||
_GridHeaderState;
|
||||
const factory GridHeaderState({
|
||||
required List<FieldInfo> fields,
|
||||
required String? editingFieldId,
|
||||
required String? newFieldId,
|
||||
}) = _GridHeaderState;
|
||||
|
||||
factory GridHeaderState.initial() => const GridHeaderState(fields: []);
|
||||
factory GridHeaderState.initial() =>
|
||||
const GridHeaderState(fields: [], editingFieldId: null, newFieldId: null);
|
||||
}
|
||||
|
@ -16,16 +16,27 @@ import 'field_editor.dart';
|
||||
import 'field_type_extension.dart';
|
||||
|
||||
class GridFieldCell extends StatefulWidget {
|
||||
final String viewId;
|
||||
final FieldController fieldController;
|
||||
final FieldInfo fieldInfo;
|
||||
const GridFieldCell({
|
||||
super.key,
|
||||
required this.viewId,
|
||||
required this.fieldController,
|
||||
required this.fieldInfo,
|
||||
required this.onTap,
|
||||
required this.onEditorOpened,
|
||||
required this.onFieldInsertedOnEitherSide,
|
||||
required this.isEditing,
|
||||
required this.isNew,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
final FieldController fieldController;
|
||||
final FieldInfo fieldInfo;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onEditorOpened;
|
||||
final void Function(String fieldId) onFieldInsertedOnEitherSide;
|
||||
final bool isEditing;
|
||||
final bool isNew;
|
||||
|
||||
@override
|
||||
State<GridFieldCell> createState() => _GridFieldCellState();
|
||||
}
|
||||
@ -39,6 +50,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
||||
super.initState();
|
||||
popoverController = PopoverController();
|
||||
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
|
||||
if (widget.isEditing) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
popoverController.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -46,6 +62,11 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
||||
if (widget.fieldInfo != oldWidget.fieldInfo && !_bloc.isClosed) {
|
||||
_bloc.add(FieldCellEvent.onFieldChanged(widget.fieldInfo));
|
||||
}
|
||||
if (widget.isEditing) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
popoverController.show();
|
||||
});
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@ -62,16 +83,20 @@ class _GridFieldCellState extends State<GridFieldCell> {
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
controller: popoverController,
|
||||
popupBuilder: (BuildContext context) {
|
||||
widget.onEditorOpened();
|
||||
return FieldEditor(
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
field: widget.fieldInfo.field,
|
||||
initialPage: FieldEditorPage.general,
|
||||
initialPage: widget.isNew
|
||||
? FieldEditorPage.details
|
||||
: FieldEditorPage.general,
|
||||
onFieldInserted: widget.onFieldInsertedOnEitherSide,
|
||||
);
|
||||
},
|
||||
child: FieldCellButton(
|
||||
field: widget.fieldInfo.field,
|
||||
onTap: () => popoverController.show(),
|
||||
onTap: widget.onTap,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -30,6 +30,7 @@ class FieldEditor extends StatefulWidget {
|
||||
final FieldController fieldController;
|
||||
final FieldPB field;
|
||||
final FieldEditorPage initialPage;
|
||||
final void Function(String fieldId)? onFieldInserted;
|
||||
|
||||
const FieldEditor({
|
||||
super.key,
|
||||
@ -37,6 +38,7 @@ class FieldEditor extends StatefulWidget {
|
||||
required this.field,
|
||||
required this.fieldController,
|
||||
this.initialPage = FieldEditorPage.details,
|
||||
this.onFieldInserted,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -61,6 +63,7 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
viewId: widget.viewId,
|
||||
field: widget.field,
|
||||
fieldController: widget.fieldController,
|
||||
onFieldInserted: widget.onFieldInserted,
|
||||
loader: FieldTypeOptionLoader(
|
||||
viewId: widget.viewId,
|
||||
field: widget.field,
|
||||
@ -89,6 +92,10 @@ class _FieldEditorState extends State<FieldEditor> {
|
||||
},
|
||||
),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
_actionCell(FieldAction.insertLeft),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
_actionCell(FieldAction.insertRight),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
_actionCell(FieldAction.toggleVisibility),
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
_actionCell(FieldAction.duplicate),
|
||||
@ -170,40 +177,56 @@ class FieldActionCell extends StatelessWidget {
|
||||
),
|
||||
onHover: (_) => popoverMutex?.close(),
|
||||
onTap: () => action.run(context, viewId, fieldInfo),
|
||||
leftIcon: FlowySvg(
|
||||
action.icon(fieldInfo),
|
||||
size: const Size.square(16),
|
||||
color: enable ? null : Theme.of(context).disabledColor,
|
||||
leftIcon: action.icon(
|
||||
fieldInfo,
|
||||
enable ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum FieldAction {
|
||||
insertLeft,
|
||||
insertRight,
|
||||
toggleVisibility,
|
||||
duplicate,
|
||||
delete,
|
||||
}
|
||||
delete;
|
||||
|
||||
extension _FieldActionExtension on FieldAction {
|
||||
FlowySvgData icon(FieldInfo fieldInfo) {
|
||||
Widget icon(FieldInfo fieldInfo, Color? color) {
|
||||
late final FlowySvgData svgData;
|
||||
switch (this) {
|
||||
case FieldAction.insertLeft:
|
||||
svgData = FlowySvgs.arrow_s;
|
||||
case FieldAction.insertRight:
|
||||
svgData = FlowySvgs.arrow_s;
|
||||
case FieldAction.toggleVisibility:
|
||||
if (fieldInfo.visibility != null &&
|
||||
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
|
||||
return FlowySvgs.show_m;
|
||||
svgData = FlowySvgs.show_m;
|
||||
} else {
|
||||
return FlowySvgs.hide_s;
|
||||
svgData = FlowySvgs.hide_s;
|
||||
}
|
||||
case FieldAction.duplicate:
|
||||
return FlowySvgs.copy_s;
|
||||
svgData = FlowySvgs.copy_s;
|
||||
case FieldAction.delete:
|
||||
return FlowySvgs.delete_s;
|
||||
svgData = FlowySvgs.delete_s;
|
||||
}
|
||||
final icon = FlowySvg(
|
||||
svgData,
|
||||
size: const Size.square(16),
|
||||
color: color,
|
||||
);
|
||||
return this == FieldAction.insertRight
|
||||
? Transform.flip(flipX: true, child: icon)
|
||||
: icon;
|
||||
}
|
||||
|
||||
String title(FieldInfo fieldInfo) {
|
||||
switch (this) {
|
||||
case FieldAction.insertLeft:
|
||||
return LocaleKeys.grid_field_insertLeft.tr();
|
||||
case FieldAction.insertRight:
|
||||
return LocaleKeys.grid_field_insertRight.tr();
|
||||
case FieldAction.toggleVisibility:
|
||||
if (fieldInfo.visibility != null &&
|
||||
fieldInfo.visibility == FieldVisibility.AlwaysHidden) {
|
||||
@ -220,6 +243,18 @@ extension _FieldActionExtension on FieldAction {
|
||||
|
||||
void run(BuildContext context, String viewId, FieldInfo fieldInfo) {
|
||||
switch (this) {
|
||||
case FieldAction.insertLeft:
|
||||
PopoverContainer.of(context).close();
|
||||
context
|
||||
.read<FieldEditorBloc>()
|
||||
.add(const FieldEditorEvent.insertLeft());
|
||||
break;
|
||||
case FieldAction.insertRight:
|
||||
PopoverContainer.of(context).close();
|
||||
context
|
||||
.read<FieldEditorBloc>()
|
||||
.add(const FieldEditorEvent.insertRight());
|
||||
break;
|
||||
case FieldAction.toggleVisibility:
|
||||
PopoverContainer.of(context).close();
|
||||
context
|
||||
|
@ -7,17 +7,14 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/theme_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';
|
||||
import 'package:reorderables/reorderables.dart';
|
||||
import '../../../../application/field/type_option/type_option_service.dart';
|
||||
import '../../layout/sizes.dart';
|
||||
import 'field_editor.dart';
|
||||
import 'field_cell.dart';
|
||||
|
||||
class GridHeaderSliverAdaptor extends StatefulWidget {
|
||||
@ -93,7 +90,6 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<GridHeaderBloc, GridHeaderState>(
|
||||
buildWhen: (previous, current) => previous.fields != current.fields,
|
||||
builder: (context, state) {
|
||||
final cells = state.fields
|
||||
.map(
|
||||
@ -103,6 +99,17 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
viewId: widget.viewId,
|
||||
fieldInfo: fieldInfo,
|
||||
fieldController: widget.fieldController,
|
||||
onTap: () => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingField(fieldInfo.id)),
|
||||
onFieldInsertedOnEitherSide: (fieldId) => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingNewField(fieldId)),
|
||||
onEditorOpened: () => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(const GridHeaderEvent.endEditingField()),
|
||||
isEditing: state.editingFieldId == fieldInfo.id,
|
||||
isNew: state.newFieldId == fieldInfo.id,
|
||||
)
|
||||
: MobileFieldButton(
|
||||
key: _getKeyById(fieldInfo.id),
|
||||
@ -184,74 +191,57 @@ class _CellTrailing extends StatelessWidget {
|
||||
)
|
||||
: null,
|
||||
padding: GridSize.headerContentInsets,
|
||||
child: CreateFieldButton(viewId: viewId),
|
||||
child: CreateFieldButton(
|
||||
viewId: viewId,
|
||||
onFieldCreated: (fieldId) => context
|
||||
.read<GridHeaderBloc>()
|
||||
.add(GridHeaderEvent.startEditingNewField(fieldId)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreateFieldButton extends StatefulWidget {
|
||||
final String viewId;
|
||||
const CreateFieldButton({
|
||||
super.key,
|
||||
required this.viewId,
|
||||
required this.onFieldCreated,
|
||||
});
|
||||
|
||||
final String viewId;
|
||||
final void Function(String fieldId) onFieldCreated;
|
||||
|
||||
@override
|
||||
State<CreateFieldButton> createState() => _CreateFieldButtonState();
|
||||
}
|
||||
|
||||
class _CreateFieldButtonState extends State<CreateFieldButton> {
|
||||
final popoverController = PopoverController();
|
||||
late TypeOptionPB typeOption;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fieldController =
|
||||
context.read<GridBloc>().databaseController.fieldController;
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
asBarrier: true,
|
||||
margin: EdgeInsets.zero,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: FlowyButton(
|
||||
margin: PlatformExtension.isDesktop
|
||||
? GridSize.cellContentInsets
|
||||
: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
radius: BorderRadius.zero,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color:
|
||||
PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
onTap: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: widget.viewId,
|
||||
);
|
||||
result.fold(
|
||||
(l) {
|
||||
typeOption = l;
|
||||
popoverController.show();
|
||||
},
|
||||
(r) => Log.error("Failed to create field type option: $r"),
|
||||
);
|
||||
},
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color:
|
||||
PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
|
||||
),
|
||||
return FlowyButton(
|
||||
margin: PlatformExtension.isDesktop
|
||||
? GridSize.cellContentInsets
|
||||
: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||
radius: BorderRadius.zero,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return FieldEditor(
|
||||
hoverColor: AFThemeExtension.of(context).greyHover,
|
||||
onTap: () async {
|
||||
final result = await TypeOptionBackendService.createFieldTypeOption(
|
||||
viewId: widget.viewId,
|
||||
fieldController: fieldController,
|
||||
field: typeOption.field_2,
|
||||
);
|
||||
result.fold(
|
||||
(typeOptionPB) => widget.onFieldCreated(typeOptionPB.field_2.id),
|
||||
(err) => Log.error("Failed to create field type option: $err"),
|
||||
);
|
||||
},
|
||||
leftIcon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: PlatformExtension.isDesktop ? null : Theme.of(context).hintColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user