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_calendar_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/presentation.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_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({
|
const MobileCardDetailScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.rowController,
|
required this.rowController,
|
||||||
|
this.scrollController,
|
||||||
|
this.isBottomSheet = false,
|
||||||
required this.fieldController,
|
required this.fieldController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -34,6 +36,8 @@ class MobileCardDetailScreen extends StatefulWidget {
|
|||||||
static const argFieldController = 'fieldController';
|
static const argFieldController = 'fieldController';
|
||||||
|
|
||||||
final RowController rowController;
|
final RowController rowController;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
final bool isBottomSheet;
|
||||||
final FieldController fieldController;
|
final FieldController fieldController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -41,24 +45,16 @@ class MobileCardDetailScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
||||||
late final ScrollController _scrollController;
|
|
||||||
late final GridCellBuilder _cellBuilder;
|
late final GridCellBuilder _cellBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_scrollController = ScrollController();
|
|
||||||
_cellBuilder = GridCellBuilder(
|
_cellBuilder = GridCellBuilder(
|
||||||
cellCache: widget.rowController.cellCache,
|
cellCache: widget.rowController.cellCache,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// TODO(yijing): fix context issue when navigating in bottom navigation bar
|
// 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)
|
create: (context) => RowDetailBloc(rowController: widget.rowController)
|
||||||
..add(const RowDetailEvent.initial()),
|
..add(const RowDetailEvent.initial()),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
// appbar with duplicate and delete card features
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.pop();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
widget.isBottomSheet ? Icons.close : Icons.arrow_back,
|
||||||
|
),
|
||||||
|
),
|
||||||
title: Text(LocaleKeys.board_cardDetail.tr()),
|
title: Text(LocaleKeys.board_cardDetail.tr()),
|
||||||
actions: [
|
actions: [
|
||||||
|
// appbar with duplicate and delete card features
|
||||||
BlocProvider<RowActionSheetBloc>(
|
BlocProvider<RowActionSheetBloc>(
|
||||||
create: (context) => RowActionSheetBloc(
|
create: (context) => RowActionSheetBloc(
|
||||||
viewId: widget.rowController.viewId,
|
viewId: widget.rowController.viewId,
|
||||||
@ -135,9 +139,9 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
controller: _scrollController,
|
controller: widget.scrollController,
|
||||||
children: [
|
children: [
|
||||||
BlocProvider<RowBannerBloc>(
|
BlocProvider<RowBannerBloc>(
|
||||||
create: (context) => RowBannerBloc(
|
create: (context) => RowBannerBloc(
|
||||||
@ -181,7 +185,7 @@ class _MobileCardDetailScreenState extends State<MobileCardDetailScreen> {
|
|||||||
RowDocument(
|
RowDocument(
|
||||||
viewId: widget.rowController.viewId,
|
viewId: widget.rowController.viewId,
|
||||||
rowId: widget.rowController.rowId,
|
rowId: widget.rowController.rowId,
|
||||||
scrollController: _scrollController,
|
scrollController: widget.scrollController ?? ScrollController(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -83,7 +83,7 @@ class _MobileRecentViewState extends State<MobileRecentView> {
|
|||||||
color: theme.colorScheme.background,
|
color: theme.colorScheme.background,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outline.withOpacity(0.5),
|
color: theme.colorScheme.outline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
@ -85,7 +85,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
startRowId: startRowId,
|
startRowId: startRowId,
|
||||||
);
|
);
|
||||||
|
|
||||||
result.fold((_) {}, (err) => Log.error(err));
|
result.fold(
|
||||||
|
(rowMeta) {
|
||||||
|
emit(state.copyWith(recentAddedRowMeta: rowMeta));
|
||||||
|
},
|
||||||
|
(err) => Log.error(err),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
createHeaderRow: (String groupId) async {
|
createHeaderRow: (String groupId) async {
|
||||||
final result = await databaseController.createRow(
|
final result = await databaseController.createRow(
|
||||||
@ -93,7 +98,12 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
|||||||
fromBeginning: true,
|
fromBeginning: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
result.fold((_) {}, (err) => Log.error(err));
|
result.fold(
|
||||||
|
(rowMeta) {
|
||||||
|
emit(state.copyWith(recentAddedRowMeta: rowMeta));
|
||||||
|
},
|
||||||
|
(err) => Log.error(err),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
createGroup: (name) async {
|
createGroup: (name) async {
|
||||||
final result = await groupBackendSvc.createGroup(name: name);
|
final result = await groupBackendSvc.createGroup(name: name);
|
||||||
@ -527,6 +537,7 @@ class BoardState with _$BoardState {
|
|||||||
required BoardLayoutSettingPB? layoutSettings,
|
required BoardLayoutSettingPB? layoutSettings,
|
||||||
String? editingHeaderId,
|
String? editingHeaderId,
|
||||||
BoardEditingRow? editingRow,
|
BoardEditingRow? editingRow,
|
||||||
|
RowMetaPB? recentAddedRowMeta,
|
||||||
required List<GroupPB> hiddenGroups,
|
required List<GroupPB> hiddenGroups,
|
||||||
}) = _BoardState;
|
}) = _BoardState;
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/database_controller.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.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_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/material.dart' hide Card;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../widgets/card/card.dart';
|
import '../../widgets/card/card.dart';
|
||||||
import '../../widgets/card/card_cell_builder.dart';
|
import '../../widgets/card/card_cell_builder.dart';
|
||||||
@ -89,11 +89,19 @@ class BoardPage extends StatelessWidget {
|
|||||||
child: CircularProgressIndicator.adaptive(),
|
child: CircularProgressIndicator.adaptive(),
|
||||||
),
|
),
|
||||||
finish: (result) => result.successOrFail.fold(
|
finish: (result) => result.successOrFail.fold(
|
||||||
(_) => BoardContent(onEditStateChanged: onEditStateChanged),
|
(_) => PlatformExtension.isMobile
|
||||||
(err) => FlowyErrorPage.message(
|
? const MobileBoardContent()
|
||||||
err.toString(),
|
: DesktopBoardContent(onEditStateChanged: onEditStateChanged),
|
||||||
howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),
|
(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 {
|
class DesktopBoardContent extends StatefulWidget {
|
||||||
const BoardContent({
|
const DesktopBoardContent({
|
||||||
super.key,
|
super.key,
|
||||||
this.onEditStateChanged,
|
this.onEditStateChanged,
|
||||||
});
|
});
|
||||||
@ -110,18 +118,18 @@ class BoardContent extends StatefulWidget {
|
|||||||
final VoidCallback? onEditStateChanged;
|
final VoidCallback? onEditStateChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BoardContent> createState() => _BoardContentState();
|
State<DesktopBoardContent> createState() => _DesktopBoardContentState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BoardContentState extends State<BoardContent> {
|
class _DesktopBoardContentState extends State<DesktopBoardContent> {
|
||||||
final renderHook = RowCardRenderHook<String>();
|
final renderHook = RowCardRenderHook<String>();
|
||||||
late final ScrollController scrollController;
|
late final ScrollController scrollController;
|
||||||
late final AppFlowyBoardScrollController scrollManager;
|
late final AppFlowyBoardScrollController scrollManager;
|
||||||
|
|
||||||
final config = const AppFlowyBoardConfig(
|
final config = const AppFlowyBoardConfig(
|
||||||
groupBackgroundColor: Color(0xffF7F8FC),
|
groupBackgroundColor: Color(0xffF7F8FC),
|
||||||
headerPadding: EdgeInsets.symmetric(horizontal: 8),
|
groupHeaderPadding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
cardPadding: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
cardMargin: EdgeInsets.symmetric(horizontal: 4, vertical: 3),
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -162,12 +170,12 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
controller: context.read<BoardBloc>().boardController,
|
controller: context.read<BoardBloc>().boardController,
|
||||||
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
groupConstraints: const BoxConstraints.tightFor(width: 256),
|
||||||
config: const AppFlowyBoardConfig(
|
config: const AppFlowyBoardConfig(
|
||||||
groupPadding: EdgeInsets.symmetric(horizontal: 4),
|
groupMargin: EdgeInsets.symmetric(horizontal: 4),
|
||||||
groupItemPadding: EdgeInsets.symmetric(horizontal: 4),
|
groupBodyPadding: EdgeInsets.symmetric(horizontal: 4),
|
||||||
footerPadding: EdgeInsets.fromLTRB(4, 14, 4, 4),
|
groupFooterPadding: EdgeInsets.fromLTRB(4, 14, 4, 4),
|
||||||
stretchGroupHeight: false,
|
stretchGroupHeight: false,
|
||||||
),
|
),
|
||||||
leading: HiddenGroupsColumn(margin: config.headerPadding),
|
leading: HiddenGroupsColumn(margin: config.groupHeaderPadding),
|
||||||
trailing: showCreateGroupButton
|
trailing: showCreateGroupButton
|
||||||
? BoardTrailing(scrollController: scrollController)
|
? BoardTrailing(scrollController: scrollController)
|
||||||
: null,
|
: null,
|
||||||
@ -175,7 +183,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
value: context.read<BoardBloc>(),
|
value: context.read<BoardBloc>(),
|
||||||
child: BoardColumnHeader(
|
child: BoardColumnHeader(
|
||||||
groupData: groupData,
|
groupData: groupData,
|
||||||
margin: config.headerPadding,
|
margin: config.groupHeaderPadding,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
footerBuilder: _buildFooter,
|
footerBuilder: _buildFooter,
|
||||||
@ -204,7 +212,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||||
return AppFlowyGroupFooter(
|
return AppFlowyGroupFooter(
|
||||||
height: 36,
|
height: 36,
|
||||||
margin: config.footerPadding,
|
margin: config.groupFooterPadding,
|
||||||
icon: FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.add_s,
|
FlowySvgs.add_s,
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
@ -244,7 +252,7 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
|
|
||||||
return AppFlowyGroupCard(
|
return AppFlowyGroupCard(
|
||||||
key: ValueKey(groupItemId),
|
key: ValueKey(groupItemId),
|
||||||
margin: config.cardPadding,
|
margin: config.cardMargin,
|
||||||
decoration: _makeBoxDecoration(context),
|
decoration: _makeBoxDecoration(context),
|
||||||
child: RowCard<String>(
|
child: RowCard<String>(
|
||||||
rowMeta: rowMeta,
|
rowMeta: rowMeta,
|
||||||
@ -329,25 +337,14 @@ class _BoardContentState extends State<BoardContent> {
|
|||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// navigate to card detail screen when it is in mobile
|
FlowyOverlay.show(
|
||||||
if (PlatformExtension.isMobile) {
|
context: context,
|
||||||
context.push(
|
builder: (_) => RowDetailPage(
|
||||||
MobileCardDetailScreen.routeName,
|
fieldController: fieldController,
|
||||||
extra: {
|
cellBuilder: GridCellBuilder(cellCache: dataController.cellCache),
|
||||||
MobileCardDetailScreen.argRowController: dataController,
|
rowController: dataController,
|
||||||
MobileCardDetailScreen.argFieldController: fieldController,
|
),
|
||||||
},
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
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/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/cell/cell_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
|
||||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/action.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) {
|
builder: (context, state) {
|
||||||
if (PlatformExtension.isMobile) {
|
if (PlatformExtension.isMobile) {
|
||||||
// TODO(yijing): refactor it in mobile to display card in database view
|
|
||||||
return RowCardContainer(
|
return RowCardContainer(
|
||||||
buildAccessoryWhen: () => state.isEditing == false,
|
buildAccessoryWhen: () => state.isEditing == false,
|
||||||
accessories: const [],
|
accessories: const [],
|
||||||
openAccessory: (p0) {},
|
openAccessory: (p0) {},
|
||||||
openCard: (context) => widget.openCard(context),
|
openCard: (context) => widget.openCard(context),
|
||||||
child: _CardContent<T>(
|
child: MobileCardContent<T>(
|
||||||
rowNotifier: rowNotifier,
|
|
||||||
cellBuilder: widget.cellBuilder,
|
cellBuilder: widget.cellBuilder,
|
||||||
styleConfiguration: widget.styleConfiguration,
|
styleConfiguration: widget.styleConfiguration,
|
||||||
cells: state.cells,
|
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/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:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@ -10,7 +13,6 @@ import 'cells/date_card_cell.dart';
|
|||||||
import 'cells/number_card_cell.dart';
|
import 'cells/number_card_cell.dart';
|
||||||
import 'cells/select_option_card_cell.dart';
|
import 'cells/select_option_card_cell.dart';
|
||||||
import 'cells/text_card_cell.dart';
|
import 'cells/text_card_cell.dart';
|
||||||
import 'cells/timestamp_card_cell.dart';
|
|
||||||
import 'cells/url_card_cell.dart';
|
import 'cells/url_card_cell.dart';
|
||||||
|
|
||||||
// T represents as the Generic card data
|
// T represents as the Generic card data
|
||||||
@ -34,6 +36,40 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
|
|
||||||
final key = cellContext.key();
|
final key = cellContext.key();
|
||||||
final style = styles?[cellContext.fieldType];
|
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) {
|
switch (cellContext.fieldType) {
|
||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
return CheckboxCardCell(
|
return CheckboxCardCell(
|
||||||
@ -104,4 +140,79 @@ class CardCellBuilder<CustomCardData> {
|
|||||||
}
|
}
|
||||||
throw UnimplementedError;
|
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>>;
|
typedef RenderHookByFieldType<C> = Map<FieldType, CellRenderHook<dynamic, C>>;
|
||||||
|
|
||||||
/// The [RowCardRenderHook] is used to customize the rendering of the
|
/// 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].
|
/// is a map of [FieldType] to [CellRenderHook].
|
||||||
class RowCardRenderHook<CustomCardData> {
|
class RowCardRenderHook<CustomCardData> {
|
||||||
final RenderHookByFieldType<CustomCardData> renderHook = {};
|
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.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_create_row_field_screen.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/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/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_events_screen.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/mobile_calendar_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/database/mobile_grid_screen.dart';
|
||||||
|
@ -37,15 +37,18 @@ class MobileAppearance extends BaseAppearance {
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
primary: _primaryColor,
|
primary: _primaryColor,
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
|
// group card header background color
|
||||||
|
primaryContainer: const Color(0xffF1F1F4), // primary 20
|
||||||
// group card & property edit background color
|
// group card & property edit background color
|
||||||
secondary: const Color(0xfff7f8fc), // shade 10
|
secondary: const Color(0xfff7f8fc), // shade 10
|
||||||
onSecondary: _onSecondaryColor,
|
onSecondary: _onSecondaryColor,
|
||||||
|
// hidden group title & card text color
|
||||||
|
tertiary: const Color(0xff858585), // for light text
|
||||||
error: const Color(0xffFB006D),
|
error: const Color(0xffFB006D),
|
||||||
onError: const Color(0xffFB006D),
|
onError: const Color(0xffFB006D),
|
||||||
background: Colors.white,
|
background: Colors.white,
|
||||||
onBackground: _onBackgroundColor,
|
onBackground: _onBackgroundColor,
|
||||||
outline: const Color(0xffE3E3E3), //caption
|
outline: const Color(0xffe3e3e3),
|
||||||
// outline: const Color(0xffBDC0C5), //caption
|
|
||||||
outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24),
|
outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24),
|
||||||
//Snack bar
|
//Snack bar
|
||||||
surface: Colors.white,
|
surface: Colors.white,
|
||||||
|
@ -45,8 +45,8 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "2de4fe0"
|
ref: "93a1b70"
|
||||||
resolved-ref: "2de4fe0b0245dcdf2c2bf43410661c28acbcc687"
|
resolved-ref: "93a1b70858801583b5a7edb0b2c06308d7982054"
|
||||||
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
url: "https://github.com/AppFlowy-IO/appflowy-board.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.1"
|
version: "0.1.1"
|
||||||
|
@ -40,10 +40,9 @@ dependencies:
|
|||||||
flowy_svg:
|
flowy_svg:
|
||||||
path: packages/flowy_svg
|
path: packages/flowy_svg
|
||||||
appflowy_board:
|
appflowy_board:
|
||||||
# path: packages/appflowy_board
|
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
url: https://github.com/AppFlowy-IO/appflowy-board.git
|
||||||
ref: 2de4fe0
|
ref: 93a1b70
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
url: https://github.com/AppFlowy-IO/appflowy-editor.git
|
||||||
|
@ -228,7 +228,8 @@
|
|||||||
"addToFavorites": "Add to favorites",
|
"addToFavorites": "Add to favorites",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"helpCenter": "Help Center",
|
"helpCenter": "Help Center",
|
||||||
"OK": "Ok",
|
"add": "Add",
|
||||||
|
"yes": "Yes",
|
||||||
"Done": "Done",
|
"Done": "Done",
|
||||||
"Cancel": "Cancel"
|
"Cancel": "Cancel"
|
||||||
},
|
},
|
||||||
@ -813,6 +814,8 @@
|
|||||||
"addToColumnTopTooltip": "Add a new card at the top",
|
"addToColumnTopTooltip": "Add a new card at the top",
|
||||||
"renameColumn": "Rename",
|
"renameColumn": "Rename",
|
||||||
"hideColumn": "Hide",
|
"hideColumn": "Hide",
|
||||||
|
"groupActions": "Group Actions",
|
||||||
|
"newGroup": "New Group",
|
||||||
"deleteColumn": "Delete",
|
"deleteColumn": "Delete",
|
||||||
"deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?"
|
"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",
|
"groupBy": "Group by",
|
||||||
"referencedBoardPrefix": "View of",
|
"referencedBoardPrefix": "View of",
|
||||||
"mobile": {
|
"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": {
|
"calendar": {
|
||||||
|
Loading…
Reference in New Issue
Block a user