chore: cell highlight

This commit is contained in:
appflowy 2022-04-04 11:10:41 +08:00
parent 5c3500e24c
commit 1abf0b565f
11 changed files with 321 additions and 97 deletions

View File

@ -185,7 +185,8 @@
"aquaColor": "Aqua",
"blueColor": "Blue",
"deleteTag": "Delete tag",
"colorPannelTitle": "Colors"
"colorPannelTitle": "Colors",
"pannelTitle": "Select an option"
}
}
}

View File

@ -1,9 +1,23 @@
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
class CellContainer extends StatefulWidget {
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
class CellStateNotifier extends ChangeNotifier {
bool _isFocus = false;
set isFocus(bool value) {
if (_isFocus != value) {
_isFocus = value;
notifyListeners();
}
}
bool get isFocus => _isFocus;
}
class CellContainer extends StatelessWidget {
final Widget child;
final double width;
const CellContainer({
@ -12,28 +26,65 @@ class CellContainer extends StatefulWidget {
required this.width,
}) : super(key: key);
@override
State<CellContainer> createState() => _CellContainerState();
}
class _CellContainerState extends State<CellContainer> {
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final borderSide = BorderSide(color: theme.shader4, width: 0.4);
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {},
child: Container(
constraints: BoxConstraints(
maxWidth: widget.width,
),
decoration: BoxDecoration(
border: Border(right: borderSide, bottom: borderSide),
),
padding: GridSize.cellContentInsets,
child: Center(child: IntrinsicHeight(child: widget.child)),
return ChangeNotifierProvider(
create: (_) => CellStateNotifier(),
child: Consumer<CellStateNotifier>(
builder: (context, state, _) {
return Container(
constraints: BoxConstraints(
maxWidth: width,
),
decoration: _makeBoxDecoration(context, state),
padding: GridSize.cellContentInsets,
child: Center(child: IntrinsicHeight(child: child)),
);
},
),
);
}
BoxDecoration _makeBoxDecoration(BuildContext context, CellStateNotifier state) {
final theme = context.watch<AppTheme>();
if (state.isFocus) {
final borderSide = BorderSide(color: theme.main1, width: 1.0);
return BoxDecoration(border: Border.fromBorderSide(borderSide));
} else {
final borderSide = BorderSide(color: theme.shader4, width: 0.4);
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
}
}
}
abstract class GridCell extends StatefulWidget {
const GridCell({Key? key}) : super(key: key);
void setFocus(BuildContext context, bool value) {
Provider.of<CellStateNotifier>(context, listen: false).isFocus = value;
}
}
class CellFocusNode extends FocusNode {
VoidCallback? focusCallback;
void addCallback(BuildContext context, VoidCallback callback) {
if (focusCallback != null) {
removeListener(focusCallback!);
}
focusCallback = () {
Provider.of<CellStateNotifier>(context, listen: false).isFocus = hasFocus;
callback();
};
addListener(focusCallback!);
}
@override
void dispose() {
if (focusCallback != null) {
removeListener(focusCallback!);
}
super.dispose();
}
}

View File

@ -5,23 +5,7 @@ import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SelectionBadge extends StatelessWidget {
final SelectOption option;
const SelectionBadge({required this.option, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: option.color.make(context),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
),
child: FlowyText.medium(option.name, fontSize: 12),
);
}
}
import 'package:textfield_tags/textfield_tags.dart';
extension SelectOptionColorExtension on SelectOptionColor {
Color make(BuildContext context) {
@ -75,3 +59,97 @@ extension SelectOptionColorExtension on SelectOptionColor {
}
}
}
class SelectOptionTextField extends StatelessWidget {
final TextEditingController _controller;
final FocusNode _focusNode;
SelectOptionTextField({
TextEditingController? controller,
FocusNode? focusNode,
Key? key,
}) : _controller = controller ?? TextEditingController(),
_focusNode = focusNode ?? FocusNode(),
super(key: key);
@override
Widget build(BuildContext context) {
return TextFieldTags(
textEditingController: _controller,
initialTags: ["abc", "bdf"],
focusNode: _focusNode,
textSeparators: const [' ', ','],
inputfieldBuilder: (
BuildContext context,
TextEditingController editController,
FocusNode focusNode,
String? error,
void Function(String)? onChanged,
void Function(String)? onSubmitted,
) {
return ((context, sc, tags, onTagDelegate) {
return TextField(
controller: editController,
focusNode: focusNode,
onChanged: (value) {},
onEditingComplete: () => focusNode.unfocus(),
maxLines: 1,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
isDense: true,
prefixIcon: _renderTags(tags, sc),
),
);
});
},
);
}
Widget? _renderTags(List<String> tags, ScrollController sc) {
if (tags.isEmpty) {
return null;
}
return SingleChildScrollView(
controller: sc,
scrollDirection: Axis.horizontal,
child: Row(children: [
Container(
decoration: BoxDecoration(
color: Color.fromARGB(255, 74, 137, 92),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
),
child: FlowyText.medium("efc", fontSize: 12),
),
Container(
decoration: BoxDecoration(
color: Color.fromARGB(255, 74, 137, 92),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
),
child: FlowyText.medium("abc", fontSize: 12),
)
]),
);
}
}
class SelectionBadge extends StatelessWidget {
final SelectOption option;
const SelectionBadge({required this.option, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: option.color.make(context),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
),
child: FlowyText.medium(option.name, fontSize: 12),
);
}
}

