Merge pull request #521 from AppFlowy-IO/fix/grid_ui_adjust

Fix: grid UI adjust
This commit is contained in:
Nathan.fooo
2022-06-01 16:45:59 +08:00
committed by GitHub
24 changed files with 483 additions and 385 deletions

View File

@ -180,7 +180,8 @@
"row": { "row": {
"duplicate": "Duplicate", "duplicate": "Duplicate",
"delete": "Delete", "delete": "Delete",
"textPlaceholder": "Empty" "textPlaceholder": "Empty",
"copyProperty": "Copied property to clipboard"
}, },
"selectOption": { "selectOption": {
"create": "Create", "create": "Create",

View File

@ -16,6 +16,7 @@ import 'package:app_flowy/workspace/presentation/home/menu/menu.dart';
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder-data-model/app.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_profile.pb.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
class DependencyResolver { class DependencyResolver {
@ -46,6 +47,8 @@ void _resolveUserDeps(GetIt getIt) {
} }
void _resolveHomeDeps(GetIt getIt) { void _resolveHomeDeps(GetIt getIt) {
getIt.registerSingleton(FToast());
getIt.registerSingleton(MenuSharedState()); getIt.registerSingleton(MenuSharedState());
getIt.registerFactoryParam<UserListener, UserProfile, void>( getIt.registerFactoryParam<UserListener, UserProfile, void>(

View File

@ -67,7 +67,8 @@ class ApplicationWidget extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) => ChangeNotifierProvider.value( Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: settingModel, value: settingModel,
builder: (context, _) { builder: (context, _) {
const ratio = 1.73; const ratio = 1.73;
@ -102,6 +103,7 @@ class ApplicationWidget extends StatelessWidget {
}, },
); );
} }
}
class AppGlobals { class AppGlobals {
static GlobalKey<NavigatorState> rootNavKey = GlobalKey(); static GlobalKey<NavigatorState> rootNavKey = GlobalKey();

View File

@ -105,7 +105,7 @@ class _GridCellContext<T, D> extends Equatable {
final FieldService _fieldService; final FieldService _fieldService;
late final CellListener _cellListener; late final CellListener _cellListener;
late final ValueNotifier<T?> _cellDataNotifier; late final ValueNotifier<T?>? _cellDataNotifier;
bool isListening = false; bool isListening = false;
VoidCallback? _onFieldChangedFn; VoidCallback? _onFieldChangedFn;
Timer? _loadDataOperation; Timer? _loadDataOperation;
@ -163,19 +163,19 @@ class _GridCellContext<T, D> extends Equatable {
} }
onCellChangedFn() { onCellChangedFn() {
onCellChanged(_cellDataNotifier.value); onCellChanged(_cellDataNotifier?.value);
if (cellDataLoader.config.reloadOnCellChanged) { if (cellDataLoader.config.reloadOnCellChanged) {
_loadData(); _loadData();
} }
} }
_cellDataNotifier.addListener(onCellChangedFn); _cellDataNotifier?.addListener(onCellChangedFn);
return onCellChangedFn; return onCellChangedFn;
} }
void removeListener(VoidCallback fn) { void removeListener(VoidCallback fn) {
_cellDataNotifier.removeListener(fn); _cellDataNotifier?.removeListener(fn);
} }
T? getCellData({bool loadIfNoCache = true}) { T? getCellData({bool loadIfNoCache = true}) {
@ -211,7 +211,7 @@ class _GridCellContext<T, D> extends Equatable {
_loadDataOperation?.cancel(); _loadDataOperation?.cancel();
_loadDataOperation = Timer(const Duration(milliseconds: 10), () { _loadDataOperation = Timer(const Duration(milliseconds: 10), () {
cellDataLoader.loadData().then((data) { cellDataLoader.loadData().then((data) {
_cellDataNotifier.value = data; _cellDataNotifier?.value = data;
cellCache.insert(GridCellCacheData(key: _cacheKey, object: data)); cellCache.insert(GridCellCacheData(key: _cacheKey, object: data));
}); });
}); });

View File

@ -2,15 +2,13 @@ import 'dart:io' show Platform;
import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/home_screen.dart'; import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/log.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:time/time.dart'; import 'package:time/time.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:app_flowy/plugin/plugin.dart'; import 'package:app_flowy/plugin/plugin.dart';
import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart'; import 'package:app_flowy/workspace/presentation/plugins/blank/blank.dart';
import 'package:app_flowy/workspace/presentation/home/home_sizes.dart'; import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
@ -22,8 +20,6 @@ import 'package:flowy_infra/notifier.dart';
typedef NavigationCallback = void Function(String id); typedef NavigationCallback = void Function(String id);
late FToast fToast;
class HomeStack extends StatelessWidget { class HomeStack extends StatelessWidget {
static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); static GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
// final Size size; // final Size size;
@ -74,8 +70,7 @@ class _FadingIndexedStackState extends State<FadingIndexedStack> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
fToast = FToast(); initToastWithContext(context);
fToast.init(HomeScreen.scaffoldKey.currentState!.context);
} }
@override @override

View File

@ -0,0 +1,37 @@
import 'package:app_flowy/startup/startup.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class FlowyMessageToast extends StatelessWidget {
final String message;
const FlowyMessageToast({required this.message, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: FlowyText.medium(message, color: Colors.white),
),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(4)),
color: Colors.black,
),
);
}
}
void initToastWithContext(BuildContext context) {
getIt<FToast>().init(context);
}
void showMessageToast(String message) {
final child = FlowyMessageToast(message: message);
getIt<FToast>().showToast(
child: child,
gravity: ToastGravity.BOTTOM,
toastDuration: const Duration(seconds: 3),
);
}

View File

@ -0,0 +1,171 @@
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/widgets.dart';
import 'package:flowy_infra/theme.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flowy_infra/size.dart';
import 'package:styled_widget/styled_widget.dart';
class GridCellAccessoryBuildContext {
final BuildContext anchorContext;
GridCellAccessoryBuildContext({required this.anchorContext});
}
abstract class GridCellAccessory implements Widget {
void onTap();
}
typedef AccessoryBuilder = List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext);
abstract class AccessoryWidget extends Widget {
const AccessoryWidget({Key? key}) : super(key: key);
// The hover will show if the onFocus's value is true
ValueNotifier<bool>? get isFocus;
AccessoryBuilder? get accessoryBuilder;
}
class AccessoryHover extends StatefulWidget {
final AccessoryWidget child;
final EdgeInsets contentPadding;
const AccessoryHover({
required this.child,
this.contentPadding = EdgeInsets.zero,
Key? key,
}) : super(key: key);
@override
State<AccessoryHover> createState() => _AccessoryHoverState();
}
class _AccessoryHoverState extends State<AccessoryHover> {
late AccessoryHoverState _hoverState;
VoidCallback? _listenerFn;
@override
void initState() {
_hoverState = AccessoryHoverState();
_listenerFn = () => _hoverState.isFocus = widget.child.isFocus?.value ?? false;
widget.child.isFocus?.addListener(_listenerFn!);
super.initState();
}
@override
void dispose() {
_hoverState.dispose();
if (_listenerFn != null) {
widget.child.isFocus?.removeListener(_listenerFn!);
_listenerFn = null;
}
super.dispose();
}
@override
Widget build(BuildContext context) {
List<Widget> children = [
const _Background(),
Padding(padding: widget.contentPadding, child: widget.child),
];
final accessoryBuilder = widget.child.accessoryBuilder;
if (accessoryBuilder != null) {
final accessories = accessoryBuilder((GridCellAccessoryBuildContext(anchorContext: context)));
children.add(
Padding(
padding: const EdgeInsets.only(right: 6),
child: AccessoryContainer(accessories: accessories),
).positioned(right: 0),
);
}
return ChangeNotifierProvider.value(
value: _hoverState,
child: MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() => _hoverState.onHover = true),
onExit: (p) => setState(() => _hoverState.onHover = false),
child: Stack(
fit: StackFit.loose,
alignment: AlignmentDirectional.center,
children: children,
),
),
);
}
}
class AccessoryHoverState extends ChangeNotifier {
bool _onHover = false;
bool _isFocus = false;
set onHover(bool value) {
if (_onHover != value) {
_onHover = value;
notifyListeners();
}
}
bool get onHover => _onHover;
set isFocus(bool value) {
if (_isFocus != value) {
_isFocus = value;
notifyListeners();
}
}
bool get isFocus => _isFocus;
}
class _Background extends StatelessWidget {
const _Background({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return Consumer<AccessoryHoverState>(
builder: (context, state, child) {
if (state.onHover || state.isFocus) {
return FlowyHoverContainer(
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
);
} else {
return const SizedBox();
}
},
);
}
}
class AccessoryContainer extends StatelessWidget {
final List<GridCellAccessory> accessories;
const AccessoryContainer({required this.accessories, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
final children = accessories.map((accessory) {
final hover = FlowyHover(
style: HoverStyle(hoverColor: theme.bg3, backgroundColor: theme.surface),
builder: (_, onHover) => Container(
width: 26,
height: 26,
padding: const EdgeInsets.all(3),
child: accessory,
),
);
return GestureDetector(
child: hover,
behavior: HitTestBehavior.opaque,
onTap: () => accessory.onTap(),
);
}).toList();
return Wrap(children: children, spacing: 6);
}
}

View File

@ -1,5 +1,4 @@
import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart'; import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType; import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
@ -8,6 +7,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'cell_accessory.dart';
import 'checkbox_cell.dart'; import 'checkbox_cell.dart';
import 'date_cell/date_cell.dart'; import 'date_cell/date_cell.dart';
import 'number_cell.dart'; import 'number_cell.dart';
@ -48,24 +48,27 @@ class BlankCell extends StatelessWidget {
} }
} }
abstract class GridCellWidget implements FlowyHoverWidget { abstract class GridCellWidget implements AccessoryWidget, CellContainerFocustable {
@override @override
final ValueNotifier<bool> onFocus = ValueNotifier<bool>(false); final ValueNotifier<bool> isFocus = ValueNotifier<bool>(false);
final GridCellRequestFocusNotifier requestFocus = GridCellRequestFocusNotifier(); @override
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext)? get accessoryBuilder => null;
@override
final GridCellRequestBeginFocus requestBeginFocus = GridCellRequestBeginFocus();
} }
class GridCellRequestFocusNotifier extends ChangeNotifier { class GridCellRequestBeginFocus extends ChangeNotifier {
VoidCallback? _listener; VoidCallback? _listener;
@override void setListener(VoidCallback listener) {
void addListener(VoidCallback listener) {
if (_listener != null) { if (_listener != null) {
removeListener(_listener!); removeListener(_listener!);
} }
_listener = listener; _listener = listener;
super.addListener(listener); addListener(listener);
} }
void removeAllListener() { void removeAllListener() {
@ -81,10 +84,10 @@ class GridCellRequestFocusNotifier extends ChangeNotifier {
abstract class GridCellStyle {} abstract class GridCellStyle {}
class CellSingleFocusNode extends FocusNode { class SingleListenrFocusNode extends FocusNode {
VoidCallback? _listener; VoidCallback? _listener;
void setSingleListener(VoidCallback listener) { void setListener(VoidCallback listener) {
if (_listener != null) { if (_listener != null) {
removeListener(_listener!); removeListener(_listener!);
} }
@ -93,7 +96,7 @@ class CellSingleFocusNode extends FocusNode {
super.addListener(listener); super.addListener(listener);
} }
void removeSingleListener() { void removeAllListener() {
if (_listener != null) { if (_listener != null) {
removeListener(_listener!); removeListener(_listener!);
} }
@ -123,9 +126,14 @@ class CellStateNotifier extends ChangeNotifier {
bool get onEnter => _onEnter; bool get onEnter => _onEnter;
} }
abstract class CellContainerFocustable {
// Listen on the requestBeginFocus if the
GridCellRequestBeginFocus get requestBeginFocus;
}
class CellContainer extends StatelessWidget { class CellContainer extends StatelessWidget {
final GridCellWidget child; final GridCellWidget child;
final Widget? expander; final AccessoryBuilder? accessoryBuilder;
final double width; final double width;
final RegionStateNotifier rowStateNotifier; final RegionStateNotifier rowStateNotifier;
const CellContainer({ const CellContainer({
@ -133,7 +141,7 @@ class CellContainer extends StatelessWidget {
required this.child, required this.child,
required this.width, required this.width,
required this.rowStateNotifier, required this.rowStateNotifier,
this.expander, this.accessoryBuilder,
}) : super(key: key); }) : super(key: key);
@override @override
@ -145,17 +153,21 @@ class CellContainer extends StatelessWidget {
selector: (context, notifier) => notifier.isFocus, selector: (context, notifier) => notifier.isFocus,
builder: (context, isFocus, _) { builder: (context, isFocus, _) {
Widget container = Center(child: child); Widget container = Center(child: child);
child.onFocus.addListener(() { child.isFocus.addListener(() {
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value; Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.isFocus.value;
}); });
if (expander != null) { if (accessoryBuilder != null) {
container = CellEnterRegion(child: container, expander: expander!); final buildContext = GridCellAccessoryBuildContext(anchorContext: context);
final accessories = accessoryBuilder!(buildContext);
if (accessories.isNotEmpty) {
container = CellEnterRegion(child: container, accessories: accessories);
}
} }
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => child.requestFocus.notify(), onTap: () => child.requestBeginFocus.notify(),
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: width, minHeight: 46), constraints: BoxConstraints(maxWidth: width, minHeight: 46),
decoration: _makeBoxDecoration(context, isFocus), decoration: _makeBoxDecoration(context, isFocus),
@ -182,8 +194,8 @@ class CellContainer extends StatelessWidget {
class CellEnterRegion extends StatelessWidget { class CellEnterRegion extends StatelessWidget {
final Widget child; final Widget child;
final Widget expander; final List<GridCellAccessory> accessories;
const CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key); const CellEnterRegion({required this.child, required this.accessories, Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -192,7 +204,7 @@ class CellEnterRegion extends StatelessWidget {
builder: (context, onEnter, _) { builder: (context, onEnter, _) {
List<Widget> children = [child]; List<Widget> children = [child];
if (onEnter) { if (onEnter) {
children.add(expander.positioned(right: 0)); children.add(AccessoryContainer(accessories: accessories).positioned(right: 0));
} }
return MouseRegion( return MouseRegion(
@ -202,7 +214,6 @@ class CellEnterRegion extends StatelessWidget {
child: Stack( child: Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
fit: StackFit.expand, fit: StackFit.expand,
// alignment: AlignmentDirectional.centerEnd,
children: children, children: children,
), ),
); );

View File

@ -24,7 +24,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
void initState() { void initState() {
final cellContext = widget.cellContextBuilder.build(); final cellContext = widget.cellContextBuilder.build();
_cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial()); _cellBloc = getIt<CheckboxCellBloc>(param1: cellContext)..add(const CheckboxCellEvent.initial());
_listenCellRequestFocus(); _handleRequestFocus();
super.initState(); super.initState();
} }
@ -51,19 +51,19 @@ class _CheckboxCellState extends State<CheckboxCell> {
@override @override
void didUpdateWidget(covariant CheckboxCell oldWidget) { void didUpdateWidget(covariant CheckboxCell oldWidget) {
_listenCellRequestFocus(); _handleRequestFocus();
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
widget.requestFocus.removeAllListener(); widget.requestBeginFocus.removeAllListener();
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }
void _listenCellRequestFocus() { void _handleRequestFocus() {
widget.requestFocus.addListener(() { widget.requestBeginFocus.setListener(() {
_cellBloc.add(const CheckboxCellEvent.select()); _cellBloc.add(const CheckboxCellEvent.select());
}); });
} }

View File

@ -76,8 +76,8 @@ class _DateCellState extends State<DateCell> {
void _showCalendar(BuildContext context) { void _showCalendar(BuildContext context) {
final bloc = context.read<DateCellBloc>(); final bloc = context.read<DateCellBloc>();
widget.onFocus.value = true; widget.isFocus.value = true;
final calendar = DateCellEditor(onDismissed: () => widget.onFocus.value = false); final calendar = DateCellEditor(onDismissed: () => widget.isFocus.value = false);
calendar.show( calendar.show(
context, context,
cellContext: bloc.cellContext.clone(), cellContext: bloc.cellContext.clone(),

View File

@ -22,7 +22,7 @@ class NumberCell extends StatefulWidget with GridCellWidget {
class _NumberCellState extends State<NumberCell> { class _NumberCellState extends State<NumberCell> {
late NumberCellBloc _cellBloc; late NumberCellBloc _cellBloc;
late TextEditingController _controller; late TextEditingController _controller;
late CellSingleFocusNode _focusNode; late SingleListenrFocusNode _focusNode;
Timer? _delayOperation; Timer? _delayOperation;
@override @override
@ -30,14 +30,14 @@ class _NumberCellState extends State<NumberCell> {
final cellContext = widget.cellContextBuilder.build(); final cellContext = widget.cellContextBuilder.build();
_cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial()); _cellBloc = getIt<NumberCellBloc>(param1: cellContext)..add(const NumberCellEvent.initial());
_controller = TextEditingController(text: contentFromState(_cellBloc.state)); _controller = TextEditingController(text: contentFromState(_cellBloc.state));
_focusNode = CellSingleFocusNode(); _focusNode = SingleListenrFocusNode();
_listenFocusNode(); _listenOnFocusNodeChanged();
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_listenCellRequestFocus(context); _handleCellRequestFocus(context);
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: MultiBlocListener( child: MultiBlocListener(
@ -65,19 +65,17 @@ class _NumberCellState extends State<NumberCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
widget.requestFocus.removeAllListener(); widget.requestBeginFocus.removeAllListener();
_delayOperation?.cancel(); _delayOperation?.cancel();
_cellBloc.close(); _cellBloc.close();
_focusNode.removeSingleListener(); _focusNode.removeAllListener();
_focusNode.dispose(); _focusNode.dispose();
super.dispose(); super.dispose();
} }
@override @override
void didUpdateWidget(covariant NumberCell oldWidget) { void didUpdateWidget(covariant NumberCell oldWidget) {
if (oldWidget != widget) { _listenOnFocusNodeChanged();
_listenFocusNode();
}
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
@ -92,16 +90,16 @@ class _NumberCellState extends State<NumberCell> {
} }
} }
void _listenFocusNode() { void _listenOnFocusNodeChanged() {
widget.onFocus.value = _focusNode.hasFocus; widget.isFocus.value = _focusNode.hasFocus;
_focusNode.setSingleListener(() { _focusNode.setListener(() {
widget.onFocus.value = _focusNode.hasFocus; widget.isFocus.value = _focusNode.hasFocus;
focusChanged(); focusChanged();
}); });
} }
void _listenCellRequestFocus(BuildContext context) { void _handleCellRequestFocus(BuildContext context) {
widget.requestFocus.addListener(() { widget.requestBeginFocus.setListener(() {
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) { if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(_focusNode); FocusScope.of(context).requestFocus(_focusNode);
} }

View File

@ -59,7 +59,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
return _SelectOptionCell( return _SelectOptionCell(
selectOptions: state.selectedOptions, selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle, cellStyle: widget.cellStyle,
onFocus: (value) => widget.onFocus.value = value, onFocus: (value) => widget.isFocus.value = value,
cellContextBuilder: widget.cellContextBuilder); cellContextBuilder: widget.cellContextBuilder);
}, },
), ),
@ -113,7 +113,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
return _SelectOptionCell( return _SelectOptionCell(
selectOptions: state.selectedOptions, selectOptions: state.selectedOptions,
cellStyle: widget.cellStyle, cellStyle: widget.cellStyle,
onFocus: (value) => widget.onFocus.value = value, onFocus: (value) => widget.isFocus.value = value,
cellContextBuilder: widget.cellContextBuilder); cellContextBuilder: widget.cellContextBuilder);
}, },
), ),

View File

@ -35,7 +35,7 @@ class GridTextCell extends StatefulWidget with GridCellWidget {
class _GridTextCellState extends State<GridTextCell> { class _GridTextCellState extends State<GridTextCell> {
late TextCellBloc _cellBloc; late TextCellBloc _cellBloc;
late TextEditingController _controller; late TextEditingController _controller;
late CellSingleFocusNode _focusNode; late SingleListenrFocusNode _focusNode;
Timer? _delayOperation; Timer? _delayOperation;
@override @override
@ -44,9 +44,9 @@ class _GridTextCellState extends State<GridTextCell> {
_cellBloc = getIt<TextCellBloc>(param1: cellContext); _cellBloc = getIt<TextCellBloc>(param1: cellContext);
_cellBloc.add(const TextCellEvent.initial()); _cellBloc.add(const TextCellEvent.initial());
_controller = TextEditingController(text: _cellBloc.state.content); _controller = TextEditingController(text: _cellBloc.state.content);
_focusNode = CellSingleFocusNode(); _focusNode = SingleListenrFocusNode();
_listenFocusNode(); _listenOnFocusNodeChanged();
_listenRequestFocus(context); _listenRequestFocus(context);
super.initState(); super.initState();
} }
@ -81,10 +81,10 @@ class _GridTextCellState extends State<GridTextCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
widget.requestFocus.removeAllListener(); widget.requestBeginFocus.removeAllListener();
_delayOperation?.cancel(); _delayOperation?.cancel();
_cellBloc.close(); _cellBloc.close();
_focusNode.removeSingleListener(); _focusNode.removeAllListener();
_focusNode.dispose(); _focusNode.dispose();
super.dispose(); super.dispose();
@ -93,21 +93,21 @@ class _GridTextCellState extends State<GridTextCell> {
@override @override
void didUpdateWidget(covariant GridTextCell oldWidget) { void didUpdateWidget(covariant GridTextCell oldWidget) {
if (oldWidget != widget) { if (oldWidget != widget) {
_listenFocusNode(); _listenOnFocusNodeChanged();
} }
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
void _listenFocusNode() { void _listenOnFocusNodeChanged() {
widget.onFocus.value = _focusNode.hasFocus; widget.isFocus.value = _focusNode.hasFocus;
_focusNode.setSingleListener(() { _focusNode.setListener(() {
widget.onFocus.value = _focusNode.hasFocus; widget.isFocus.value = _focusNode.hasFocus;
focusChanged(); focusChanged();
}); });
} }
void _listenRequestFocus(BuildContext context) { void _listenRequestFocus(BuildContext context) {
widget.requestFocus.addListener(() { widget.requestBeginFocus.setListener(() {
if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) { if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
FocusScope.of(context).requestFocus(_focusNode); FocusScope.of(context).requestFocus(_focusNode);
} }

View File

@ -6,7 +6,7 @@ import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class URLCellEditor extends StatefulWidget { class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate {
final GridURLCellContext cellContext; final GridURLCellContext cellContext;
const URLCellEditor({required this.cellContext, Key? key}) : super(key: key); const URLCellEditor({required this.cellContext, Key? key}) : super(key: key);
@ -25,18 +25,27 @@ class URLCellEditor extends StatefulWidget {
// //
FlowyOverlay.of(context).insertWithAnchor( FlowyOverlay.of(context).insertWithAnchor(
widget: OverlayContainer( widget: OverlayContainer(
child: SizedBox(width: 200, child: editor), child: SizedBox(
width: 200,
child: Padding(padding: const EdgeInsets.all(6), child: editor),
),
constraints: BoxConstraints.loose(const Size(300, 160)), constraints: BoxConstraints.loose(const Size(300, 160)),
), ),
identifier: URLCellEditor.identifier(), identifier: URLCellEditor.identifier(),
anchorContext: context, anchorContext: context,
anchorDirection: AnchorDirection.bottomWithCenterAligned, anchorDirection: AnchorDirection.bottomWithCenterAligned,
delegate: editor,
); );
} }
static String identifier() { static String identifier() {
return (URLCellEditor).toString(); return (URLCellEditor).toString();
} }
@override
bool asBarrier() {
return true;
}
} }
class _URLCellEditorState extends State<URLCellEditor> { class _URLCellEditorState extends State<URLCellEditor> {

View File

@ -1,10 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart'; import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -14,11 +17,19 @@ import 'cell_editor.dart';
class GridURLCellStyle extends GridCellStyle { class GridURLCellStyle extends GridCellStyle {
String? placeholder; String? placeholder;
List<GridURLCellAccessoryType> accessoryTypes;
GridURLCellStyle({ GridURLCellStyle({
this.placeholder, this.placeholder,
this.accessoryTypes = const [],
}); });
} }
enum GridURLCellAccessoryType {
edit,
copyURL,
}
class GridURLCell extends StatefulWidget with GridCellWidget { class GridURLCell extends StatefulWidget with GridCellWidget {
final GridCellContextBuilder cellContextBuilder; final GridCellContextBuilder cellContextBuilder;
late final GridURLCellStyle? cellStyle; late final GridURLCellStyle? cellStyle;
@ -36,6 +47,35 @@ class GridURLCell extends StatefulWidget with GridCellWidget {
@override @override
State<GridURLCell> createState() => _GridURLCellState(); State<GridURLCell> createState() => _GridURLCellState();
GridCellAccessory accessoryFromType(GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) {
switch (ty) {
case GridURLCellAccessoryType.edit:
final cellContext = cellContextBuilder.build() as GridURLCellContext;
return _EditURLAccessory(cellContext: cellContext, anchorContext: buildContext.anchorContext);
case GridURLCellAccessoryType.copyURL:
final cellContext = cellContextBuilder.build() as GridURLCellContext;
return _CopyURLAccessory(cellContext: cellContext);
}
}
@override
List<GridCellAccessory> Function(GridCellAccessoryBuildContext buildContext) get accessoryBuilder => (buildContext) {
final List<GridCellAccessory> accessories = [];
if (cellStyle != null) {
accessories.addAll(cellStyle!.accessoryTypes.map((ty) {
return accessoryFromType(ty, buildContext);
}));
}
// If the accessories is empty then the default accessory will be GridURLCellAccessoryType.edit
if (accessories.isEmpty) {
accessories.add(accessoryFromType(GridURLCellAccessoryType.edit, buildContext));
}
return accessories;
};
} }
class _GridURLCellState extends State<GridURLCell> { class _GridURLCellState extends State<GridURLCell> {
@ -46,7 +86,7 @@ class _GridURLCellState extends State<GridURLCell> {
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext; final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
_cellBloc = URLCellBloc(cellContext: cellContext); _cellBloc = URLCellBloc(cellContext: cellContext);
_cellBloc.add(const URLCellEvent.initial()); _cellBloc.add(const URLCellEvent.initial());
_listenRequestFocus(context); _handleRequestFocus();
super.initState(); super.initState();
} }
@ -66,17 +106,18 @@ class _GridURLCellState extends State<GridURLCell> {
fontSize: 14, fontSize: 14,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
), ),
recognizer: _tapGesture(context),
), ),
); );
return CellEnterRegion( return SizedBox.expand(
child: GestureDetector(
child: Align(alignment: Alignment.centerLeft, child: richText), child: Align(alignment: Alignment.centerLeft, child: richText),
expander: _EditCellIndicator(onTap: () { onTap: () async {
final cellContext = widget.cellContextBuilder.build() as GridURLCellContext; widget.isFocus.value = true;
URLCellEditor.show(context, cellContext); final url = context.read<URLCellBloc>().state.url;
}), await _openUrlOrEdit(url);
); },
));
}, },
), ),
); );
@ -84,18 +125,15 @@ class _GridURLCellState extends State<GridURLCell> {
@override @override
Future<void> dispose() async { Future<void> dispose() async {
widget.requestFocus.removeAllListener(); widget.requestBeginFocus.removeAllListener();
_cellBloc.close(); _cellBloc.close();
super.dispose(); super.dispose();
} }
TapGestureRecognizer _tapGesture(BuildContext context) { @override
final gesture = TapGestureRecognizer(); void didUpdateWidget(covariant GridURLCell oldWidget) {
gesture.onTap = () async { _handleRequestFocus();
final url = context.read<URLCellBloc>().state.url; super.didUpdateWidget(oldWidget);
await _openUrlOrEdit(url);
};
return gesture;
} }
Future<void> _openUrlOrEdit(String url) async { Future<void> _openUrlOrEdit(String url) async {
@ -108,27 +146,48 @@ class _GridURLCellState extends State<GridURLCell> {
} }
} }
void _listenRequestFocus(BuildContext context) { void _handleRequestFocus() {
widget.requestFocus.addListener(() { widget.requestBeginFocus.setListener(() {
_openUrlOrEdit(_cellBloc.state.url); _openUrlOrEdit(_cellBloc.state.url);
}); });
} }
} }
class _EditCellIndicator extends StatelessWidget { class _EditURLAccessory extends StatelessWidget with GridCellAccessory {
final VoidCallback onTap; final GridURLCellContext cellContext;
const _EditCellIndicator({required this.onTap, Key? key}) : super(key: key); final BuildContext anchorContext;
const _EditURLAccessory({
required this.cellContext,
required this.anchorContext,
Key? key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FlowyIconButton( return svgWidget("editor/edit", color: theme.iconColor);
width: 26, }
onPressed: onTap,
hoverColor: theme.hover, @override
radius: BorderRadius.circular(4), void onTap() {
iconPadding: const EdgeInsets.all(5), URLCellEditor.show(anchorContext, cellContext);
icon: svgWidget("editor/edit", color: theme.iconColor), }
); }
class _CopyURLAccessory extends StatelessWidget with GridCellAccessory {
final GridURLCellContext cellContext;
const _CopyURLAccessory({required this.cellContext, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return svgWidget("editor/copy", color: theme.iconColor);
}
@override
void onTap() {
final content = cellContext.getCellData(loadIfNoCache: false)?.content ?? "";
Clipboard.setData(ClipboardData(text: content));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
} }
} }

View File

@ -86,7 +86,6 @@ class _GridHeaderCellContainer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
final borderSide = BorderSide(color: theme.shader5, width: 1.0); final borderSide = BorderSide(color: theme.shader5, width: 1.0);
final decoration = BoxDecoration( final decoration = BoxDecoration(
border: Border( border: Border(
top: borderSide, top: borderSide,

View File

@ -1,5 +1,6 @@
import 'package:app_flowy/workspace/application/grid/prelude.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
@ -170,16 +171,30 @@ class _RowCells extends StatelessWidget {
List<Widget> _makeCells(BuildContext context, GridCellMap gridCellMap) { List<Widget> _makeCells(BuildContext context, GridCellMap gridCellMap) {
return gridCellMap.values.map( return gridCellMap.values.map(
(gridCell) { (gridCell) {
Widget? expander; final GridCellWidget child = buildGridCellWidget(gridCell, cellCache);
List<GridCellAccessory> accessories = [];
if (gridCell.field.isPrimary) { if (gridCell.field.isPrimary) {
expander = _CellExpander(onExpand: onExpand); accessories.add(_PrimaryCellAccessory(onTapCallback: onExpand));
}
accessoryBuilder(buildContext) {
final builder = child.accessoryBuilder;
List<GridCellAccessory> accessories = [];
if (gridCell.field.isPrimary) {
accessories.add(_PrimaryCellAccessory(onTapCallback: onExpand));
}
if (builder != null) {
accessories.addAll(builder(buildContext));
}
return accessories;
} }
return CellContainer( return CellContainer(
width: gridCell.field.width.toDouble(), width: gridCell.field.width.toDouble(),
child: buildGridCellWidget(gridCell, cellCache), child: child,
rowStateNotifier: Provider.of<RegionStateNotifier>(context, listen: false), rowStateNotifier: Provider.of<RegionStateNotifier>(context, listen: false),
expander: expander, accessoryBuilder: accessoryBuilder,
); );
}, },
).toList(); ).toList();
@ -199,23 +214,19 @@ class RegionStateNotifier extends ChangeNotifier {
bool get onEnter => _onEnter; bool get onEnter => _onEnter;
} }
class _CellExpander extends StatelessWidget { class _PrimaryCellAccessory extends StatelessWidget with GridCellAccessory {
final VoidCallback onExpand; final VoidCallback onTapCallback;
const _CellExpander({required this.onExpand, Key? key}) : super(key: key); const _PrimaryCellAccessory({required this.onTapCallback, Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
return FittedBox( return svgWidget("grid/expander", color: theme.main1);
fit: BoxFit.contain, }
child: FlowyIconButton(
width: 26, @override
onPressed: onExpand, void onTap() {
iconPadding: const EdgeInsets.all(5), onTapCallback();
radius: BorderRadius.circular(4),
icon: svgWidget("grid/expander", color: theme.main1),
),
);
} }
} }

View File

@ -1,44 +0,0 @@
import 'package:app_flowy/startup/startup.dart';
import 'package:app_flowy/workspace/application/grid/prelude.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class NumberCell extends StatefulWidget {
final GridCell cellData;
const NumberCell({
required this.cellData,
Key? key,
}) : super(key: key);
@override
State<NumberCell> createState() => _NumberCellState();
}
class _NumberCellState extends State<NumberCell> {
late NumberCellBloc _cellBloc;
@override
void initState() {
_cellBloc = getIt<NumberCellBloc>(param1: widget.cellData);
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<NumberCellBloc, NumberCellState>(
builder: (context, state) {
return Container();
},
),
);
}
@override
Future<void> dispose() async {
_cellBloc.close();
super.dispose();
}
}

View File

@ -24,6 +24,7 @@ class GridRowActionSheet extends StatelessWidget {
child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>( child: BlocBuilder<RowActionSheetBloc, RowActionSheetState>(
builder: (context, state) { builder: (context, state) {
final cells = _RowAction.values final cells = _RowAction.values
.where((value) => value.enable())
.map( .map(
(action) => _RowActionCell( (action) => _RowActionCell(
action: action, action: action,

View File

@ -3,6 +3,7 @@ import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart'; import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart';
import 'package:app_flowy/workspace/application/grid/row/row_service.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:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_accessory.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart';
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart';
@ -10,7 +11,6 @@ import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.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/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
@ -149,12 +149,18 @@ class _RowDetailCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = context.watch<AppTheme>(); final theme = context.watch<AppTheme>();
final style = _customCellStyle(theme, gridCell.field.fieldType);
final cell = buildGridCellWidget(gridCell, cellCache, style: style);
final cell = buildGridCellWidget( final gesture = GestureDetector(
gridCell, behavior: HitTestBehavior.translucent,
cellCache, onTap: () => cell.requestBeginFocus.notify(),
style: _buildCellStyle(theme, gridCell.field.fieldType), child: AccessoryHover(
child: cell,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
),
); );
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 40), constraints: const BoxConstraints(minHeight: 40),
child: IntrinsicHeight( child: IntrinsicHeight(
@ -167,12 +173,7 @@ class _RowDetailCell extends StatelessWidget {
child: FieldCellButton(field: gridCell.field, onTap: () => _showFieldEditor(context)), child: FieldCellButton(field: gridCell.field, onTap: () => _showFieldEditor(context)),
), ),
const HSpace(10), const HSpace(10),
Expanded( Expanded(child: gesture),
child: FlowyHover2(
child: cell,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
),
),
], ],
), ),
), ),
@ -191,7 +192,7 @@ class _RowDetailCell extends StatelessWidget {
} }
} }
GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) { GridCellStyle? _customCellStyle(AppTheme theme, FieldType fieldType) {
switch (fieldType) { switch (fieldType) {
case FieldType.Checkbox: case FieldType.Checkbox:
return null; return null;
@ -217,7 +218,11 @@ GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) {
case FieldType.URL: case FieldType.URL:
return GridURLCellStyle( return GridURLCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
accessoryTypes: [
GridURLCellAccessoryType.edit,
GridURLCellAccessoryType.copyURL,
],
); );
} }
return null; throw UnimplementedError;
} }

View File

@ -85,7 +85,7 @@ class GridSettingList extends StatelessWidget {
} }
Widget _renderList() { Widget _renderList() {
final cells = GridSettingAction.values.map((action) { final cells = GridSettingAction.values.where((value) => value.enable()).map((action) {
return _SettingItem(action: action); return _SettingItem(action: action);
}).toList(); }).toList();

View File

@ -1,5 +1,5 @@
import 'package:app_flowy/startup/tasks/rust_sdk.dart'; import 'package:app_flowy/startup/tasks/rust_sdk.dart';
import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:app_flowy/workspace/presentation/home/toast.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
@ -16,7 +16,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:fluttertoast/fluttertoast.dart';
class QuestionBubble extends StatelessWidget { class QuestionBubble extends StatelessWidget {
const QuestionBubble({Key? key}) : super(key: key); const QuestionBubble({Key? key}) : super(key: key);
@ -46,7 +45,7 @@ class QuestionBubble extends StatelessWidget {
_launchURL("https://discord.gg/9Q2xaN37tV"); _launchURL("https://discord.gg/9Q2xaN37tV");
break; break;
case BubbleAction.debug: case BubbleAction.debug:
const _DebugToast().show(); _DebugToast().show();
break; break;
} }
}); });
@ -71,55 +70,14 @@ class QuestionBubble extends StatelessWidget {
} }
} }
class _DebugToast extends StatelessWidget { class _DebugToast {
const _DebugToast({Key? key}) : super(key: key); void show() async {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: Future(() async {
var debugInfo = ""; var debugInfo = "";
debugInfo += await _getDeviceInfo(); debugInfo += await _getDeviceInfo();
debugInfo += await _getDocumentPath(); debugInfo += await _getDocumentPath();
Clipboard.setData(ClipboardData(text: debugInfo)); Clipboard.setData(ClipboardData(text: debugInfo));
}),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return _done(context, Text("Error: ${snapshot.error}"));
} else {
return _done(context, null);
}
} else {
return const CircularProgressIndicator();
}
},
);
}
Widget _done(BuildContext context, Widget? error) { showMessageToast(LocaleKeys.questionBubble_debug_success.tr());
final theme = context.watch<AppTheme>();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(25.0), color: theme.main1),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.check),
const SizedBox(width: 12.0),
(error == null) ? Text(LocaleKeys.questionBubble_debug_success.tr()) : error
],
),
);
}
void show() {
fToast.showToast(
child: this,
gravity: ToastGravity.BOTTOM,
toastDuration: const Duration(seconds: 3),
);
} }
Future<String> _getDeviceInfo() async { Future<String> _getDeviceInfo() async {

View File

@ -1,9 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// ignore: unused_import // ignore: unused_import
import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme.dart';
import 'package:provider/provider.dart';
typedef HoverBuilder = Widget Function(BuildContext context, bool onHover); typedef HoverBuilder = Widget Function(BuildContext context, bool onHover);
@ -52,7 +49,7 @@ class _FlowyHoverState extends State<FlowyHover> {
child: child, child: child,
); );
} else { } else {
return child; return Container(child: child, color: widget.style.backgroundColor);
} }
} }
} }
@ -63,12 +60,14 @@ class HoverStyle {
final Color hoverColor; final Color hoverColor;
final BorderRadius borderRadius; final BorderRadius borderRadius;
final EdgeInsets contentMargin; final EdgeInsets contentMargin;
final Color backgroundColor;
const HoverStyle( const HoverStyle(
{this.borderColor = Colors.transparent, {this.borderColor = Colors.transparent,
this.borderWidth = 0, this.borderWidth = 0,
this.borderRadius = const BorderRadius.all(Radius.circular(6)), this.borderRadius = const BorderRadius.all(Radius.circular(6)),
this.contentMargin = EdgeInsets.zero, this.contentMargin = EdgeInsets.zero,
this.backgroundColor = Colors.transparent,
required this.hoverColor}); required this.hoverColor});
} }
@ -100,120 +99,3 @@ class FlowyHoverContainer extends StatelessWidget {
); );
} }
} }
//
abstract class FlowyHoverWidget extends Widget {
const FlowyHoverWidget({Key? key}) : super(key: key);
ValueNotifier<bool>? get onFocus;
}
class FlowyHover2 extends StatefulWidget {
final FlowyHoverWidget child;
final EdgeInsets contentPadding;
const FlowyHover2({
required this.child,
this.contentPadding = EdgeInsets.zero,
Key? key,
}) : super(key: key);
@override
State<FlowyHover2> createState() => _FlowyHover2State();
}
class _FlowyHover2State extends State<FlowyHover2> {
late FlowyHoverState _hoverState;
VoidCallback? _listenerFn;
@override
void initState() {
_hoverState = FlowyHoverState();
listener() {
_hoverState.onFocus = widget.child.onFocus?.value ?? false;
}
_listenerFn = listener;
widget.child.onFocus?.addListener(listener);
super.initState();
}
@override
void dispose() {
_hoverState.dispose();
if (_listenerFn != null) {
widget.child.onFocus?.removeListener(_listenerFn!);
_listenerFn = null;
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _hoverState,
child: MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() => _hoverState.onHover = true),
onExit: (p) => setState(() => _hoverState.onHover = false),
child: Stack(
fit: StackFit.loose,
alignment: AlignmentDirectional.center,
children: [
const _HoverBackground(),
Padding(
padding: widget.contentPadding,
child: widget.child,
),
],
),
),
);
}
}
class _HoverBackground extends StatelessWidget {
const _HoverBackground({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = context.watch<AppTheme>();
return Consumer<FlowyHoverState>(
builder: (context, state, child) {
if (state.onHover || state.onFocus) {
return FlowyHoverContainer(
style: HoverStyle(borderRadius: Corners.s6Border, hoverColor: theme.shader6),
);
} else {
return const SizedBox();
}
},
);
}
}
class FlowyHoverState extends ChangeNotifier {
bool _onHover = false;
bool _onFocus = false;
set onHover(bool value) {
if (_onHover != value) {
_onHover = value;
notifyListeners();
}
}
bool get onHover => _onHover;
set onFocus(bool value) {
if (_onFocus != value) {
_onFocus = value;
notifyListeners();
}
}
bool get onFocus => _onFocus;
}

View File

@ -72,7 +72,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2 cupertino_icons: ^1.0.2
device_info_plus: ^3.2.1 device_info_plus: ^3.2.1
fluttertoast: ^8.0.8 fluttertoast: ^8.0.9
table_calendar: ^3.0.5 table_calendar: ^3.0.5
reorderables: ^0.5.0 reorderables: ^0.5.0
linked_scroll_controller: ^0.2.0 linked_scroll_controller: ^0.2.0