Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import 'package:provider/provider.dart';
Expand All @@ -37,6 +38,7 @@ import 'widgets/footer/grid_footer.dart';
import 'widgets/header/grid_header.dart';
import 'widgets/row/row.dart';
import 'widgets/shortcuts.dart';
import 'widgets/selection_controller.dart';

class ToggleExtensionNotifier extends ChangeNotifier {
bool _isToggled = false;
Expand Down Expand Up @@ -131,9 +133,14 @@ class _GridPageState extends State<GridPage> {
shrinkWrapped: widget.shrinkWrap,
)..add(const GridEvent.initial());

late final GridSelectionController selectionController = GridSelectionController(
getRowInfos: () => gridBloc.state.rowInfos,
);

@override
void dispose() {
gridBloc.close();
selectionController.dispose();
super.dispose();
}

Expand All @@ -149,35 +156,38 @@ class _GridPageState extends State<GridPage> {
..add(PageAccessLevelEvent.initial()),
),
],
child: BlocListener<ActionNavigationBloc, ActionNavigationState>(
listener: (context, state) {
final action = state.action;
if (action?.type == ActionType.openRow &&
action?.objectId == widget.view.id) {
final rowId = action!.arguments?[ActionArgumentKeys.rowId];
if (rowId != null) {
// If Reminder in existing database is pressed
// then open the row
_openRow(context, rowId);
child: ChangeNotifierProvider<GridSelectionController>.value(
value: selectionController,
child: BlocListener<ActionNavigationBloc, ActionNavigationState>(
listener: (context, state) {
final action = state.action;
if (action?.type == ActionType.openRow &&
action?.objectId == widget.view.id) {
final rowId = action!.arguments?[ActionArgumentKeys.rowId];
if (rowId != null) {
// If Reminder in existing database is pressed
// then open the row
_openRow(context, rowId);
}
}
}
},
child: BlocConsumer<GridBloc, GridState>(
listener: listener,
builder: (context, state) => state.loadingState.map(
idle: (_) => const SizedBox.shrink(),
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
finish: (result) => result.successOrFail.fold(
(_) => GridShortcuts(
child: GridPageContent(
key: ValueKey(widget.view.id),
view: widget.view,
shrinkWrap: widget.shrinkWrap,
},
child: BlocConsumer<GridBloc, GridState>(
listener: listener,
builder: (context, state) => state.loadingState.map(
idle: (_) => const SizedBox.shrink(),
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
finish: (result) => result.successOrFail.fold(
(_) => GridShortcuts(
child: GridPageContent(
key: ValueKey(widget.view.id),
view: widget.view,
shrinkWrap: widget.shrinkWrap,
),
),
(err) => Center(child: AppFlowyErrorPage(error: err)),
),
(err) => Center(child: AppFlowyErrorPage(error: err)),
),
),
),
Expand Down Expand Up @@ -363,6 +373,7 @@ class _GridRows extends StatefulWidget {
class _GridRowsState extends State<_GridRows> {
bool showFloatingCalculations = false;
bool isAtBottom = false;
int? _dragStartRowIndex;

@override
void initState() {
Expand Down Expand Up @@ -455,6 +466,57 @@ class _GridRowsState extends State<_GridRows> {
);
}

final horizontalPadding = context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding;
final compactMode = context.read<GridBloc>().databaseController.compactModeNotifier.value;
final rowHeight = compactMode ? GridSize.compactRowHeight : GridSize.rowHeight;

child = Listener(
onPointerDown: (PointerDownEvent event) {
final localX = event.localPosition.dx;
final localY = event.localPosition.dy;
if (localX >= paddingLeft && localX <= paddingLeft + horizontalPadding) {
final scrollOffset = widget.scrollController.verticalController.offset;
final absoluteY = localY + scrollOffset;
final clickedRowIndex = (absoluteY ~/ rowHeight);

final selection = context.read<GridSelectionController>();
final rowInfos = selection.getRowInfos();
if (clickedRowIndex >= 0 && clickedRowIndex < rowInfos.length) {
final isShift = HardwareKeyboard.instance.isShiftPressed;
final isMeta = HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed;

_dragStartRowIndex = clickedRowIndex;
selection.selectRow(
rowInfos[clickedRowIndex].rowId,
isMultiSelect: isMeta,
isRangeSelect: isShift,
);
}
}
},
onPointerMove: (PointerMoveEvent event) {
if (_dragStartRowIndex != null) {
final localY = event.localPosition.dy;
final scrollOffset = widget.scrollController.verticalController.offset;
final absoluteY = localY + scrollOffset;
final currentRowIndex = (absoluteY ~/ rowHeight);

final selection = context.read<GridSelectionController>();
final rowInfos = selection.getRowInfos();
if (rowInfos.isEmpty) return;
final clampedIndex = currentRowIndex.clamp(0, rowInfos.length - 1).toInt();
selection.selectRange(_dragStartRowIndex!, clampedIndex);
}
},
onPointerUp: (PointerUpEvent event) {
_dragStartRowIndex = null;
},
onPointerCancel: (PointerCancelEvent event) {
_dragStartRowIndex = null;
},
child: child,
);

if (widget.shrinkWrap) {
return child;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ class GridSize {

static double get footerHeight => 36 * scale;

static double get rowHeight => 36 * scale;

static double get compactRowHeight => 32 * scale;

static double get horizontalHeaderPadding =>
UniversalPlatform.isDesktop ? 40 * scale : 16 * scale;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import '../selection_controller.dart';


class RowActionMenu extends StatelessWidget {
const RowActionMenu({
Expand Down Expand Up @@ -132,11 +135,21 @@ enum RowAction {
RowBackendService.duplicateRow(viewId, rowId);
break;
case delete:
final selection = context.read<GridSelectionController>();
final rowsToDelete = selection.isSelected(rowId)
? selection.selectedRowIds.toList()
: [rowId];

showConfirmDeletionDialog(
context: context,
name: LocaleKeys.grid_row_label.tr(),
description: LocaleKeys.grid_row_deleteRowPrompt.tr(),
onConfirm: () => RowBackendService.deleteRows(viewId, [rowId]),
onConfirm: () {
RowBackendService.deleteRows(viewId, rowsToDelete);
if (selection.isSelected(rowId)) {
selection.clearSelection();
}
},
);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import '../../../../widgets/row/accessory/cell_accessory.dart';
import '../../../../widgets/row/cells/cell_container.dart';
import '../../layout/sizes.dart';
import 'action.dart';
import '../selection_controller.dart';


class GridRow extends StatelessWidget {
const GridRow({
Expand Down Expand Up @@ -81,7 +83,18 @@ class GridRow extends StatelessWidget {
);
}

return rowContent;
return Consumer<GridSelectionController>(
builder: (context, selection, child) {
final isSelected = selection.isSelected(rowId);
return Container(
color: isSelected
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.12)
: Colors.transparent,
child: child,
);
},
child: rowContent,
);
}
}

Expand Down Expand Up @@ -111,8 +124,13 @@ class _RowLeadingState extends State<_RowLeading> {
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
popupBuilder: (_) {
final bloc = context.read<RowBloc>();
return BlocProvider.value(
value: context.read<GridBloc>(),
return MultiProvider(
providers: [
BlocProvider.value(value: context.read<GridBloc>()),
ChangeNotifierProvider<GridSelectionController>.value(
value: context.read<GridSelectionController>(),
),
],
child: RowActionMenu(
viewId: bloc.viewId,
rowId: bloc.rowId,
Expand Down Expand Up @@ -313,7 +331,7 @@ class RowContent extends StatelessWidget {
builder: (context, compactMode, _) {
return Container(
width: GridSize.newPropertyButtonWidth,
constraints: BoxConstraints(minHeight: compactMode ? 32 : 36),
constraints: BoxConstraints(minHeight: compactMode ? GridSize.compactRowHeight : GridSize.rowHeight),
decoration: BoxDecoration(
border: Border(
bottom:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'dart:collection';

import 'package:flutter/widgets.dart';
import 'package:appflowy/plugins/database/application/row/row_cache.dart';

class GridSelectionController extends ChangeNotifier {
GridSelectionController({required this.getRowInfos});

final List<RowInfo> Function() getRowInfos;
final Set<String> _selectedRowIds = {};
String? _lastSelectedRowId;

UnmodifiableSetView<String> get selectedRowIds =>
UnmodifiableSetView(_selectedRowIds);
bool get hasSelection => _selectedRowIds.isNotEmpty;

bool isSelected(String rowId) => _selectedRowIds.contains(rowId);

void selectRow(
String rowId, {
bool isMultiSelect = false,
bool isRangeSelect = false,
}) {
if (isRangeSelect && _lastSelectedRowId != null) {
final rowInfos = getRowInfos();
final startIndex =
rowInfos.indexWhere((r) => r.rowId == _lastSelectedRowId);
final endIndex = rowInfos.indexWhere((r) => r.rowId == rowId);
if (startIndex != -1 && endIndex != -1) {
_applyRange(startIndex, endIndex, clearFirst: !isMultiSelect);
}
// Don't update _lastSelectedRowId on range select so subsequent
// shift-clicks extend from the original anchor.
} else if (isMultiSelect) {
if (_selectedRowIds.contains(rowId)) {
_selectedRowIds.remove(rowId);
} else {
_selectedRowIds.add(rowId);
}
_lastSelectedRowId = rowId;
} else {
_selectedRowIds.clear();
_selectedRowIds.add(rowId);
_lastSelectedRowId = rowId;
}
notifyListeners();
}

void selectRange(int startIndex, int endIndex) {
_applyRange(startIndex, endIndex, clearFirst: true);
// Update _lastSelectedRowId to the end of the range so subsequent
// shift-clicks anchor correctly after a drag.
final rowInfos = getRowInfos();
final clampedEnd = endIndex.clamp(0, rowInfos.length - 1);
if (rowInfos.isNotEmpty) {
_lastSelectedRowId = rowInfos[clampedEnd].rowId;
}
notifyListeners();
}

void clearSelection() {
if (_selectedRowIds.isNotEmpty) {
_selectedRowIds.clear();
_lastSelectedRowId = null;
notifyListeners();
}
}

void selectAll() {
final rowInfos = getRowInfos();
_selectedRowIds.clear();
_selectedRowIds.addAll(rowInfos.map((r) => r.rowId));
if (rowInfos.isNotEmpty) {
_lastSelectedRowId = rowInfos.first.rowId;
}
notifyListeners();
}

/// Shared helper that selects all rows between [startIndex] and [endIndex].
void _applyRange(int startIndex, int endIndex, {required bool clearFirst}) {
final rowInfos = getRowInfos();
final start = startIndex < endIndex ? startIndex : endIndex;
final end = startIndex < endIndex ? endIndex : startIndex;

if (clearFirst) {
_selectedRowIds.clear();
}
for (int i = start; i <= end; i++) {
if (i >= 0 && i < rowInfos.length) {
_selectedRowIds.add(rowInfos[i].rowId);
}
}
}
}
Loading
Loading