View File

@ -1,7 +1,10 @@
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart';
import 'package:flutter/material.dart';
import 'extension.dart';
class SingleSelectCell extends StatefulWidget {
final FutureCellData cellData;
@ -15,22 +18,31 @@ class SingleSelectCell extends StatefulWidget {
}
class _SingleSelectCellState extends State<SingleSelectCell> {
late CellFocusNode _focusNode;
late SelectionCellBloc _cellBloc;
late TextEditingController _controller;
@override
void initState() {
_cellBloc = getIt<SelectionCellBloc>(param1: widget.cellData);
_controller = TextEditingController();
_focusNode = CellFocusNode();
super.initState();
}
@override
Widget build(BuildContext context) {
return Container();
_focusNode.addCallback(context, () {});
return SelectOptionTextField(
focusNode: _focusNode,
controller: _controller,
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
_focusNode.dispose();
super.dispose();
}
}

View File

@ -1,37 +1,108 @@
import 'package:app_flowy/workspace/application/grid/cell_bloc/selection_editor_bloc.dart';
import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'selection.dart';
import 'extension.dart';
class SelectionEditor extends StatelessWidget {
final GridCellData cellData;
const SelectionEditor({required this.cellData, Key? key}) : super(key: key);
void show(BuildContext context) {
FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer(
child: this,
constraints: BoxConstraints.loose(const Size(240, 200)),
),
identifier: toString(),
anchorContext: context,
anchorDirection: AnchorDirection.bottomWithLeftAligned,
);
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SelectionEditorBloc(gridId: cellData.gridId, field: cellData.field),
child: BlocBuilder<SelectionEditorBloc, SelectionEditorState>(
builder: (context, state) {
return Container();
return Column(
children: const [
_Title(),
VSpace(10),
_OptionList(),
],
);
},
),
);
}
}
class _OptionList extends StatelessWidget {
const _OptionList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<SelectionEditorBloc, SelectionEditorState>(
builder: (context, state) {
final cells = state.options.map((option) => _SelectionCell(option)).toList();
final list = ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: cells.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return cells[index];
},
);
return list;
},
);
}
}
class _Title extends StatelessWidget {
const _Title({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.typeOptionItemHeight,
child: FlowyText.medium(LocaleKeys.grid_selectOption_pannelTitle.tr(), fontSize: 12),
);
}
}
class _SelectionCell extends StatelessWidget {
final SelectOption option;
const _SelectionCell({required this.option, Key? key}) : super(key: key);
const _SelectionCell(this.option, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
// return FlowyButton(
// text: FlowyText.medium(fieldType.title(), fontSize: 12),
// hoverColor: theme.hover,
// onTap: () => onSelectField(fieldType),
// leftIcon: svgWidget(fieldType.iconName(), color: theme.iconColor),
// );
return InkWell(
onTap: () {},
child: FlowyHover(
@ -43,3 +114,20 @@ class _SelectionCell extends StatelessWidget {
);
}
}
class SelectionBadge extends StatelessWidget {
final SelectOption option;
const SelectionBadge({required this.option, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: option.color.make(context),
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
),
child: FlowyText.medium(option.name, fontSize: 12),
);
}
}

View File

@ -1,13 +1,11 @@
import 'dart:async';
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'cell_container.dart';
/// The interface of base cell.
class GridTextCell extends StatefulWidget {
class GridTextCell extends GridCell {
final FutureCellData cellData;
const GridTextCell({
required this.cellData,
@ -19,21 +17,23 @@ class GridTextCell extends StatefulWidget {
}
class _GridTextCellState extends State<GridTextCell> {
late TextEditingController _controller;
Timer? _delayOperation;
final _focusNode = FocusNode();
late TextCellBloc _cellBloc;
late TextEditingController _controller;
late CellFocusNode _focusNode;
Timer? _delayOperation;
@override
void initState() {
_cellBloc = getIt<TextCellBloc>(param1: widget.cellData);
_controller = TextEditingController(text: _cellBloc.state.content);
_focusNode.addListener(save);
_focusNode = CellFocusNode();
super.initState();
}
@override
Widget build(BuildContext context) {
_focusNode.addCallback(context, focusChanged);
return BlocProvider.value(
value: _cellBloc,
child: BlocConsumer<TextCellBloc, TextCellState>(
@ -46,16 +46,8 @@ class _GridTextCellState extends State<GridTextCell> {
return TextField(
controller: _controller,
focusNode: _focusNode,
onChanged: (value) {
Log.info("On change");
save();
},
onEditingComplete: () {
Log.info("On complete");
},
onSubmitted: (value) {
Log.info("On submit");
},
onChanged: (value) => focusChanged(),
onEditingComplete: () => _focusNode.unfocus(),
maxLines: 1,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
decoration: const InputDecoration(
@ -72,18 +64,18 @@ class _GridTextCellState extends State<GridTextCell> {
@override
Future<void> dispose() async {
_cellBloc.close();
_focusNode.removeListener(save);
_focusNode.dispose();
super.dispose();
}
Future<void> save() async {
_delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 300), () {
if (_cellBloc.isClosed == false) {
_cellBloc.add(TextCellEvent.updateText(_controller.text));
}
});
// and later, before the timer goes off...
Future<void> focusChanged() async {
if (mounted) {
_delayOperation?.cancel();
_delayOperation = Timer(const Duration(milliseconds: 300), () {
if (_cellBloc.isClosed == false) {
_cellBloc.add(TextCellEvent.updateText(_controller.text));
}
});
}
}
}

View File

@ -46,7 +46,7 @@ class _GridHeaderDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return _GridHeaderWidget(gridId: gridId, fields: fields, key: ObjectKey(fields));
return _GridHeaderWidget(gridId: gridId, fields: fields);
}
@override
@ -73,27 +73,22 @@ class _GridHeaderWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return BlocBuilder<GridHeaderBloc, GridHeaderState>(
builder: (context, state) {
final cells = state.fields.map(
(field) => GridFieldCell(
GridFieldCellContext(gridId: gridId, field: field),
),
);
final row = Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _HeaderLeading(),
...cells,
_HeaderTrailing(gridId: gridId),
],
key: UniqueKey(),
);
return Container(height: GridSize.headerHeight, color: theme.surface, child: row);
},
final cells = fields.map(
(field) => GridFieldCell(
GridFieldCellContext(gridId: gridId, field: field),
),
);
final row = Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _HeaderLeading(),
...cells,
_HeaderTrailing(gridId: gridId),
],
);
return Container(height: GridSize.headerHeight, color: theme.surface, child: row);
}
}

View File

@ -1,6 +1,6 @@
import 'package:app_flowy/workspace/application/grid/field/type_option/edit_option_bloc.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart';
@ -164,7 +164,7 @@ class _SelectOptionColorCell extends StatelessWidget {
dimension: 16,
child: Container(
decoration: BoxDecoration(
color: color.color(context),
color: color.make(context),
shape: BoxShape.circle,
),
),

View File

@ -1148,6 +1148,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.11"
textfield_tags:
dependency: "direct main"
description:
name: textfield_tags
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
textstyle_extensions:
dependency: transitive
description:

View File

@ -67,7 +67,7 @@ dependencies:
clipboard: ^0.1.3
connectivity_plus: 2.2.0
easy_localization: ^3.0.0
textfield_tags: ^2.0.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2

View File

@ -303,12 +303,12 @@ impl ClientGridEditor {
};
let field_metas = self.pad.read().await.get_field_metas(field_orders)?;
debug_assert!(field_metas.len() == expected_len);
if field_metas.len() != expected_len {
if expected_len != 0 && field_metas.len() != expected_len {
tracing::error!(
"This is a bug. The len of the field_metas should equal to {}",
expected_len
);
debug_assert!(field_metas.len() == expected_len);
}
// field_metas.retain(|field_meta| field_meta.visibility);
Ok(field_metas)