diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart index b701763a4a..70cedae16b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart @@ -20,6 +20,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CalendarEventEditor extends StatelessWidget { @@ -86,16 +87,22 @@ class EventEditorControls extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - FlowyIconButton( - width: 20, - icon: const FlowySvg(FlowySvgs.m_duplicate_s), - iconColorOnHover: Theme.of(context).colorScheme.onSecondary, - onPressed: () => context.read().add( - CalendarEvent.duplicateEvent( - rowController.viewId, - rowController.rowId, + FlowyTooltip( + message: LocaleKeys.calendar_duplicateEvent.tr(), + child: FlowyIconButton( + width: 20, + icon: const FlowySvg( + FlowySvgs.m_duplicate_s, + size: Size.square(17), + ), + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + onPressed: () => context.read().add( + CalendarEvent.duplicateEvent( + rowController.viewId, + rowController.rowId, + ), ), - ), + ), ), const HSpace(8.0), FlowyIconButton( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart index 698fe0ea76..8fbb40ffde 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/field_type_calc_ext.dart @@ -6,9 +6,15 @@ extension AvailableCalculations on FieldType { CalculationType.Count, ]; - // These FieldTypes cannot be empty, no need to count empty/non-empty - if (![FieldType.Checkbox, FieldType.LastEditedTime, FieldType.CreatedTime] - .contains(this)) { + // These FieldTypes cannot be empty, or might hold secondary + // data causing them to be seen as not empty when in fact they + // are empty. + if (![ + FieldType.URL, + FieldType.Checkbox, + FieldType.LastEditedTime, + FieldType.CreatedTime, + ].contains(this)) { calculationTypes.addAll([ CalculationType.CountEmpty, CalculationType.CountNonEmpty, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 0d2a49e092..1c23939771 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -19,7 +19,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import '../../application/database_controller.dart'; -import '../../application/row/row_cache.dart'; import '../../application/row/row_controller.dart'; import '../../tab_bar/tab_bar_view.dart'; import '../../widgets/row/row_detail.dart'; @@ -259,7 +258,7 @@ class _GridHeader extends StatelessWidget { } } -class _GridRows extends StatelessWidget { +class _GridRows extends StatefulWidget { const _GridRows({ required this.viewId, required this.scrollController, @@ -268,6 +267,30 @@ class _GridRows extends StatelessWidget { final String viewId; final GridScrollController scrollController; + @override + State<_GridRows> createState() => _GridRowsState(); +} + +class _GridRowsState extends State<_GridRows> { + bool showFloatingCalculations = false; + + @override + void initState() { + super.initState(); + _evaluateFloatingCalculations(); + } + + void _evaluateFloatingCalculations() { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + // maxScrollExtent is 0.0 if scrolling is not possible + showFloatingCalculations = widget + .scrollController.verticalController.position.maxScrollExtent > + 0; + }); + }); + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -275,24 +298,18 @@ class _GridRows extends StatelessWidget { builder: (context, state) { return Flexible( child: _WrapScrollView( - scrollController: scrollController, + scrollController: widget.scrollController, contentWidth: GridLayout.headerWidth(state.fields), - child: BlocBuilder( - buildWhen: (previous, current) => current.reason.maybeWhen( - reorderRows: () => true, - reorderSingleRow: (reorderRow, rowInfo) => true, - delete: (item) => true, - insert: (item) => true, - orElse: () => true, - ), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.rowCount != current.rowCount, + listener: (context, state) => _evaluateFloatingCalculations(), builder: (context, state) { - final rowInfos = state.rowInfos; - final behavior = ScrollConfiguration.of(context).copyWith( - scrollbars: false, - ); return ScrollConfiguration( - behavior: behavior, - child: _renderList(context, state, rowInfos), + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + ), + child: _renderList(context, state), ); }, ), @@ -305,9 +322,8 @@ class _GridRows extends StatelessWidget { Widget _renderList( BuildContext context, GridState state, - List rowInfos, ) { - final children = rowInfos.mapIndexed((index, rowInfo) { + final children = state.rowInfos.mapIndexed((index, rowInfo) { return _renderRow( context, rowInfo.rowId, @@ -315,32 +331,56 @@ class _GridRows extends StatelessWidget { index: index, ); }).toList() - ..add(const GridRowBottomBar(key: Key('grid_footer'))) - ..add( - GridCalculationsRow( - key: const Key('grid_calculations'), - viewId: viewId, + ..add(const GridRowBottomBar(key: Key('grid_footer'))); + + if (showFloatingCalculations) { + children.add( + const SizedBox( + key: Key('calculations_bottom_padding'), + height: 36, ), ); + } else { + children.add( + GridCalculationsRow( + key: const Key('grid_calculations'), + viewId: widget.viewId, + ), + ); + } - return ReorderableListView.builder( - /// This is a workaround related to - /// https://github.com/flutter/flutter/issues/25652 - cacheExtent: 5000, - scrollController: scrollController.verticalController, - buildDefaultDragHandles: false, - proxyDecorator: (child, index, animation) => Material( - color: Colors.white.withOpacity(.1), - child: Opacity(opacity: .5, child: child), - ), - onReorder: (fromIndex, newIndex) { - final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; - if (fromIndex != toIndex) { - context.read().add(GridEvent.moveRow(fromIndex, toIndex)); - } - }, - itemCount: children.length, - itemBuilder: (context, index) => children[index], + children.add(const SizedBox(key: Key('footer_padding'), height: 10)); + + return Stack( + children: [ + Positioned.fill( + child: ReorderableListView.builder( + /// This is a workaround related to + /// https://github.com/flutter/flutter/issues/25652 + cacheExtent: 5000, + scrollController: widget.scrollController.verticalController, + physics: const ClampingScrollPhysics(), + buildDefaultDragHandles: false, + proxyDecorator: (child, index, animation) => Material( + color: Colors.white.withOpacity(.1), + child: Opacity(opacity: .5, child: child), + ), + onReorder: (fromIndex, newIndex) { + final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; + if (fromIndex != toIndex) { + context + .read() + .add(GridEvent.moveRow(fromIndex, toIndex)); + } + }, + itemCount: children.length, + itemBuilder: (context, index) => children[index], + ), + ), + if (showFloatingCalculations) ...[ + _PositionedCalculationsRow(viewId: widget.viewId), + ], + ], ); } @@ -378,21 +418,16 @@ class _GridRows extends StatelessWidget { openDetailPage: (context, cellBuilder) { FlowyOverlay.show( context: context, - builder: (BuildContext context) { - return RowDetailPage( - rowController: rowController, - databaseController: databaseController, - ); - }, + builder: (_) => RowDetailPage( + rowController: rowController, + databaseController: databaseController, + ), ); }, ); if (animation != null) { - return SizeTransition( - sizeFactor: animation, - child: child, - ); + return SizeTransition(sizeFactor: animation, child: child); } return child; @@ -413,12 +448,14 @@ class _WrapScrollView extends StatelessWidget { @override Widget build(BuildContext context) { return ScrollbarListStack( + includeInsets: false, axis: Axis.vertical, controller: scrollController.verticalController, barSize: GridSize.scrollBarSize, autoHideScrollbar: false, child: StyledSingleChildScrollView( autoHideScrollbar: false, + includeInsets: false, controller: scrollController.horizontalController, axis: Axis.horizontal, child: SizedBox( @@ -429,3 +466,49 @@ class _WrapScrollView extends StatelessWidget { ); } } + +/// This Widget is used to show the Calculations Row at the bottom of the Grids ScrollView +/// when the ScrollView is scrollable. +/// +class _PositionedCalculationsRow extends StatefulWidget { + const _PositionedCalculationsRow({ + required this.viewId, + }); + + final String viewId; + + @override + State<_PositionedCalculationsRow> createState() => + _PositionedCalculationsRowState(); +} + +class _PositionedCalculationsRowState + extends State<_PositionedCalculationsRow> { + @override + Widget build(BuildContext context) { + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + margin: EdgeInsets.only(left: GridSize.horizontalHeaderPadding), + padding: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Theme.of(context).canvasColor, + border: Border( + top: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: SizedBox( + height: 36, + width: double.infinity, + child: GridCalculationsRow( + key: const Key('floating_grid_calculations'), + viewId: widget.viewId, + includeDefaultInsets: false, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart index 2e73e72e12..c8199ce135 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; @@ -16,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dar 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:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CalculateCell extends StatefulWidget { @@ -35,7 +35,38 @@ class CalculateCell extends StatefulWidget { } class _CalculateCellState extends State { + final _cellScrollController = ScrollController(); bool isSelected = false; + bool isScrollable = false; + + @override + void initState() { + super.initState(); + _checkScrollable(); + } + + @override + void didUpdateWidget(covariant CalculateCell oldWidget) { + _checkScrollable(); + super.didUpdateWidget(oldWidget); + } + + void _checkScrollable() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_cellScrollController.hasClients) { + setState( + () => + isScrollable = _cellScrollController.position.maxScrollExtent > 0, + ); + } + }); + } + + @override + void dispose() { + _cellScrollController.dispose(); + super.dispose(); + } void setIsSelected(bool selected) => setState(() => isSelected = selected); @@ -98,36 +129,62 @@ class _CalculateCellState extends State { Widget _showCalculateValue(BuildContext context, String? prefix) { prefix = prefix != null ? '$prefix ' : ''; + final calculateValue = + '$prefix${_withoutTrailingZeros(widget.calculation!.value)}'; - return FlowyButton( - radius: BorderRadius.zero, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Flexible( - child: FlowyText( - widget.calculation!.calculationType.shortLabel, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, + return FlowyTooltip( + message: !isScrollable ? "" : null, + richMessage: !isScrollable + ? null + : TextSpan( + children: [ + TextSpan( + text: widget.calculation!.calculationType.shortLabel + .toUpperCase(), + ), + const TextSpan(text: ' '), + TextSpan( + text: calculateValue, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ], ), - ), - if (widget.calculation!.value.isNotEmpty) ...[ - const HSpace(8), - Flexible( - child: FlowyText( - '$prefix${_withoutTrailingZeros(widget.calculation!.value)}', - color: AFThemeExtension.of(context).textColor, - overflow: TextOverflow.ellipsis, + child: FlowyButton( + radius: BorderRadius.zero, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: Row( + children: [ + Expanded( + child: SingleChildScrollView( + controller: _cellScrollController, + key: ValueKey(widget.calculation!.id), + reverse: true, + physics: const NeverScrollableScrollPhysics(), + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FlowyText( + widget.calculation!.calculationType.shortLabel + .toUpperCase(), + color: Theme.of(context).hintColor, + fontSize: 10, + ), + if (widget.calculation!.value.isNotEmpty) ...[ + const HSpace(8), + FlowyText( + calculateValue, + color: AFThemeExtension.of(context).textColor, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), ), ), ], - const HSpace(8), - FlowySvg( - FlowySvgs.arrow_down_s, - color: Theme.of(context).hintColor, - ), - ], + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart index 0d44e94190..9d9255e3a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart @@ -7,9 +7,14 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations import 'package:flutter_bloc/flutter_bloc.dart'; class GridCalculationsRow extends StatelessWidget { - const GridCalculationsRow({super.key, required this.viewId}); + const GridCalculationsRow({ + super.key, + required this.viewId, + this.includeDefaultInsets = true, + }); final String viewId; + final bool includeDefaultInsets; @override Widget build(BuildContext context) { @@ -23,7 +28,8 @@ class GridCalculationsRow extends StatelessWidget { child: BlocBuilder( builder: (context, state) { return Padding( - padding: GridSize.contentInsets, + padding: + includeDefaultInsets ? GridSize.contentInsets : EdgeInsets.zero, child: Row( children: [ ...state.fields.map( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 1ae2ff35c3..c8634b3df5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -66,7 +66,7 @@ class _MoreViewActionsState extends State { builder: (context, state) { return AppFlowyPopover( mutex: popoverMutex, - constraints: BoxConstraints.loose(const Size(210, 400)), + constraints: BoxConstraints.loose(const Size(215, 400)), offset: const Offset(0, 30), popupBuilder: (_) { final actions = [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart index f290286097..a5a72964af 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart @@ -24,6 +24,8 @@ class ViewMetaInfo extends StatelessWidget { @override Widget build(BuildContext context) { + final numberFormat = NumberFormat(); + // If more info is added to this Widget, use a separated ListView return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), @@ -33,15 +35,21 @@ class ViewMetaInfo extends StatelessWidget { if (documentCounters != null) ...[ FlowyText.regular( LocaleKeys.moreAction_wordCount.tr( - args: [documentCounters!.wordCount.toString()], + args: [ + numberFormat.format(documentCounters!.wordCount).toString(), + ], ), + fontSize: 11, color: Theme.of(context).hintColor, ), const VSpace(2), FlowyText.regular( LocaleKeys.moreAction_charCount.tr( - args: [documentCounters!.charCount.toString()], + args: [ + numberFormat.format(documentCounters!.charCount).toString(), + ], ), + fontSize: 11, color: Theme.of(context).hintColor, ), ], @@ -51,6 +59,7 @@ class ViewMetaInfo extends StatelessWidget { LocaleKeys.moreAction_createdAt.tr( args: [dateFormat.formatDate(createdAt!, true, timeFormat)], ), + fontSize: 11, maxLines: 2, color: Theme.of(context).hintColor, ), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart index b3326d0e60..da22674152 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart @@ -1,13 +1,28 @@ -import 'dart:math'; import 'dart:async'; -import 'package:async/async.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/widget/mouse_hover_builder.dart'; +import 'dart:math'; + import 'package:flutter/material.dart'; + +import 'package:async/async.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/widget/mouse_hover_builder.dart'; import 'package:styled_widget/styled_widget.dart'; class StyledScrollbar extends StatefulWidget { + const StyledScrollbar({ + super.key, + this.size, + required this.axis, + required this.controller, + this.onDrag, + this.contentSize, + this.showTrack = false, + this.autoHideScrollbar = true, + this.handleColor, + this.trackColor, + }); + final double? size; final Axis axis; final ScrollController controller; @@ -22,18 +37,6 @@ class StyledScrollbar extends StatefulWidget { // https://stackoverflow.com/questions/60855712/flutter-how-to-force-scrollcontroller-to-recalculate-position-maxextents final double? contentSize; - const StyledScrollbar( - {super.key, - this.size, - required this.axis, - required this.controller, - this.onDrag, - this.contentSize, - this.showTrack = false, - this.autoHideScrollbar = true, - this.handleColor, - this.trackColor}); - @override ScrollbarState createState() => ScrollbarState(); } @@ -46,25 +49,29 @@ class ScrollbarState extends State { @override void initState() { super.initState(); - widget.controller.addListener(() => setState(() {})); - - widget.controller.position.isScrollingNotifier.addListener( - _hideScrollbarInTime, - ); + widget.controller.addListener(_onScrollChanged); + widget.controller.position.isScrollingNotifier + .addListener(_hideScrollbarInTime); } @override - void didUpdateWidget(StyledScrollbar oldWidget) { - if (oldWidget.contentSize != widget.contentSize) setState(() {}); - super.didUpdateWidget(oldWidget); + void dispose() { + if (widget.controller.hasClients) { + widget.controller.removeListener(_onScrollChanged); + widget.controller.position.isScrollingNotifier + .removeListener(_hideScrollbarInTime); + } + super.dispose(); } + void _onScrollChanged() => setState(() {}); + @override Widget build(BuildContext context) { return LayoutBuilder( builder: (_, BoxConstraints constraints) { double maxExtent; - final contentSize = widget.contentSize; + final double? contentSize = widget.contentSize; switch (widget.axis) { case Axis.vertical: @@ -109,11 +116,9 @@ class ScrollbarState extends State { } // Hide the handle if content is < the viewExtent - var showHandle = contentExtent > _viewExtent && contentExtent > 0; - - if (hideHandler) { - showHandle = false; - } + var showHandle = hideHandler + ? false + : contentExtent > _viewExtent && contentExtent > 0; // Handle color var handleColor = widget.handleColor ?? @@ -184,7 +189,7 @@ class ScrollbarState extends State { if (!widget.controller.position.isScrollingNotifier.value) { _hideScrollbarOperation = CancelableOperation.fromFuture( - Future.delayed(const Duration(seconds: 2), () {}), + Future.delayed(const Duration(seconds: 2)), ).then((_) { hideHandler = true; if (mounted) { @@ -216,17 +221,6 @@ class ScrollbarState extends State { } class ScrollbarListStack extends StatelessWidget { - final double barSize; - final Axis axis; - final Widget child; - final ScrollController controller; - final double? contentSize; - final EdgeInsets? scrollbarPadding; - final Color? handleColor; - final Color? trackColor; - final bool showTrack; - final bool autoHideScrollbar; - const ScrollbarListStack({ super.key, required this.barSize, @@ -239,20 +233,37 @@ class ScrollbarListStack extends StatelessWidget { this.autoHideScrollbar = true, this.trackColor, this.showTrack = false, + this.includeInsets = true, }); + final double barSize; + final Axis axis; + final Widget child; + final ScrollController controller; + final double? contentSize; + final EdgeInsets? scrollbarPadding; + final Color? handleColor; + final Color? trackColor; + final bool showTrack; + final bool autoHideScrollbar; + final bool includeInsets; + @override Widget build(BuildContext context) { return Stack( children: [ - /// LIST - /// Wrap with a bit of padding on the right - child.padding( - right: axis == Axis.vertical ? barSize + Insets.m : 0, - bottom: axis == Axis.horizontal ? barSize + Insets.m : 0, + /// Wrap with a bit of padding on the right or bottom to make room for the scrollbar + Padding( + padding: !includeInsets + ? EdgeInsets.zero + : EdgeInsets.only( + right: axis == Axis.vertical ? barSize + Insets.m : 0, + bottom: axis == Axis.horizontal ? barSize + Insets.m : 0, + ), + child: child, ), - /// SCROLLBAR + /// Display the scrollbar Padding( padding: scrollbarPadding ?? EdgeInsets.zero, child: StyledScrollbar( @@ -266,7 +277,7 @@ class ScrollbarListStack extends StatelessWidget { showTrack: showTrack, ), ) - // The animate will be used by the children that using styled_widget. + // The animate will be used by the children that are using styled_widget. .animate(const Duration(milliseconds: 250), Curves.easeOut), ], ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart index 664dbc7ed1..b1b7c8afc7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart @@ -4,17 +4,6 @@ import 'styled_list.dart'; import 'styled_scroll_bar.dart'; class StyledSingleChildScrollView extends StatefulWidget { - final double? contentSize; - final Axis axis; - final Color? trackColor; - final Color? handleColor; - final ScrollController? controller; - final EdgeInsets? scrollbarPadding; - final double barSize; - final bool autoHideScrollbar; - - final Widget? child; - const StyledSingleChildScrollView({ super.key, required this.child, @@ -26,8 +15,20 @@ class StyledSingleChildScrollView extends StatefulWidget { this.scrollbarPadding, this.barSize = 8, this.autoHideScrollbar = true, + this.includeInsets = true, }); + final Widget? child; + final double? contentSize; + final Axis axis; + final Color? trackColor; + final Color? handleColor; + final ScrollController? controller; + final EdgeInsets? scrollbarPadding; + final double barSize; + final bool autoHideScrollbar; + final bool includeInsets; + @override State createState() => StyledSingleChildScrollViewState(); @@ -35,13 +36,8 @@ class StyledSingleChildScrollView extends StatefulWidget { class StyledSingleChildScrollViewState extends State { - late ScrollController scrollController; - - @override - void initState() { - scrollController = widget.controller ?? ScrollController(); - super.initState(); - } + late final ScrollController scrollController = + widget.controller ?? ScrollController(); @override void dispose() { @@ -51,14 +47,6 @@ class StyledSingleChildScrollViewState super.dispose(); } - @override - void didUpdateWidget(StyledSingleChildScrollView oldWidget) { - if (oldWidget.child != widget.child) { - setState(() {}); - } - super.didUpdateWidget(oldWidget); - } - @override Widget build(BuildContext context) { return ScrollbarListStack( @@ -70,6 +58,7 @@ class StyledSingleChildScrollViewState barSize: widget.barSize, trackColor: widget.trackColor, handleColor: widget.handleColor, + includeInsets: widget.includeInsets, child: SingleChildScrollView( scrollDirection: widget.axis, physics: StyledScrollPhysics(), @@ -81,13 +70,6 @@ class StyledSingleChildScrollViewState } class StyledCustomScrollView extends StatefulWidget { - final Axis axis; - final Color? trackColor; - final Color? handleColor; - final ScrollController? verticalController; - final List slivers; - final double barSize; - const StyledCustomScrollView({ super.key, this.axis = Axis.vertical, @@ -98,32 +80,20 @@ class StyledCustomScrollView extends StatefulWidget { this.barSize = 8, }); + final Axis axis; + final Color? trackColor; + final Color? handleColor; + final ScrollController? verticalController; + final List slivers; + final double barSize; + @override StyledCustomScrollViewState createState() => StyledCustomScrollViewState(); } class StyledCustomScrollViewState extends State { - late ScrollController controller; - - @override - void initState() { - controller = widget.verticalController ?? ScrollController(); - - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - void didUpdateWidget(StyledCustomScrollView oldWidget) { - if (oldWidget.slivers != widget.slivers) { - setState(() {}); - } - super.didUpdateWidget(oldWidget); - } + late final ScrollController controller = + widget.verticalController ?? ScrollController(); @override Widget build(BuildContext context) { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 2e60ac8be7..fb50bf0998 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -93,7 +93,7 @@ "moreOptions": "More options", "wordCount": "Word count: {}", "charCount": "Character count: {}", - "createdAt": "Created at: {}", + "createdAt": "Created: {}", "deleteView": "Delete", "duplicateView": "Duplicate" }, @@ -743,9 +743,9 @@ "sum": "Sum", "count": "Count", "countEmpty": "Count empty", - "countEmptyShort": "Empty", - "countNonEmpty": "Count non empty", - "countNonEmptyShort": "Not empty" + "countEmptyShort": "EMPTY", + "countNonEmpty": "Count not empty", + "countNonEmptyShort": "FILLED" } }, "document": { @@ -1051,7 +1051,8 @@ "name": "Calendar settings" }, "referencedCalendarPrefix": "View of", - "quickJumpYear": "Jump to" + "quickJumpYear": "Jump to", + "duplicateEvent": "Duplicate event" }, "errorDialog": { "title": "AppFlowy Error", diff --git a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs index c45b567640..8137b905d9 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs @@ -116,8 +116,13 @@ impl CalculationType { | CalculationType::Sum => { matches!(field_type, FieldType::Number) }, + // Exclude some fields from CountNotEmpty & CountEmpty + CalculationType::CountEmpty | CalculationType::CountNonEmpty => !matches!( + field_type, + FieldType::URL | FieldType::Checkbox | FieldType::CreatedTime | FieldType::LastEditedTime + ), // All fields - CalculationType::Count | CalculationType::CountEmpty | CalculationType::CountNonEmpty => true, + CalculationType::Count => true, } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs index dda8a68f3e..dad31a12f0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs @@ -27,8 +27,8 @@ impl CalculationsService { CalculationType::Min => self.calculate_min(field, row_cells), CalculationType::Sum => self.calculate_sum(field, row_cells), CalculationType::Count => self.calculate_count(row_cells), - CalculationType::CountEmpty => self.calculate_count_empty(row_cells), - CalculationType::CountNonEmpty => self.calculate_count_non_empty(row_cells), + CalculationType::CountEmpty => self.calculate_count_empty(field, row_cells), + CalculationType::CountNonEmpty => self.calculate_count_non_empty(field, row_cells), } } @@ -129,34 +129,51 @@ impl CalculationsService { } } - fn calculate_count_empty(&self, row_cells: Vec>) -> String { - if !row_cells.is_empty() { - format!( - "{}", - row_cells - .iter() - .filter(|c| c.is_none()) - .collect::>() - .len() - ) - } else { - String::new() + fn calculate_count_empty(&self, field: &Field, row_cells: Vec>) -> String { + let field_type = FieldType::from(field.field_type); + if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None) + .get_type_option_cell_data_handler(&field_type) + { + if !row_cells.is_empty() { + return format!( + "{}", + row_cells + .iter() + .filter(|c| c.is_none() + || handler + .handle_stringify_cell(&c.cell.clone().unwrap_or_default(), &field_type, field) + .is_empty()) + .collect::>() + .len() + ); + } } + + String::new() } - fn calculate_count_non_empty(&self, row_cells: Vec>) -> String { - if !row_cells.is_empty() { - format!( - "{}", - row_cells - .iter() - .filter(|c| c.is_some()) - .collect::>() - .len() - ) - } else { - String::new() + fn calculate_count_non_empty(&self, field: &Field, row_cells: Vec>) -> String { + let field_type = FieldType::from(field.field_type); + if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None) + .get_type_option_cell_data_handler(&field_type) + { + if !row_cells.is_empty() { + return format!( + "{}", + row_cells + .iter() + // Check the Cell has data and that the stringified version is not empty + .filter(|c| c.is_some() + && !handler + .handle_stringify_cell(&c.cell.clone().unwrap_or_default(), &field_type, field) + .is_empty()) + .collect::>() + .len() + ); + } } + + String::new() } fn reduce_values_f64(&self, field: &Field, row_cells: Vec>, f: F) -> T