mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: mobile board view (#4030)
This commit is contained in:
parent
64aa2ba7e4
commit
6b4d4fef15
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
|
@ -0,0 +1,4 @@
|
||||
export 'mobile_board_screen.dart';
|
||||
export 'mobile_board_content.dart';
|
||||
export 'widgets/mobile_hidden_groups_column.dart';
|
||||
export 'widgets/mobile_board_trailing.dart';
|
@ -0,0 +1,284 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/board.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
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:go_router/go_router.dart';
|
||||
|
||||
class MobileBoardContent extends StatefulWidget {
|
||||
const MobileBoardContent({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileBoardContent> createState() => _MobileBoardContentState();
|
||||
}
|
||||
|
||||
class _MobileBoardContentState extends State<MobileBoardContent> {
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
late final ScrollController scrollController;
|
||||
late final AppFlowyBoardScrollController scrollManager;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//mobile may not need this
|
||||
//scroll to bottom when add a new card
|
||||
scrollManager = AppFlowyBoardScrollController();
|
||||
scrollController = ScrollController();
|
||||
renderHook.addSelectOptionHook((options, groupId, _) {
|
||||
// The cell should hide if the option id is equal to the groupId.
|
||||
final isInGroup =
|
||||
options.where((element) => element.id == groupId).isNotEmpty;
|
||||
|
||||
if (isInGroup || options.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final config = AppFlowyBoardConfig(
|
||||
groupCornerRadius: 8,
|
||||
groupBackgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
groupMargin: const EdgeInsets.fromLTRB(4, 8, 4, 12),
|
||||
groupHeaderPadding: const EdgeInsets.all(8),
|
||||
groupBodyPadding: const EdgeInsets.all(4),
|
||||
groupFooterPadding: const EdgeInsets.all(8),
|
||||
cardMargin: const EdgeInsets.all(4),
|
||||
);
|
||||
|
||||
return BlocListener<BoardBloc, BoardState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.recentAddedRowMeta != current.recentAddedRowMeta,
|
||||
listener: (context, state) {
|
||||
// when add a new card
|
||||
// it push to the card detail screen of the new card
|
||||
final rowCache = context.read<BoardBloc>().getRowCache()!;
|
||||
context.push(
|
||||
MobileCardDetailScreen.routeName,
|
||||
extra: {
|
||||
MobileCardDetailScreen.argRowController: RowController(
|
||||
rowMeta: state.recentAddedRowMeta!,
|
||||
viewId: state.viewId,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
MobileCardDetailScreen.argFieldController:
|
||||
context.read<BoardBloc>().fieldController,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final showCreateGroupButton =
|
||||
context.read<BoardBloc>().groupingFieldType.canCreateNewGroup;
|
||||
final showHiddenGroups = state.hiddenGroups.isNotEmpty;
|
||||
return AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: scrollController,
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: BoxConstraints.tightFor(width: screenWidth * 0.7),
|
||||
config: config,
|
||||
leading: showHiddenGroups
|
||||
? MobileHiddenGroupsColumn(padding: config.groupHeaderPadding)
|
||||
: const HSpace(16),
|
||||
trailing: showCreateGroupButton
|
||||
? const MobileBoardTrailing()
|
||||
: const HSpace(16),
|
||||
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
|
||||
value: context.read<BoardBloc>(),
|
||||
child: GroupCardHeader(
|
||||
groupData: groupData,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context: context,
|
||||
afGroupData: column,
|
||||
afGroupItem: columnItem,
|
||||
cardMargin: config.cardMargin,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
final style = Theme.of(context);
|
||||
|
||||
return SizedBox(
|
||||
height: 42,
|
||||
width: double.infinity,
|
||||
child: TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.add_m,
|
||||
color: style.colorScheme.onSurface,
|
||||
),
|
||||
label: Text(
|
||||
LocaleKeys.board_column_createNewCard.tr(),
|
||||
style: style.textTheme.bodyMedium?.copyWith(
|
||||
color: style.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.createBottomRow(columnData.id)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard({
|
||||
required BuildContext context,
|
||||
required AppFlowyGroupData afGroupData,
|
||||
required AppFlowyGroupItem afGroupItem,
|
||||
required EdgeInsets cardMargin,
|
||||
}) {
|
||||
final boardBloc = context.read<BoardBloc>();
|
||||
final groupItem = afGroupItem as GroupItem;
|
||||
final groupData = afGroupData.customData as GroupData;
|
||||
final rowMeta = groupItem.row;
|
||||
final rowCache = boardBloc.getRowCache();
|
||||
|
||||
/// Return placeholder widget if the rowCache is null.
|
||||
if (rowCache == null) return SizedBox.shrink(key: ObjectKey(groupItem));
|
||||
final cellCache = rowCache.cellCache;
|
||||
final fieldController = boardBloc.fieldController;
|
||||
final viewId = boardBloc.viewId;
|
||||
|
||||
final cellBuilder = CardCellBuilder<String>(cellCache);
|
||||
final isEditing = boardBloc.state.isEditingRow &&
|
||||
boardBloc.state.editingRow?.row.id == groupItem.row.id;
|
||||
|
||||
final groupItemId = groupItem.row.id + groupData.group.groupId;
|
||||
|
||||
return Container(
|
||||
key: ValueKey(groupItemId),
|
||||
margin: cardMargin,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: RowCard<String>(
|
||||
rowMeta: rowMeta,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
cardData: groupData.group.groupId,
|
||||
groupingFieldId: groupItem.fieldInfo.id,
|
||||
groupId: groupData.group.groupId,
|
||||
isEditing: isEditing,
|
||||
cellBuilder: cellBuilder,
|
||||
renderHook: renderHook,
|
||||
openCard: (context) => _openCard(
|
||||
context: context,
|
||||
viewId: viewId,
|
||||
groupId: groupData.group.groupId,
|
||||
fieldController: fieldController,
|
||||
rowMeta: rowMeta,
|
||||
rowCache: rowCache,
|
||||
),
|
||||
onStartEditing: () => boardBloc
|
||||
.add(BoardEvent.startEditingRow(groupData.group, groupItem.row)),
|
||||
onEndEditing: () =>
|
||||
boardBloc.add(BoardEvent.endEditingRow(groupItem.row.id)),
|
||||
styleConfiguration: const RowCardStyleConfiguration(
|
||||
showAccessory: false,
|
||||
hoverStyle: null,
|
||||
cardPadding: EdgeInsets.all(8),
|
||||
cellPadding: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
||||
return BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
// card shadow
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _openCard({
|
||||
required BuildContext context,
|
||||
required String viewId,
|
||||
required String groupId,
|
||||
required FieldController fieldController,
|
||||
required RowMetaPB rowMeta,
|
||||
required RowCache rowCache,
|
||||
}) {
|
||||
final rowInfo = RowInfo(
|
||||
viewId: viewId,
|
||||
fields: UnmodifiableListView(fieldController.fieldInfos),
|
||||
rowMeta: rowMeta,
|
||||
rowId: rowMeta.id,
|
||||
);
|
||||
|
||||
final dataController = RowController(
|
||||
rowMeta: rowInfo.rowMeta,
|
||||
viewId: rowInfo.viewId,
|
||||
rowCache: rowCache,
|
||||
groupId: groupId,
|
||||
);
|
||||
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
// To avoid the appbar of [MobileCardDetailScreen] being covered by status bar.
|
||||
useSafeArea: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
builder: (context) => DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.4,
|
||||
minChildSize: 0.4,
|
||||
snapSizes: const [0.4, 1],
|
||||
snap: true,
|
||||
builder: (context, scrollController) {
|
||||
return MobileCardDetailScreen(
|
||||
fieldController: fieldController,
|
||||
rowController: dataController,
|
||||
scrollController: scrollController,
|
||||
isBottomSheet: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
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:go_router/go_router.dart';
|
||||
|
||||
// similar to [BoardColumnHeader] in Desktop
|
||||
class GroupCardHeader extends StatefulWidget {
|
||||
const GroupCardHeader({
|
||||
super.key,
|
||||
required this.groupData,
|
||||
});
|
||||
|
||||
final AppFlowyGroupData groupData;
|
||||
|
||||
@override
|
||||
State<GroupCardHeader> createState() => _GroupCardHeaderState();
|
||||
}
|
||||
|
||||
class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
late final TextEditingController _controller =
|
||||
TextEditingController.fromValue(
|
||||
TextEditingValue(
|
||||
selection: TextSelection.collapsed(
|
||||
offset: widget.groupData.headerData.groupName.length,
|
||||
),
|
||||
text: widget.groupData.headerData.groupName,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final boardCustomData = widget.groupData.customData as GroupData;
|
||||
final titleTextStyle = Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
Widget title = Text(
|
||||
widget.groupData.headerData.groupName,
|
||||
style: titleTextStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
|
||||
// header can be edited if it's not default group(no status) and the field type can be edited
|
||||
if (!boardCustomData.group.isDefault &&
|
||||
boardCustomData.fieldType.canEditHeader) {
|
||||
title = GestureDetector(
|
||||
onTap: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.startEditingHeader(widget.groupData.id)),
|
||||
child: Text(
|
||||
widget.groupData.headerData.groupName,
|
||||
style: titleTextStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.isEditingHeader &&
|
||||
state.editingHeaderId == widget.groupData.id) {
|
||||
title = TextField(
|
||||
controller: _controller,
|
||||
autofocus: true,
|
||||
onEditingComplete: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.endEditingHeader(
|
||||
widget.groupData.id,
|
||||
_controller.text,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
style: titleTextStyle,
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: SizedBox(
|
||||
height: 42,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildHeaderIcon(boardCustomData),
|
||||
Expanded(child: title),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.more_horiz_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
splashRadius: 5,
|
||||
onPressed: () => showFlowyMobileBottomSheet(
|
||||
context,
|
||||
title: LocaleKeys.board_column_groupActions.tr(),
|
||||
builder: (_) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
svg: FlowySvgs.edit_s,
|
||||
text: LocaleKeys.board_column_renameColumn.tr(),
|
||||
onTap: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.startEditingHeader(
|
||||
widget.groupData.id,
|
||||
),
|
||||
);
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: BottomSheetActionWidget(
|
||||
svg: FlowySvgs.hide_s,
|
||||
text: LocaleKeys.board_column_hideColumn.tr(),
|
||||
onTap: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
widget.groupData.customData.group
|
||||
as GroupPB,
|
||||
false,
|
||||
),
|
||||
);
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
splashRadius: 5,
|
||||
onPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.createHeaderRow(widget.groupData.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderIcon(GroupData customData) =>
|
||||
switch (customData.fieldType) {
|
||||
FieldType.Checkbox => FlowySvg(
|
||||
customData.asCheckboxGroup()!.isCheck
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Add new group
|
||||
class MobileBoardTrailing extends StatefulWidget {
|
||||
const MobileBoardTrailing({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MobileBoardTrailing> createState() => _MobileBoardTrailingState();
|
||||
}
|
||||
|
||||
class _MobileBoardTrailingState extends State<MobileBoardTrailing> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
|
||||
bool isEditing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final style = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: SizedBox(
|
||||
width: screenSize.width * 0.7,
|
||||
child: isEditing
|
||||
? DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: style.colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _textController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: style.colorScheme.onBackground,
|
||||
),
|
||||
onPressed: () => _textController.clear(),
|
||||
),
|
||||
isDense: true,
|
||||
),
|
||||
onEditingComplete: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createGroup(
|
||||
_textController.text,
|
||||
),
|
||||
);
|
||||
_textController.clear();
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
style: style.textTheme.titleSmall?.copyWith(
|
||||
color: style.colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text(
|
||||
LocaleKeys.button_add.tr(),
|
||||
style: style.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: style.colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createGroup(
|
||||
_textController.text,
|
||||
),
|
||||
);
|
||||
_textController.clear();
|
||||
setState(() {
|
||||
isEditing = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: style.colorScheme.onBackground,
|
||||
backgroundColor: style.colorScheme.secondary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(
|
||||
LocaleKeys.board_column_newGroup.tr(),
|
||||
),
|
||||
onPressed: () => setState(
|
||||
() => isEditing = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,323 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
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:go_router/go_router.dart';
|
||||
|
||||
class MobileHiddenGroupsColumn extends StatelessWidget {
|
||||
const MobileHiddenGroupsColumn({super.key, required this.padding});
|
||||
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = context.read<BoardBloc>().databaseController;
|
||||
return BlocSelector<BoardBloc, BoardState, BoardLayoutSettingPB?>(
|
||||
selector: (state) => state.layoutSettings,
|
||||
builder: (context, layoutSettings) {
|
||||
if (layoutSettings == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final isCollapsed = layoutSettings.collapseHiddenGroups;
|
||||
return Container(
|
||||
padding: padding,
|
||||
child: AnimatedSize(
|
||||
alignment: AlignmentDirectional.topStart,
|
||||
curve: Curves.easeOut,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: isCollapsed
|
||||
? SizedBox(
|
||||
height: 50,
|
||||
child: _collapseExpandIcon(context, isCollapsed),
|
||||
)
|
||||
: SizedBox(
|
||||
width: 180,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
_collapseExpandIcon(context, isCollapsed),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
LocaleKeys.board_hiddenGroupSection_sectionTitle.tr(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
Expanded(
|
||||
child: MobileHiddenGroupList(
|
||||
databaseController: databaseController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) {
|
||||
return CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
child: IconButton(
|
||||
icon: FlowySvg(
|
||||
isCollapsed
|
||||
? FlowySvgs.hamburger_s_s
|
||||
: FlowySvgs.pull_left_outlined_s,
|
||||
size: isCollapsed ? const Size.square(12) : const Size.square(40),
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.toggleHiddenSectionVisibility(!isCollapsed)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileHiddenGroupList extends StatelessWidget {
|
||||
const MobileHiddenGroupList({
|
||||
super.key,
|
||||
required this.databaseController,
|
||||
});
|
||||
|
||||
final DatabaseController databaseController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bloc = context.read<BoardBloc>();
|
||||
return BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (_, state) => ReorderableListView.builder(
|
||||
itemCount: state.hiddenGroups.length,
|
||||
itemBuilder: (_, index) => MobileHiddenGroup(
|
||||
key: ValueKey(state.hiddenGroups[index].groupId),
|
||||
group: state.hiddenGroups[index],
|
||||
index: index,
|
||||
bloc: bloc,
|
||||
),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromGroupId = state.hiddenGroups[oldIndex].groupId;
|
||||
final toGroupId = state.hiddenGroups[newIndex].groupId;
|
||||
bloc.add(BoardEvent.reorderGroup(fromGroupId, toGroupId));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileHiddenGroup extends StatelessWidget {
|
||||
const MobileHiddenGroup({
|
||||
super.key,
|
||||
required this.group,
|
||||
required this.index,
|
||||
required this.bloc,
|
||||
});
|
||||
|
||||
final GroupPB group;
|
||||
final BoardBloc bloc;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final databaseController = bloc.databaseController;
|
||||
final primaryField = databaseController.fieldController.fieldInfos
|
||||
.firstWhereOrNull((element) => element.isPrimary)!;
|
||||
|
||||
return BlocProvider<BoardBloc>.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == this.group.groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: EdgeInsets.zero,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
group.groupName,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.hide_m,
|
||||
size: Size.square(20),
|
||||
),
|
||||
),
|
||||
onTap: () => showFlowyMobileConfirmDialog(
|
||||
context,
|
||||
title: LocaleKeys.board_mobile_unhideGroup.tr(),
|
||||
content: LocaleKeys.board_mobile_unhideGroupContent.tr(),
|
||||
actionButtonTitle: LocaleKeys.button_yes.tr(),
|
||||
actionButtonColor: Theme.of(context).colorScheme.primary,
|
||||
onActionButtonPressed: () => context.read<BoardBloc>().add(
|
||||
BoardEvent.toggleGroupVisibility(
|
||||
group,
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
MobileHiddenGroupItemList(
|
||||
bloc: bloc,
|
||||
viewId: databaseController.viewId,
|
||||
groupId: group.groupId,
|
||||
primaryField: primaryField,
|
||||
rowCache: databaseController.rowCache,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileHiddenGroupItemList extends StatelessWidget {
|
||||
const MobileHiddenGroupItemList({
|
||||
required this.bloc,
|
||||
required this.groupId,
|
||||
required this.viewId,
|
||||
required this.primaryField,
|
||||
required this.rowCache,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final BoardBloc bloc;
|
||||
final String groupId;
|
||||
final String viewId;
|
||||
final FieldInfo primaryField;
|
||||
final RowCache rowCache;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<BoardBloc, BoardState>(
|
||||
builder: (context, state) {
|
||||
final group = state.hiddenGroups.firstWhereOrNull(
|
||||
(g) => g.groupId == groupId,
|
||||
);
|
||||
if (group == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final cells = <Widget>[
|
||||
...group.rows.map(
|
||||
(item) {
|
||||
final cellContext = rowCache.loadCells(item)[primaryField.id]!;
|
||||
final rowController = RowController(
|
||||
rowMeta: item,
|
||||
viewId: viewId,
|
||||
rowCache: rowCache,
|
||||
);
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
renderHook.addTextCellHook((cellData, _, __) {
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final text = cellData.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
: cellData;
|
||||
|
||||
if (text.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (!cellContext.rowMeta.isDocumentEmpty) ...[
|
||||
const FlowySvg(FlowySvgs.notes_s),
|
||||
const HSpace(4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
foregroundColor: Theme.of(context).colorScheme.onBackground,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: CardCellBuilder<String>(rowController.cellCache)
|
||||
.buildCell(
|
||||
cellContext: cellContext,
|
||||
renderHook: renderHook,
|
||||
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
|
||||
),
|
||||
onPressed: () {
|
||||
context.push(
|
||||
MobileCardDetailScreen.routeName,
|
||||
extra: {
|
||||
MobileCardDetailScreen.argRowController: rowController,
|
||||
MobileCardDetailScreen.argFieldController:
|
||||
context.read<BoardBloc>().fieldController,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
itemBuilder: (context, index) => cells[index],
|
||||
itemCount: cells.length,
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export 'group_card_header.dart';
|
||||
export 'mobile_board_trailing.dart';
|
||||
export 'mobile_hidden_groups_column.dart';
|
@ -1 +1,2 @@
|
||||
export 'card_detail/mobile_card_detail_screen.dart';
|
||||
export 'card_content/mobile_card_content.dart';
|
||||
|
@ -0,0 +1,9 @@
|
||||
export 'checkbox.dart';
|
||||
export 'date.dart';
|
||||
export 'style.dart';
|
||||
export 'time_stamp.dart';
|
||||
export 'number.dart';
|
||||
export 'text.dart';
|
||||
export 'select_option.dart';
|
||||
export 'url.dart';
|
||||
export 'checklist.dart';
|
@ -0,0 +1,68 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checkbox_cell/checkbox_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileCheckboxCardCell extends CardCell {
|
||||
const MobileCheckboxCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
@override
|
||||
State<MobileCheckboxCardCell> createState() => _CheckboxCellState();
|
||||
}
|
||||
|
||||
class _CheckboxCellState extends State<MobileCheckboxCardCell> {
|
||||
late final CheckboxCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as CheckboxCellController;
|
||||
_cellBloc = CheckboxCellBloc(cellController: cellController)
|
||||
..add(const CheckboxCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<CheckboxCellBloc, CheckboxCellState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.isSelected != current.isSelected,
|
||||
builder: (context, state) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
alignment: Alignment.centerLeft,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: FlowySvg(
|
||||
state.isSelected
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
size: const Size.square(24),
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<CheckboxCellBloc>()
|
||||
.add(const CheckboxCellEvent.select()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||
|
||||
class MobileChecklistCardCell extends CardCell {
|
||||
const MobileChecklistCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
@override
|
||||
State<MobileChecklistCardCell> createState() => _ChecklistCellState();
|
||||
}
|
||||
|
||||
class _ChecklistCellState extends State<MobileChecklistCardCell> {
|
||||
late final ChecklistCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as ChecklistCellController;
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController)
|
||||
..add(const ChecklistCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.tasks.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: cellStyle.padding,
|
||||
child: MobileChecklistProgressBar(
|
||||
tasks: state.tasks,
|
||||
percent: state.percent,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileChecklistProgressBar extends StatefulWidget {
|
||||
const MobileChecklistProgressBar({
|
||||
super.key,
|
||||
required this.tasks,
|
||||
required this.percent,
|
||||
});
|
||||
|
||||
final List<ChecklistSelectOption> tasks;
|
||||
final double percent;
|
||||
final int segmentLimit = 5;
|
||||
|
||||
@override
|
||||
State<MobileChecklistProgressBar> createState() =>
|
||||
_MobileChecklistProgresssBarState();
|
||||
}
|
||||
|
||||
class _MobileChecklistProgresssBarState
|
||||
extends State<MobileChecklistProgressBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
final numFinishedTasks = widget.tasks.where((e) => e.isSelected).length;
|
||||
final completedTaskColor = numFinishedTasks == widget.tasks.length
|
||||
? AFThemeExtension.of(context).success
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.tasks.isNotEmpty &&
|
||||
widget.tasks.length <= widget.segmentLimit)
|
||||
...List<Widget>.generate(
|
||||
widget.tasks.length,
|
||||
(index) => Flexible(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(2)),
|
||||
color: index < numFinishedTasks
|
||||
? completedTaskColor
|
||||
: AFThemeExtension.of(context).progressBarBGColor,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 1),
|
||||
height: 6.0,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: LinearPercentIndicator(
|
||||
lineHeight: 6.0,
|
||||
percent: widget.percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: completedTaskColor,
|
||||
backgroundColor:
|
||||
AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
"${(widget.percent * 100).round()}%",
|
||||
style: cellStyle.secondaryTextStyle(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileDateCardCell<CustomCardData> extends CardCell {
|
||||
const MobileDateCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
this.renderHook,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<dynamic, CustomCardData>? renderHook;
|
||||
|
||||
@override
|
||||
State<MobileDateCardCell> createState() => _DateCellState();
|
||||
}
|
||||
|
||||
class _DateCellState extends State<MobileDateCardCell> {
|
||||
late final DateCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as DateCellController;
|
||||
|
||||
_cellBloc = DateCellBloc(cellController: cellController)
|
||||
..add(const DateCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<DateCellBloc, DateCellState>(
|
||||
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
||||
builder: (context, state) {
|
||||
if (state.dateStr.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.data,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: cellStyle.padding,
|
||||
child: Text(
|
||||
state.dateStr,
|
||||
style: cellStyle.secondaryTextStyle(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/number_cell/number_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileNumberCardCell<CustomCardData> extends CardCell {
|
||||
const MobileNumberCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
CustomCardData? cardData,
|
||||
this.renderHook,
|
||||
});
|
||||
|
||||
final CellRenderHook<String, CustomCardData>? renderHook;
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
@override
|
||||
State<MobileNumberCardCell> createState() => _NumberCellState();
|
||||
}
|
||||
|
||||
class _NumberCellState extends State<MobileNumberCardCell> {
|
||||
late final NumberCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as NumberCellController;
|
||||
|
||||
_cellBloc = NumberCellBloc(cellController: cellController)
|
||||
..add(const NumberCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<NumberCellBloc, NumberCellState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.cellContent != current.cellContent,
|
||||
builder: (context, state) {
|
||||
if (state.cellContent.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.cellContent,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: cellStyle.padding,
|
||||
child: Text(
|
||||
state.cellContent,
|
||||
style: cellStyle.primaryTextStyle(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_cell_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileSelectOptionCardCell<CustomCardData> extends CardCell {
|
||||
const MobileSelectOptionCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
required CustomCardData? cardData,
|
||||
this.renderHook,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<List<SelectOptionPB>, CustomCardData>? renderHook;
|
||||
|
||||
@override
|
||||
State<MobileSelectOptionCardCell> createState() => _SelectOptionCellState();
|
||||
}
|
||||
|
||||
class _SelectOptionCellState extends State<MobileSelectOptionCardCell> {
|
||||
late final SelectOptionCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as SelectOptionCellController;
|
||||
_cellBloc = SelectOptionCellBloc(cellController: cellController)
|
||||
..add(const SelectOptionCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
buildWhen: (previous, current) {
|
||||
return previous.selectedOptions != current.selectedOptions;
|
||||
},
|
||||
builder: (context, state) {
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.selectedOptions,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
final children = state.selectedOptions
|
||||
.map(
|
||||
(option) => MobileSelectOptionTag.fromOption(
|
||||
context: context,
|
||||
option: option,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return IntrinsicHeight(
|
||||
child: Padding(
|
||||
padding: cellStyle.padding,
|
||||
child: SizedBox.expand(
|
||||
child: Wrap(spacing: 4, runSpacing: 2, children: children),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileSelectOptionTag extends StatelessWidget {
|
||||
const MobileSelectOptionTag({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.color,
|
||||
this.onSelected,
|
||||
this.onRemove,
|
||||
});
|
||||
|
||||
factory MobileSelectOptionTag.fromOption({
|
||||
required BuildContext context,
|
||||
required SelectOptionPB option,
|
||||
VoidCallback? onSelected,
|
||||
Function(String)? onRemove,
|
||||
}) {
|
||||
return MobileSelectOptionTag(
|
||||
name: option.name,
|
||||
color: option.color.toColor(context),
|
||||
onSelected: onSelected,
|
||||
onRemove: onRemove,
|
||||
);
|
||||
}
|
||||
|
||||
final String name;
|
||||
final Color color;
|
||||
final VoidCallback? onSelected;
|
||||
final void Function(String)? onRemove;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
name,
|
||||
style: cellStyle.tagTextStyle(),
|
||||
),
|
||||
),
|
||||
if (onRemove != null) ...[
|
||||
const HSpace(2),
|
||||
IconButton(
|
||||
onPressed: () => onRemove?.call(name),
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.close_s,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MobileCardCellStyle {
|
||||
MobileCardCellStyle(this.context);
|
||||
|
||||
BuildContext context;
|
||||
|
||||
EdgeInsets get padding => const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
);
|
||||
|
||||
TextStyle? primaryTextStyle() {
|
||||
final theme = Theme.of(context);
|
||||
return theme.textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 16,
|
||||
color: theme.colorScheme.onBackground,
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? secondaryTextStyle() {
|
||||
final theme = Theme.of(context);
|
||||
return theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.tertiary,
|
||||
fontSize: 14,
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? tagTextStyle() {
|
||||
final theme = Theme.of(context);
|
||||
return theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onBackground,
|
||||
fontSize: 12,
|
||||
);
|
||||
}
|
||||
|
||||
TextStyle? urlTextStyle() {
|
||||
final theme = Theme.of(context);
|
||||
return theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontSize: 16,
|
||||
decoration: TextDecoration.underline,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileTextCardCell<CustomCardData> extends CardCell {
|
||||
const MobileTextCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
CustomCardData? cardData,
|
||||
this.renderHook,
|
||||
});
|
||||
|
||||
final CellRenderHook<String, CustomCardData>? renderHook;
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
@override
|
||||
State<MobileTextCardCell> createState() => _NumberCellState();
|
||||
}
|
||||
|
||||
class _NumberCellState extends State<MobileTextCardCell> {
|
||||
late final TextCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as TextCellController;
|
||||
|
||||
_cellBloc = TextCellBloc(cellController: cellController)
|
||||
..add(const TextCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<TextCellBloc, TextCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.content,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: cellStyle.padding,
|
||||
child: Text(
|
||||
state.content,
|
||||
style: cellStyle.primaryTextStyle(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileTimestampCardCell<CustomCardData> extends CardCell {
|
||||
const MobileTimestampCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
this.renderHook,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
final CellRenderHook<dynamic, CustomCardData>? renderHook;
|
||||
|
||||
@override
|
||||
State<MobileTimestampCardCell> createState() => _TimestampCellState();
|
||||
}
|
||||
|
||||
class _TimestampCellState extends State<MobileTimestampCardCell> {
|
||||
late final TimestampCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as TimestampCellController;
|
||||
|
||||
_cellBloc = TimestampCellBloc(cellController: cellController)
|
||||
..add(const TimestampCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
|
||||
buildWhen: (previous, current) => previous.dateStr != current.dateStr,
|
||||
builder: (context, state) {
|
||||
if (state.dateStr.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final Widget? custom = widget.renderHook?.call(
|
||||
state.data,
|
||||
widget.cardData,
|
||||
context,
|
||||
);
|
||||
if (custom != null) {
|
||||
return custom;
|
||||
}
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: cellStyle.padding,
|
||||
child: Text(
|
||||
state.dateStr,
|
||||
style: cellStyle.secondaryTextStyle(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/style.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/url_cell/url_cell_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileURLCardCell<CustomCardData> extends CardCell {
|
||||
const MobileURLCardCell({
|
||||
super.key,
|
||||
required this.cellControllerBuilder,
|
||||
});
|
||||
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
|
||||
@override
|
||||
State<MobileURLCardCell> createState() => _URLCellState();
|
||||
}
|
||||
|
||||
class _URLCellState extends State<MobileURLCardCell> {
|
||||
late final URLCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as URLCellController;
|
||||
|
||||
_cellBloc = URLCellBloc(cellController: cellController)
|
||||
..add(const URLCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cellStyle = MobileCardCellStyle(context);
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<URLCellBloc, URLCellState>(
|
||||
buildWhen: (previous, current) => previous.content != current.content,
|
||||
builder: (context, state) {
|
||||
if (state.content.isEmpty) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: cellStyle.padding,
|
||||
child: Text(
|
||||
state.content,
|
||||
style: cellStyle.urlTextStyle(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/card_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/card_cell.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/text_cell/text_cell_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MobileCardContent<CustomCardData> extends StatelessWidget {
|
||||
const MobileCardContent({
|
||||
super.key,
|
||||
required this.cellBuilder,
|
||||
required this.cells,
|
||||
required this.cardData,
|
||||
required this.styleConfiguration,
|
||||
this.renderHook,
|
||||
});
|
||||
|
||||
final CardCellBuilder<CustomCardData> cellBuilder;
|
||||
|
||||
final List<DatabaseCellContext> cells;
|
||||
final RowCardRenderHook<CustomCardData>? renderHook;
|
||||
final CustomCardData? cardData;
|
||||
final RowCardStyleConfiguration styleConfiguration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: styleConfiguration.cardPadding,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _makeCells(context, cells),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _makeCells(
|
||||
BuildContext context,
|
||||
List<DatabaseCellContext> cells,
|
||||
) {
|
||||
final List<Widget> children = [];
|
||||
|
||||
cells.asMap().forEach((int index, DatabaseCellContext cellContext) {
|
||||
Widget child;
|
||||
if (index == 0) {
|
||||
// The title cell UI is different with a normal text cell.
|
||||
// Use render hook to customize its UI
|
||||
child = _buildTitleCell(cellContext);
|
||||
} else {
|
||||
child = Padding(
|
||||
key: cellContext.key(),
|
||||
padding: styleConfiguration.cellPadding,
|
||||
child: cellBuilder.buildCell(
|
||||
cellContext: cellContext,
|
||||
cardData: cardData,
|
||||
renderHook: renderHook,
|
||||
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
children.add(child);
|
||||
});
|
||||
return children;
|
||||
}
|
||||
|
||||
Widget _buildTitleCell(
|
||||
DatabaseCellContext cellContext,
|
||||
) {
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
renderHook.addTextCellHook((cellData, _, __) {
|
||||
return BlocBuilder<TextCellBloc, TextCellState>(
|
||||
builder: (context, state) {
|
||||
final text = cellData.isEmpty
|
||||
? LocaleKeys.grid_row_titlePlaceholder.tr()
|
||||
: cellData;
|
||||
|
||||
final textStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: cellData.isEmpty
|
||||
? Theme.of(context).colorScheme.onSecondary
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: 20,
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (!cellContext.rowMeta.isDocumentEmpty) ...[
|
||||
const FlowySvg(FlowySvgs.notes_s),
|
||||
const HSpace(4),
|
||||
],
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: textStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return Padding(
|
||||
key: cellContext.key(),
|
||||
padding: styleConfiguration.cellPadding,
|
||||
child: CardCellBuilder<String>(cellBuilder.cellCache).buildCell(
|
||||
cellContext: cellContext,
|
||||
renderHook: renderHook,
|
||||
hasNotes: !cellContext.rowMeta.isDocumentEmpty,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -25,6 +25,8 @@ class MobileCardDetailScreen extends StatefulWidget {
|
||||
const MobileCardDetailScreen({
|
||||
super.key,
|
||||
required this.rowController,
|
||||
this.scrollController,
|
||||
this.isBottomSheet = false,
|
||||
required this.fieldController,
|
||||
});
|
||||
|
||||
@ -34,6 +36,8 @@ class MobileCardDetailScreen extends StatefulWidget {
|
||||
static const argFieldController = 'fieldController';
|
||||
|
||||
final RowController rowController;
|
||||
final ScrollController? scrollController;
|
||||
final bool isBottomSheet;
|
||||
final FieldController fieldController;
|
||||
|
||||
@override
|
||||
@ -41,24 +45,16 @@ class MobileCardDetailScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
||||
late final ScrollController _scrollController;
|
||||
late final GridCellBuilder _cellBuilder;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController();
|
||||
_cellBuilder = GridCellBuilder(
|
||||
cellCache: widget.rowController.cellCache,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// TODO(yijing): fix context issue when navigating in bottom navigation bar
|
||||
@ -66,10 +62,18 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
||||
create: (context) => RowDetailBloc(rowController: widget.rowController)
|
||||
..add(const RowDetailEvent.initial()),
|
||||
child: Scaffold(
|
||||
// appbar with duplicate and delete card features
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
icon: Icon(
|
||||
widget.isBottomSheet ? Icons.close : Icons.arrow_back,
|
||||
),
|
||||
),
|
||||
title: Text(LocaleKeys.board_cardDetail.tr()),
|
||||
actions: [
|
||||
// appbar with duplicate and delete card features
|
||||
BlocProvider<RowActionSheetBloc>(
|
||||
create: (context) => RowActionSheetBloc(
|
||||
viewId: widget.rowController.viewId,
|
||||
@ -135,9 +139,9 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: ListView(
|
||||
controller: _scrollController,
|
||||
controller: widget.scrollController,
|
||||
children: [
|
||||
BlocProvider<RowBannerBloc>(
|
||||
create: (context) => RowBannerBloc(
|
||||
@ -181,7 +185,7 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
||||
RowDocument(
|
||||
viewId: widget.rowController.viewId,
|
||||
rowId: widget.rowController.rowId,
|
||||
scrollController: _scrollController,
|
||||
scrollController: widget.scrollController ?? ScrollController(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -83,7 +83,7 @@ class _MobileRecentViewState extends State<MobileRecentView> {
|
||||
color: theme.colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withOpacity(0.5),
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
|
@ -85,7 +85,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
startRowId: startRowId,
|
||||
);
|
||||
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
result.fold(
|
||||
(rowMeta) {
|
||||
emit(state.copyWith(recentAddedRowMeta: rowMeta));
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
createHeaderRow: (String groupId) async {
|
||||
final result = await databaseController.createRow(
|
||||
@ -93,7 +98,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
fromBeginning: true,
|
||||
);
|
||||
|
||||
result.fold((_) {}, (err) => Log.error(err));
|
||||
result.fold(
|
||||
(rowMeta) {
|
||||
emit(state.copyWith(recentAddedRowMeta: rowMeta));
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
createGroup: (name) async {
|
||||
final result = await groupBackendSvc.createGroup(name: name);
|
||||
@ -527,6 +537,7 @@ class BoardState with _$BoardState {
|
||||
required BoardLayoutSettingPB? layoutSettings,
|
||||
String? editingHeaderId,
|
||||
BoardEditingRow? editingRow,
|
||||
RowMetaPB? recentAddedRowMeta,
|
||||
required List<GroupPB> hiddenGroups,
|
||||
}) = _BoardState;
|
||||
|
||||
|
@ -2,7 +2,8 @@ import 'dart:collection';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_content.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
@ -23,7 +24,6 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart' hide Card;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../widgets/card/card.dart';
|
||||
import '../../widgets/card/card_cell_builder.dart';
|
||||
@ -89,11 +89,19 @@ class BoardPage extends StatelessWidget {
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
finish: (result) => result.successOrFail.fold(
|
||||
(_) => BoardContent(onEditStateChanged: onEditStateChanged),
|
||||
(err) => FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
(_) => PlatformExtension.isMobile
|
||||
? const MobileBoardContent()
|
||||
: DesktopBoardContent(onEditStateChanged: onEditStateChanged),
|
||||
(err) => PlatformExtension.isMobile
|
||||
? FlowyMobileStateContainer.error(
|
||||
emoji: '🛸',
|
||||
title: LocaleKeys.board_mobile_faildToLoad.tr(),
|
||||
errorMsg: err.toString(),
|
||||
)
|
||||
: FlowyErrorPage.message(
|
||||
err.toString(),
|
||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -101,8 +109,8 @@ class BoardPage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class BoardContent extends StatefulWidget {
|
||||
const BoardContent({
|
||||
class DesktopBoardContent extends StatefulWidget {
|
||||
const DesktopBoardContent({
|
||||
super.key,
|
||||
this.onEditStateChanged,
|
||||
});
|
||||
@ -110,18 +118,18 @@ class BoardContent extends StatefulWidget {
|
||||
final VoidCallback? onEditStateChanged;
|
||||
|
||||
@override
|
||||
State<BoardContent> createState() => _BoardContentState();
|
||||
State<DesktopBoardContent> createState() => _DesktopBoardContentState();
|
||||
}
|
||||
|
||||
class _BoardContentState extends State<BoardContent> {
|
||||
class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||
final renderHook = RowCardRenderHook<String>();
|
||||
late final ScrollController scrollController;
|
||||
late final AppFlowyBoardScrollController scrollManager;
|
||||
|
||||
final config = const AppFlowyBoardConfig(
|
||||
groupBackgroundColor: Color(0xffF7F8FC),
|
||||
headerPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||
groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||
cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||
);
|
||||
|
||||
@override
|
||||
@ -162,12 +170,12 @@ class _BoardContentState extends State<BoardContent> {
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
||||
config: const AppFlowyBoardConfig(
|
||||
groupPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
groupItemPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
footerPadding: EdgeInsets.fromLTRB(4, 14, 4, 4),
|
||||
groupMargin: EdgeInsets.symmetric(horizontal: 4),
|
||||
groupBodyPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||
groupFooterPadding: EdgeInsets.fromLTRB(4, 14, 4, 4),
|
||||
stretchGroupHeight: false,
|
||||
),
|
||||
leading: HiddenGroupsColumn(margin: config.headerPadding),
|
||||
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
|
||||
trailing: showCreateGroupButton
|
||||
? BoardTrailing(scrollController: scrollController)
|
||||
: null,
|
||||
@ -175,7 +183,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
value: context.read<BoardBloc>(),
|
||||
child: BoardColumnHeader(
|
||||
groupData: groupData,
|
||||
margin: config.headerPadding,
|
||||
margin: config.groupHeaderPadding,
|
||||
),
|
||||
),
|
||||
footerBuilder: _buildFooter,
|
||||
@ -204,7 +212,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
return AppFlowyGroupFooter(
|
||||
height: 36,
|
||||
margin: config.footerPadding,
|
||||
margin: config.groupFooterPadding,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
@ -244,7 +252,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
|
||||
return AppFlowyGroupCard(
|
||||
key: ValueKey(groupItemId),
|
||||
margin: config.cardPadding,
|
||||
margin: config.cardMargin,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: RowCard<String>(
|
||||
rowMeta: rowMeta,
|
||||
@ -329,25 +337,14 @@ class _BoardContentState extends State<BoardContent> {
|
||||
groupId: groupId,
|
||||
);
|
||||
|
||||
// navigate to card detail screen when it is in mobile
|
||||
if (PlatformExtension.isMobile) {
|
||||
context.push(
|
||||
MobileCardDetailScreen.routeName,
|
||||
extra: {
|
||||
MobileCardDetailScreen.argRowController: dataController,
|
||||
MobileCardDetailScreen.argFieldController: fieldController,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (_) => RowDetailPage(
|
||||
fieldController: fieldController,
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
rowController: dataController,
|
||||
),
|
||||
);
|
||||
}
|
||||
FlowyOverlay.show(
|
||||
context: context,
|
||||
builder: (_) => RowDetailPage(
|
||||
fieldController: fieldController,
|
||||
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||
rowController: dataController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.dart';
|
||||
@ -120,14 +121,12 @@ class _RowCardState<T> extends State<RowCard<T>> {
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (PlatformExtension.isMobile) {
|
||||
// TODO(yijing): refactor it in mobile to display card in database view
|
||||
return RowCardContainer(
|
||||
buildAccessoryWhen: () => state.isEditing == false,
|
||||
accessories: const [],
|
||||
openAccessory: (p0) {},
|
||||
openCard: (context) => widget.openCard(context),
|
||||
child: _CardContent<T>(
|
||||
rowNotifier: rowNotifier,
|
||||
child: MobileCardContent<T>(
|
||||
cellBuilder: widget.cellBuilder,
|
||||
styleConfiguration: widget.styleConfiguration,
|
||||
cells: state.cells,
|
||||
|
@ -1,4 +1,7 @@
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_content/card_cells/card_cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/card/cells/timestamp_card_cell.dart';
|
||||
import 'package:appflowy/util/platform_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -10,7 +13,6 @@ import 'cells/date_card_cell.dart';
|
||||
import 'cells/number_card_cell.dart';
|
||||
import 'cells/select_option_card_cell.dart';
|
||||
import 'cells/text_card_cell.dart';
|
||||
import 'cells/timestamp_card_cell.dart';
|
||||
import 'cells/url_card_cell.dart';
|
||||
|
||||
// T represents as the Generic card data
|
||||
@ -34,6 +36,40 @@ class CardCellBuilder<CustomCardData> {
|
||||
|
||||
final key = cellContext.key();
|
||||
final style = styles?[cellContext.fieldType];
|
||||
|
||||
return PlatformExtension.isMobile
|
||||
? _getMobileCardCellWidget(
|
||||
key: key,
|
||||
cellContext: cellContext,
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
style: style,
|
||||
cardData: cardData,
|
||||
cellNotifier: cellNotifier,
|
||||
renderHook: renderHook,
|
||||
hasNotes: hasNotes,
|
||||
)
|
||||
: _getDesktopCardCellWidget(
|
||||
key: key,
|
||||
cellContext: cellContext,
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
style: style,
|
||||
cardData: cardData,
|
||||
cellNotifier: cellNotifier,
|
||||
renderHook: renderHook,
|
||||
hasNotes: hasNotes,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getDesktopCardCellWidget({
|
||||
required Key key,
|
||||
required DatabaseCellContext cellContext,
|
||||
required CellControllerBuilder cellControllerBuilder,
|
||||
CardCellStyle? style,
|
||||
CustomCardData? cardData,
|
||||
EditableCardNotifier? cellNotifier,
|
||||
RowCardRenderHook<CustomCardData>? renderHook,
|
||||
required bool hasNotes,
|
||||
}) {
|
||||
switch (cellContext.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return CheckboxCardCell(
|
||||
@ -104,4 +140,79 @@ class CardCellBuilder<CustomCardData> {
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
|
||||
Widget _getMobileCardCellWidget({
|
||||
required Key key,
|
||||
required DatabaseCellContext cellContext,
|
||||
required CellControllerBuilder cellControllerBuilder,
|
||||
CardCellStyle? style,
|
||||
CustomCardData? cardData,
|
||||
EditableCardNotifier? cellNotifier,
|
||||
RowCardRenderHook<CustomCardData>? renderHook,
|
||||
required bool hasNotes,
|
||||
}) {
|
||||
switch (cellContext.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return MobileCheckboxCardCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
return MobileDateCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.DateTime],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.LastEditedTime:
|
||||
return MobileTimestampCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.CreatedTime:
|
||||
return MobileTimestampCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.CreatedTime],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return MobileSelectOptionCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.SingleSelect],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
cardData: cardData,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return MobileSelectOptionCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.MultiSelect],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
cardData: cardData,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return MobileChecklistCardCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return MobileNumberCardCell<CustomCardData>(
|
||||
renderHook: renderHook?.renderHook[FieldType.Number],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return MobileTextCardCell<CustomCardData>(
|
||||
key: key,
|
||||
cardData: cardData,
|
||||
renderHook: renderHook?.renderHook[FieldType.RichText],
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
);
|
||||
case FieldType.URL:
|
||||
return MobileURLCardCell<CustomCardData>(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
}
|
||||
throw UnimplementedError;
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ typedef CellRenderHook<C, CustomCardData> = Widget? Function(
|
||||
typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
|
||||
|
||||
/// The [RowCardRenderHook] is used to customize the rendering of the
|
||||
/// card cell. Each cell has itw own field type. So the [renderHook]
|
||||
/// card cell. Each cell has its own field type. So the [renderHook]
|
||||
/// is a map of [FieldType] to [CellRenderHook].
|
||||
class RowCardRenderHook<CustomCardData> {
|
||||
final RenderHookByFieldType<CustomCardData> renderHook = {};
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/card/card_property_edit/card_property_edit_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_board_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
||||
|
@ -37,15 +37,18 @@ class MobileAppearance extends BaseAppearance {
|
||||
brightness: brightness,
|
||||
primary: _primaryColor,
|
||||
onPrimary: Colors.white,
|
||||
// group card header background color
|
||||
primaryContainer: const Color(0xffF1F1F4), // primary 20
|
||||
// group card & property edit background color
|
||||
secondary: const Color(0xfff7f8fc), // shade 10
|
||||
onSecondary: _onSecondaryColor,
|
||||
// hidden group title & card text color
|
||||
tertiary: const Color(0xff858585), // for light text
|
||||
error: const Color(0xffFB006D),
|
||||
onError: const Color(0xffFB006D),
|
||||
background: Colors.white,
|
||||
onBackground: _onBackgroundColor,
|
||||
outline: const Color(0xffE3E3E3), //caption
|
||||
// outline: const Color(0xffBDC0C5), //caption
|
||||
outline: const Color(0xffe3e3e3),
|
||||
outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24),
|
||||
//Snack bar
|
||||
surface: Colors.white,
|
||||
|
@ -45,8 +45,8 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: "2de4fe0"
|
||||
resolved-ref: "2de4fe0b0245dcdf2c2bf43410661c28acbcc687"
|
||||
ref: "93a1b70"
|
||||
resolved-ref: "93a1b70858801583b5a7edb0b2c06308d7982054"
|
||||
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
||||
source: git
|
||||
version: "0.1.1"
|
||||
|
@ -40,10 +40,9 @@ dependencies:
|
||||
flowy_svg:
|
||||
path: packages/flowy_svg
|
||||
appflowy_board:
|
||||
# path: packages/appflowy_board
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||
ref: 2de4fe0
|
||||
ref: 93a1b70
|
||||
appflowy_editor:
|
||||
git:
|
||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||
|
@ -228,7 +228,8 @@
|
||||
"addToFavorites": "Add to favorites",
|
||||
"rename": "Rename",
|
||||
"helpCenter": "Help Center",
|
||||
"OK": "Ok",
|
||||
"add": "Add",
|
||||
"yes": "Yes",
|
||||
"Done": "Done",
|
||||
"Cancel": "Cancel"
|
||||
},
|
||||
@ -813,6 +814,8 @@
|
||||
"addToColumnTopTooltip": "Add a new card at the top",
|
||||
"renameColumn": "Rename",
|
||||
"hideColumn": "Hide",
|
||||
"groupActions": "Group Actions",
|
||||
"newGroup": "New Group",
|
||||
"deleteColumn": "Delete",
|
||||
"deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?"
|
||||
},
|
||||
@ -836,7 +839,10 @@
|
||||
"groupBy": "Group by",
|
||||
"referencedBoardPrefix": "View of",
|
||||
"mobile": {
|
||||
"editURL": "Edit URL"
|
||||
"editURL": "Edit URL",
|
||||
"unhideGroup": "Unhide group",
|
||||
"unhideGroupContent": "Are you sure you want to show this group on the board?",
|
||||
"faildToLoad": "Failed to load board view"
|
||||
}
|
||||
},
|
||||
"calendar": {
|
||||
|
Loading…
Reference in New Issue
Block a user