feat: open the row page on mobile (#5975)

* chore: add dart dependency validator

* feat: open the row page on mobile

* Revert "chore: add dart dependency validator"

This reverts commit c81e5ef0ed.

* chore: update translations

* feat: preload row page to reduce open time

* chore: don't add orphan doc into recent records

* fix: bloc error

* fix: migrate the row page title to latest design

* chore: optimize database mobile UI
This commit is contained in:
Lucas.Xu 2024-08-15 20:12:09 +08:00 committed by GitHub
parent 88cc0caab7
commit 6283649a6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 229 additions and 163 deletions

View File

@ -2,20 +2,23 @@ import 'dart:async';
import 'dart:convert';
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
extension MobileRouter on BuildContext {
Future<void> pushView(ViewPB view, [Map<String, dynamic>? arguments]) async {
Future<void> pushView(
ViewPB view, {
Map<String, dynamic>? arguments,
bool addInRecent = true,
}) async {
// set the current view before pushing the new view
getIt<MenuSharedState>().latestOpenView = view;
unawaited(getIt<CachedRecentService>().updateRecentViews([view.id], true));

View File

@ -10,7 +10,7 @@ enum FlowyAppBarLeadingType {
Widget getWidget(VoidCallback? onTap) {
switch (this) {
case FlowyAppBarLeadingType.back:
return AppBarBackButton(onTap: onTap);
return AppBarImmersiveBackButton(onTap: onTap);
case FlowyAppBarLeadingType.close:
return AppBarCloseButton(onTap: onTap);
case FlowyAppBarLeadingType.cancel:

View File

@ -26,6 +26,31 @@ class AppBarBackButton extends StatelessWidget {
}
}
class AppBarImmersiveBackButton extends StatelessWidget {
const AppBarImmersiveBackButton({
super.key,
this.onTap,
});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return AppBarButton(
onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
padding: const EdgeInsets.only(
left: 12.0,
top: 8.0,
bottom: 8.0,
right: 4.0,
),
child: const FlowySvg(
FlowySvgs.m_app_bar_back_s,
),
);
}
}
class AppBarCloseButton extends StatelessWidget {
const AppBarCloseButton({
super.key,

View File

@ -4,7 +4,6 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@ -232,19 +231,20 @@ class _MobileViewPageState extends State<MobileViewPage> {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.isNotEmpty)
ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 34.0),
child: EmojiText(
emoji: '$icon ',
fontSize: 22.0,
),
if (icon != null && icon.isNotEmpty) ...[
FlowyText.emoji(
icon,
fontSize: 15.0,
figmaLineHeight: 18.0,
),
const HSpace(4),
],
Expanded(
child: FlowyText.medium(
view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 18.0,
),
),
],

View File

@ -52,6 +52,7 @@ class _MobileBottomSheetRenameWidgetState
height: 42.0,
child: FlowyTextField(
controller: controller,
textStyle: Theme.of(context).textTheme.bodyMedium,
keyboardType: TextInputType.text,
onSubmitted: (text) => widget.onRename(text),
),

View File

@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
@ -366,6 +367,9 @@ class MobileRowDetailPageContentState
if (rowDetailState.numHiddenFields != 0) ...[
const ToggleHiddenFieldsVisibilityButton(),
],
OpenRowPageButton(
documentId: rowController.rowMeta.documentId,
),
MobileRowDetailCreateFieldButton(
viewId: viewId,
fieldController: fieldController,

View File

@ -0,0 +1,116 @@
import 'dart:async';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class OpenRowPageButton extends StatefulWidget {
const OpenRowPageButton({
super.key,
required this.documentId,
});
final String documentId;
@override
State<OpenRowPageButton> createState() => _OpenRowPageButtonState();
}
class _OpenRowPageButtonState extends State<OpenRowPageButton> {
ViewPB? view;
@override
void initState() {
super.initState();
_preloadView(context, createDocumentIfMissed: true);
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
minHeight: GridSize.headerHeight,
),
child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith(
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
),
overlayColor: WidgetStateProperty.all<Color>(
Theme.of(context).hoverColor,
),
alignment: AlignmentDirectional.centerStart,
splashFactory: NoSplash.splashFactory,
padding: const WidgetStatePropertyAll(
EdgeInsets.symmetric(vertical: 14, horizontal: 6),
),
),
label: FlowyText.medium(
LocaleKeys.grid_field_openRowDocument.tr(),
fontSize: 15,
),
icon: const Padding(
padding: EdgeInsets.all(4.0),
child: FlowySvg(
FlowySvgs.full_view_s,
size: Size.square(16.0),
),
),
onPressed: () => _openRowPage(context),
),
);
}
Future<void> _openRowPage(BuildContext context) async {
Log.info('Open row page(${widget.documentId})');
if (view == null) {
showToastNotification(context, message: 'Failed to open row page');
// reload the view again
unawaited(_preloadView(context));
Log.error('Failed to open row page(${widget.documentId})');
return;
}
if (context.mounted) {
// the document in row is an orphan document, so we don't add it to recent
await context.pushView(
view!,
addInRecent: false,
);
}
}
// preload view to reduce the time to open the view
Future<void> _preloadView(
BuildContext context, {
bool createDocumentIfMissed = false,
}) async {
Log.info('Preload row page(${widget.documentId})');
final result = await ViewBackendService.getView(widget.documentId);
view = result.fold((s) => s, (f) => null);
if (view == null && createDocumentIfMissed) {
// create view if not exists
Log.info('Create row page(${widget.documentId})');
final result = await ViewBackendService.createOrphanView(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
viewId: widget.documentId,
layoutType: ViewLayoutPB.Document,
);
view = result.fold((s) => s, (f) => null);
}
}
}

View File

@ -39,11 +39,13 @@ class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
_startListening();
await _getAllCalculations();
if (!isClosed) {
add(
CalculationsEvent.didReceiveFieldUpdate(
_fieldController.fieldInfos,
),
);
}
},
didReceiveFieldUpdate: (fields) async {
emit(
@ -131,6 +133,10 @@ class CalculationsBloc extends Bloc<CalculationsEvent, CalculationsState> {
Future<void> _getAllCalculations() async {
final calculationsOrFailure = await _calculationsService.getCalculations();
if (isClosed) {
return;
}
final RepeatedCalculationsPB? calculations =
calculationsOrFailure.fold((s) => s, (e) => null);
if (calculations != null) {

View File

@ -1,15 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:collection/collection.dart';
@ -47,20 +44,16 @@ class ViewTitleBarWithRow extends StatelessWidget {
if (state.ancestors.isEmpty) {
return const SizedBox.shrink();
}
const maxWidth = WindowSizeManager.minWindowWidth - 200;
return LayoutBuilder(
builder: (context, constraints) {
return Visibility(
visible: maxWidth < constraints.maxWidth,
// if the width is too small, only show one view title bar without the ancestors
replacement: _buildRowName(),
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
height: 24,
child: Row(
// refresh the view title bar when the ancestors changed
key: ValueKey(state.ancestors.hashCode),
children: _buildViewTitles(state.ancestors),
),
);
},
),
);
},
),
@ -71,16 +64,22 @@ class ViewTitleBarWithRow extends StatelessWidget {
// if the level is too deep, only show the root view, the database view and the row
return views.length > 2
? [
_buildViewButton(views.first),
const FlowyText.regular('/'),
const FlowyText.regular(' ... /'),
_buildViewButton(views[1]),
const FlowySvg(FlowySvgs.title_bar_divider_s),
const FlowyText.regular(' ... '),
const FlowySvg(FlowySvgs.title_bar_divider_s),
_buildViewButton(views.last),
const FlowyText.regular('/'),
const FlowySvg(FlowySvgs.title_bar_divider_s),
_buildRowName(),
]
: [
...views
.map((e) => [_buildViewButton(e), const FlowyText.regular('/')])
.map(
(e) => [
_buildViewButton(e),
const FlowySvg(FlowySvgs.title_bar_divider_s),
],
)
.flattened,
_buildRowName(),
];
@ -89,9 +88,9 @@ class ViewTitleBarWithRow extends StatelessWidget {
Widget _buildViewButton(ViewPB view) {
return FlowyTooltip(
message: view.name,
child: _ViewTitle(
child: ViewTitle(
view: view,
behavior: _ViewTitleBehavior.uneditable,
behavior: ViewTitleBehavior.uneditable,
onUpdated: () {},
),
);
@ -180,11 +179,14 @@ class _TitleSkin extends IEditableTextCellSkin {
onTap: () {},
text: Row(
children: [
EmojiText(
emoji: state.icon ?? "",
fontSize: 18.0,
if (state.icon != null) ...[
FlowyText.emoji(
state.icon!,
fontSize: 14.0,
figmaLineHeight: 18.0,
),
const HSpace(2.0),
const HSpace(4.0),
],
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: FlowyText.regular(
@ -204,106 +206,6 @@ class _TitleSkin extends IEditableTextCellSkin {
}
}
enum _ViewTitleBehavior {
editable,
uneditable,
}
class _ViewTitle extends StatefulWidget {
const _ViewTitle({
required this.view,
this.behavior = _ViewTitleBehavior.editable,
required this.onUpdated,
}) : maxTitleWidth = 180;
final ViewPB view;
final _ViewTitleBehavior behavior;
final double maxTitleWidth;
final VoidCallback onUpdated;
@override
State<_ViewTitle> createState() => _ViewTitleState();
}
class _ViewTitleState extends State<_ViewTitle> {
late final viewListener = ViewListener(viewId: widget.view.id);
String name = '';
String icon = '';
@override
void initState() {
super.initState();
name = widget.view.name.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: widget.view.name;
icon = widget.view.icon.value;
viewListener.start(
onViewUpdated: (view) {
if (name != view.name || icon != view.icon.value) {
widget.onUpdated();
}
setState(() {
name = view.name.isEmpty
? LocaleKeys.document_title_placeholder.tr()
: view.name;
icon = view.icon.value;
});
},
);
}
@override
void dispose() {
viewListener.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
// root view
if (widget.view.parentViewId.isEmpty) {
return Row(
children: [
FlowyText.regular(name),
const HSpace(4.0),
],
);
}
final child = Row(
children: [
EmojiText(
emoji: icon,
fontSize: 18.0,
),
const HSpace(2.0),
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: widget.maxTitleWidth,
),
child: FlowyText.regular(
name,
overflow: TextOverflow.ellipsis,
),
),
],
);
return Listener(
onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view),
child: FlowyButton(
useIntrinsicWidth: true,
onTap: () {},
text: child,
),
);
}
}
class RenameRowPopover extends StatefulWidget {
const RenameRowPopover({
super.key,

View File

@ -39,6 +39,10 @@ class DocumentCollaboratorsBloc
if (userProfile != null) {
_listener.start(
onDocAwarenessUpdate: (states) {
if (isClosed) {
return;
}
add(
DocumentCollaboratorsEvent.update(
userProfile,

View File

@ -190,9 +190,12 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
if (view != null) {
final view = action.arguments?[ActionArgumentKeys.view];
final rowId = action.arguments?[ActionArgumentKeys.rowId];
AppGlobals.rootNavKey.currentContext?.pushView(view, {
AppGlobals.rootNavKey.currentContext?.pushView(
view,
arguments: {
PluginArgumentKeys.rowId: rowId,
});
},
);
}
}
});

View File

@ -79,11 +79,11 @@ class ViewTitleBar extends StatelessWidget {
final child = FlowyTooltip(
key: ValueKey(view.id),
message: view.name,
child: _ViewTitle(
child: ViewTitle(
view: view,
behavior: i == views.length - 1
? _ViewTitleBehavior.editable // only the last one is editable
: _ViewTitleBehavior.uneditable, // others are not editable
? ViewTitleBehavior.editable // only the last one is editable
: ViewTitleBehavior.uneditable, // others are not editable
onUpdated: () {
context
.read<ViewTitleBarBloc>()
@ -103,27 +103,28 @@ class ViewTitleBar extends StatelessWidget {
}
}
enum _ViewTitleBehavior {
enum ViewTitleBehavior {
editable,
uneditable,
}
class _ViewTitle extends StatefulWidget {
const _ViewTitle({
class ViewTitle extends StatefulWidget {
const ViewTitle({
super.key,
required this.view,
this.behavior = _ViewTitleBehavior.editable,
this.behavior = ViewTitleBehavior.editable,
required this.onUpdated,
});
final ViewPB view;
final _ViewTitleBehavior behavior;
final ViewTitleBehavior behavior;
final VoidCallback onUpdated;
@override
State<_ViewTitle> createState() => _ViewTitleState();
State<ViewTitle> createState() => _ViewTitleState();
}
class _ViewTitleState extends State<_ViewTitle> {
class _ViewTitleState extends State<ViewTitle> {
final popoverController = PopoverController();
final textEditingController = TextEditingController();
@ -137,7 +138,7 @@ class _ViewTitleState extends State<_ViewTitle> {
@override
Widget build(BuildContext context) {
final isEditable = widget.behavior == _ViewTitleBehavior.editable;
final isEditable = widget.behavior == ViewTitleBehavior.editable;
return BlocProvider(
create: (_) =>

View File

@ -1327,6 +1327,7 @@
"addOption": "Add option",
"editProperty": "Edit property",
"newProperty": "New property",
"openRowDocument": "Open document",
"deleteFieldPromptMessage": "Are you sure? This property will be deleted",
"clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied",
"newColumn": "New Column",