feat: lock page on mobile (#7366)

* feat: support lock button in view more actions

* feat: add lock page on mobile

* feat: disable actions in locked page

* feat: disable more actions in locked page

* feat: support locked grid on mobile

* feat: support locked board/calendar on mobile

* fix: exclude lock page button from AI Chat
This commit is contained in:
Lucas 2025-02-12 15:07:21 +08:00 committed by GitHub
parent 71ce9affbe
commit c1a8d89938
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 419 additions and 139 deletions

View File

@ -1,5 +1,5 @@
PODS:
- app_links (0.0.1):
- app_links (0.0.2):
- Flutter
- appflowy_backend (0.0.1):
- Flutter
@ -79,7 +79,7 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3):
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- super_native_extensions (0.0.1):
@ -90,6 +90,7 @@ PODS:
- Flutter
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
@ -111,10 +112,10 @@ DEPENDENCIES:
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
@ -165,17 +166,17 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
super_native_extensions:
:path: ".symlinks/plugins/super_native_extensions/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a
connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
@ -186,7 +187,7 @@ SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: d5929033778cc4991a187e4e1a85396fa4f59b3a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
@ -199,12 +200,12 @@ SPEC CHECKSUMS:
sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
webview_flutter_wkwebview: 45a041c7831641076618876de3ba75c712860c6b
webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca

View File

@ -1,7 +1,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
@ -7,6 +8,7 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
@ -18,6 +20,9 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.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';
@ -91,7 +96,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
final body = _buildBody(context, state);
if (view == null) {
return _buildApp(context, null, body);
return SizedBox.shrink();
}
return MultiBlocProvider(
@ -122,6 +127,11 @@ class _MobileViewPageState extends State<MobileViewPage> {
create: (_) => DocumentPageStyleBloc(view: view)
..add(const DocumentPageStyleEvent.initial()),
),
if (view.layout.isDocumentView || view.layout.isDatabaseView)
BlocProvider(
create: (_) => ViewLockStatusBloc(view: view)
..add(const ViewLockStatusEvent.initial()),
),
],
child: Builder(
builder: (context) {
@ -152,6 +162,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
title: title,
appBarOpacity: _appBarOpacity,
actions: actions,
view: view,
)
: FlowyAppBar(title: title, actions: actions);
final body = isDocument
@ -222,6 +233,8 @@ class _MobileViewPageState extends State<MobileViewPage> {
final isImmersiveMode =
context.read<MobileViewPageBloc>().state.isImmersiveMode;
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
final actions = <Widget>[];
if (FeatureFlag.syncDocument.isOn) {
@ -240,7 +253,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
}
}
if (view.layout.isDocumentView) {
if (view.layout.isDocumentView && !isLocked) {
actions.addAll([
MobileViewPageLayoutButton(
view: view,
@ -270,25 +283,104 @@ class _MobileViewPageState extends State<MobileViewPage> {
Widget _buildTitle(BuildContext context, ViewPB? view) {
final icon = view?.icon;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null && icon.value.isNotEmpty) ...[
RawEmojiIconWidget(
emoji: icon.toEmojiIconData(),
emojiSize: 15,
return ValueListenableBuilder(
valueListenable: _appBarOpacity,
builder: (_, value, child) {
if (value < 0.99) {
return Padding(
padding: const EdgeInsets.only(left: 6.0),
child: _buildLockStatus(context, view),
);
}
return Opacity(
opacity: value,
child: Row(
children: [
if (icon != null && icon.value.isNotEmpty) ...[
RawEmojiIconWidget(
emoji: icon.toEmojiIconData(),
emojiSize: 15,
),
const HSpace(4),
],
FlowyText.medium(
widget.fixedTitle ?? view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 18.0,
),
const HSpace(4.0),
_buildLockStatusIcon(context, view),
],
),
const HSpace(4),
],
Expanded(
child: FlowyText.medium(
widget.fixedTitle ?? view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
figmaLineHeight: 18.0,
),
),
],
);
},
);
}
Widget _buildLockStatus(BuildContext context, ViewPB? view) {
if (view == null || view.layout == ViewLayoutPB.Chat) {
return const SizedBox.shrink();
}
return BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
listenWhen: (previous, current) =>
previous.isLoadingLockStatus == current.isLoadingLockStatus &&
current.isLoadingLockStatus == false,
listener: (context, state) {
if (state.isLocked) {
showToastNotification(
context,
message: LocaleKeys.lockPage_pageLockedToast.tr(),
);
EditorNotification.exitEditing().post();
}
},
builder: (context, state) {
if (state.isLocked) {
return LockedPageStatus();
} else if (!state.isLocked && state.lockCounter > 0) {
return ReLockedPageStatus();
}
return const SizedBox.shrink();
},
);
}
Widget _buildLockStatusIcon(BuildContext context, ViewPB? view) {
if (view == null || view.layout == ViewLayoutPB.Chat) {
return const SizedBox.shrink();
}
return BlocConsumer<ViewLockStatusBloc, ViewLockStatusState>(
listenWhen: (previous, current) =>
previous.isLoadingLockStatus == current.isLoadingLockStatus &&
current.isLoadingLockStatus == false,
listener: (context, state) {
if (state.isLocked) {
showToastNotification(
context,
message: LocaleKeys.lockPage_pageLockedToast.tr(),
);
}
},
builder: (context, state) {
if (state.isLocked) {
return FlowySvg(
FlowySvgs.lock_page_s,
color: const Color(0xFFD95A0B),
);
} else if (!state.isLocked && state.lockCounter > 0) {
return FlowySvg(
FlowySvgs.unlock_page_s,
color: Color(0xFF8F959E),
blendMode: null,
);
}
return const SizedBox.shrink();
},
);
}

View File

@ -12,6 +12,7 @@ import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -28,12 +29,13 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
required this.appBarOpacity,
required this.title,
required this.actions,
required this.view,
});
final ValueListenable appBarOpacity;
final Widget title;
final List<Widget> actions;
final ViewPB? view;
@override
final Size preferredSize;
@ -45,7 +47,7 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
backgroundColor:
AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity),
showDivider: false,
title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title),
title: _buildTitle(context, opacity: opacity),
leadingWidth: 44,
leading: Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0),
@ -56,6 +58,13 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget
);
}
Widget _buildTitle(
BuildContext context, {
required double opacity,
}) {
return title;
}
Widget _buildAppBarBackButton(BuildContext context) {
return AppBarButton(
padding: EdgeInsets.zero,
@ -102,6 +111,12 @@ class MobileViewPageMoreButton extends StatelessWidget {
BlocProvider.value(value: context.read<FavoriteBloc>()),
BlocProvider.value(value: context.read<MobileViewPageBloc>()),
BlocProvider.value(value: context.read<ShareBloc>()),
BlocProvider(
create: (context) => ViewLockStatusBloc(view: view)
..add(
ViewLockStatusEvent.initial(),
),
),
],
child: MobileViewPageMoreBottomSheet(view: view),
),

View File

@ -14,6 +14,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -43,7 +44,8 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
},
child: ViewPageBottomSheet(
view: view,
onAction: (action) async => _onAction(context, action),
onAction: (action, {arguments}) async =>
_onAction(context, action, arguments),
onRename: (name) {
_onRename(context, name);
context.pop();
@ -56,6 +58,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
Future<void> _onAction(
BuildContext context,
MobileViewBottomSheetBodyAction action,
Map<String, dynamic>? arguments,
) async {
switch (action) {
case MobileViewBottomSheetBodyAction.duplicate:
@ -107,12 +110,32 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget {
break;
case MobileViewBottomSheetBodyAction.updatePathName:
_updatePathName(context);
case MobileViewBottomSheetBodyAction.lockPage:
final isLocked =
arguments?[MobileViewBottomSheetBodyActionArguments.isLockedKey] ??
false;
await _lockPage(context, isLocked: isLocked);
// context.pop();
break;
case MobileViewBottomSheetBodyAction.rename:
// no need to implement, rename is handled by the onRename callback.
throw UnimplementedError();
}
}
Future<void> _lockPage(
BuildContext context, {
required bool isLocked,
}) async {
if (isLocked) {
context.read<ViewLockStatusBloc>().add(const ViewLockStatusEvent.lock());
} else {
context
.read<ViewLockStatusBloc>()
.add(const ViewLockStatusEvent.unlock());
}
}
Future<void> _publish(BuildContext context) async {
final id = context.read<ShareBloc>().view.id;
final lastPublishName = context.read<ShareBloc>().state.pathName;

View File

@ -1,8 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
enum MobileViewItemBottomSheetBodyAction {
rename,
@ -40,6 +42,8 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
BuildContext context,
MobileViewItemBottomSheetBodyAction action,
) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
return FlowyOptionTile.text(
@ -49,6 +53,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
FlowySvgs.view_item_rename_s,
size: Size.square(18),
),
enable: !isLocked,
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
@ -94,6 +99,7 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
enable: !isLocked,
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(

View File

@ -4,9 +4,12 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -25,11 +28,27 @@ enum MobileViewBottomSheetBodyAction {
visitSite,
copyShareLink,
updatePathName,
lockPage;
static const disableInLockedView = [
undo,
redo,
rename,
delete,
];
}
class MobileViewBottomSheetBodyActionArguments {
static const isLockedKey = 'is_locked';
}
typedef MobileViewBottomSheetBodyActionCallback = void Function(
MobileViewBottomSheetBodyAction action,
);
// for the [MobileViewBottomSheetBodyAction.lockPage] action,
// it will pass the [isLocked] value to the callback.
{
Map<String, dynamic>? arguments,
});
class ViewPageBottomSheet extends StatefulWidget {
const ViewPageBottomSheet({
@ -56,7 +75,7 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
case MobileBottomSheetType.view:
return MobileViewBottomSheetBody(
view: widget.view,
onAction: (action) {
onAction: (action, {arguments}) {
switch (action) {
case MobileViewBottomSheetBodyAction.rename:
setState(() {
@ -64,7 +83,7 @@ class _ViewPageBottomSheetState extends State<ViewPageBottomSheet> {
});
break;
default:
widget.onAction(action);
widget.onAction(action, arguments: arguments);
}
},
);
@ -93,6 +112,8 @@ class MobileViewBottomSheetBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isFavorite = view.isFavorite;
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@ -100,6 +121,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
text: LocaleKeys.button_rename.tr(),
icon: FlowySvgs.view_item_rename_s,
iconSize: const Size.square(18),
enable: !isLocked,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.rename,
),
@ -118,6 +140,28 @@ class MobileViewBottomSheetBody extends StatelessWidget {
),
),
_divider(),
if (view.layout.isDatabaseView || view.layout.isDocumentView) ...[
MobileQuickActionButton(
text: LocaleKeys.disclosureAction_lockPage.tr(),
icon: FlowySvgs.lock_page_s,
iconSize: const Size.square(18),
rightIconBuilder: (context) => _LockPageRightIconBuilder(
onAction: onAction,
),
onTap: () {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
onAction(
MobileViewBottomSheetBodyAction.lockPage,
arguments: {
MobileViewBottomSheetBodyActionArguments.isLockedKey:
!isLocked,
},
);
},
),
_divider(),
],
MobileQuickActionButton(
text: LocaleKeys.button_duplicate.tr(),
icon: FlowySvgs.duplicate_s,
@ -144,6 +188,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
icon: FlowySvgs.trash_s,
iconColor: Theme.of(context).colorScheme.error,
iconSize: const Size.square(18),
enable: !isLocked,
onTap: () => onAction(
MobileViewBottomSheetBodyAction.delete,
),
@ -206,3 +251,36 @@ class MobileViewBottomSheetBody extends StatelessWidget {
Widget _divider() => const MobileQuickActionDivider();
}
class _LockPageRightIconBuilder extends StatelessWidget {
const _LockPageRightIconBuilder({
required this.onAction,
});
final MobileViewBottomSheetBodyActionCallback onAction;
@override
Widget build(BuildContext context) {
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return SizedBox(
width: 46,
height: 30,
child: FittedBox(
fit: BoxFit.fill,
child: CupertinoSwitch(
value: isLocked,
activeTrackColor: Theme.of(context).colorScheme.primary,
onChanged: (value) {
onAction(
MobileViewBottomSheetBodyAction.lockPage,
arguments: {
MobileViewBottomSheetBodyActionArguments.isLockedKey: !value,
},
);
},
),
),
);
}
}

View File

@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@ -131,6 +132,11 @@ enum MobilePaneActionType {
BlocProvider.value(value: favoriteBloc),
if (recentViewsBloc != null)
BlocProvider.value(value: recentViewsBloc),
BlocProvider(
create: (_) =>
ViewLockStatusBloc(view: viewBloc.state.view)
..add(const ViewLockStatusEvent.initial()),
),
],
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {

View File

@ -11,6 +11,7 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobi
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_board/appflowy_board.dart';
@ -142,6 +143,8 @@ class _BoardContentState extends State<_BoardContent> {
return state.maybeMap(
orElse: () => const SizedBox.shrink(),
ready: (state) {
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
final showCreateGroupButton = context
.read<BoardBloc>()
.groupingFieldType
@ -159,15 +162,20 @@ class _BoardContentState extends State<_BoardContent> {
padding: config.groupHeaderPadding,
)
: const HSpace(16),
trailing: showCreateGroupButton
trailing: showCreateGroupButton && !isLocked
? const MobileBoardTrailing()
: const HSpace(16),
headerBuilder: (_, groupData) => BlocProvider<BoardBloc>.value(
value: context.read<BoardBloc>(),
child: GroupCardHeader(
groupData: groupData,
),
),
headerBuilder: (_, groupData) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ??
false;
return IgnorePointer(
ignoring: isLocked,
child: GroupCardHeader(
groupData: groupData,
),
);
},
footerBuilder: _buildFooter,
cardBuilder: (_, column, columnItem) => _buildCard(
context: context,
@ -183,34 +191,39 @@ class _BoardContentState extends State<_BoardContent> {
}
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
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(
child: IgnorePointer(
ignoring: isLocked,
child: TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 8),
alignment: Alignment.centerLeft,
),
icon: FlowySvg(
FlowySvgs.add_m,
color: style.colorScheme.onSurface,
),
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
columnData.id,
OrderObjectPositionTypePB.End,
null,
null,
),
label: Text(
LocaleKeys.board_column_createNewCard.tr(),
style: style.textTheme.bodyMedium?.copyWith(
color: style.colorScheme.onSurface,
),
),
onPressed: () => context.read<BoardBloc>().add(
BoardEvent.createRow(
columnData.id,
OrderObjectPositionTypePB.End,
null,
null,
),
),
),
),
);
}
@ -230,6 +243,8 @@ class _BoardContentState extends State<_BoardContent> {
CardCellBuilder(databaseController: boardBloc.databaseController);
final groupItemId = groupItem.row.id + groupData.group.groupId;
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return Container(
key: ValueKey(groupItemId),
@ -237,31 +252,34 @@ class _BoardContentState extends State<_BoardContent> {
decoration: _makeBoxDecoration(context),
child: BlocProvider.value(
value: boardBloc,
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
child: IgnorePointer(
ignoring: isLocked,
child: RowCard(
fieldController: boardBloc.fieldController,
rowMeta: rowMeta,
viewId: boardBloc.viewId,
rowCache: boardBloc.rowCache,
groupingFieldId: groupItem.fieldInfo.id,
isEditing: false,
cellBuilder: cellBuilder,
onTap: (context) {
context.push(
MobileRowDetailPage.routeName,
extra: {
MobileRowDetailPage.argRowId: rowMeta.id,
MobileRowDetailPage.argDatabaseController:
context.read<BoardBloc>().databaseController,
},
);
},
onStartEditing: () {},
onEndEditing: () {},
styleConfiguration: RowCardStyleConfiguration(
cellStyleMap: mobileBoardCardCellStyleMap(context),
showAccessory: false,
),
userProfile: boardBloc.userProfile,
),
userProfile: boardBloc.userProfile,
),
),
);

View File

@ -12,6 +12,7 @@ class MobileQuickActionButton extends StatelessWidget {
this.iconColor,
this.iconSize,
this.enable = true,
this.rightIconBuilder,
});
final VoidCallback onTap;
@ -21,34 +22,39 @@ class MobileQuickActionButton extends StatelessWidget {
final Color? iconColor;
final Size? iconSize;
final bool enable;
final WidgetBuilder? rightIconBuilder;
@override
Widget build(BuildContext context) {
final iconSize = this.iconSize ?? const Size.square(18);
return InkWell(
onTap: enable ? onTap : null,
overlayColor:
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
FlowySvg(
icon,
size: iconSize,
color: enable ? iconColor : Theme.of(context).disabledColor,
),
HSpace(30 - iconSize.width),
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: enable ? textColor : Theme.of(context).disabledColor,
return Opacity(
opacity: enable ? 1.0 : 0.5,
child: InkWell(
onTap: enable ? onTap : null,
overlayColor:
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
FlowySvg(
icon,
size: iconSize,
color: iconColor,
),
),
],
HSpace(30 - iconSize.width),
Expanded(
child: FlowyText.regular(
text,
fontSize: 16,
color: textColor,
),
),
if (rightIconBuilder != null) rightIconBuilder!(context),
],
),
),
),
);

View File

@ -37,6 +37,7 @@ class FlowyOptionTile extends StatelessWidget {
this.backgroundColor,
this.fontFamily,
this.height,
this.enable = true,
});
factory FlowyOptionTile.text({
@ -49,6 +50,7 @@ class FlowyOptionTile extends StatelessWidget {
Widget? trailing,
VoidCallback? onTap,
double? height,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
@ -61,6 +63,7 @@ class FlowyOptionTile extends StatelessWidget {
leading: leftIcon,
trailing: trailing,
height: height,
enable: enable,
);
}
@ -77,6 +80,7 @@ class FlowyOptionTile extends StatelessWidget {
Widget? trailing,
String? textFieldHintText,
bool autofocus = false,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.textField,
@ -90,6 +94,7 @@ class FlowyOptionTile extends StatelessWidget {
onTextChanged: onTextChanged,
onTextSubmitted: onTextSubmitted,
autofocus: autofocus,
enable: enable,
);
}
@ -105,6 +110,7 @@ class FlowyOptionTile extends StatelessWidget {
bool showBottomBorder = true,
String? fontFamily,
Color? backgroundColor,
bool enable = true,
}) {
return FlowyOptionTile._(
key: key,
@ -119,6 +125,7 @@ class FlowyOptionTile extends StatelessWidget {
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
enable: enable,
trailing: isSelected
? const FlowySvg(
FlowySvgs.m_blue_check_s,
@ -136,6 +143,7 @@ class FlowyOptionTile extends StatelessWidget {
bool showTopBorder = true,
bool showBottomBorder = true,
Widget? leftIcon,
bool enable = true,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.toggle,
@ -146,6 +154,7 @@ class FlowyOptionTile extends StatelessWidget {
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: _Toggle(value: isSelected, onChanged: onValueChanged),
enable: enable,
);
}
@ -181,11 +190,13 @@ class FlowyOptionTile extends StatelessWidget {
final double? height;
final bool enable;
@override
Widget build(BuildContext context) {
final leadingWidget = _buildLeading();
final child = FlowyOptionDecorateBox(
Widget child = FlowyOptionDecorateBox(
color: backgroundColor,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
@ -209,12 +220,21 @@ class FlowyOptionTile extends StatelessWidget {
if (type == FlowyOptionTileType.checkbox ||
type == FlowyOptionTileType.toggle ||
type == FlowyOptionTileType.text) {
return GestureDetector(
child = GestureDetector(
onTap: onTap,
child: child,
);
}
if (!enable) {
child = Opacity(
opacity: 0.5,
child: IgnorePointer(
child: child,
),
);
}
return child;
}

View File

@ -379,7 +379,8 @@ class _CalendarPageState extends State<CalendarPage> {
// is implemnted in the develop branch(WIP). Will be replaced with that.
final events = calenderEvents.map((value) => value.event!).toList()
..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp));
final isLocked = context.watch<ViewLockStatusBloc>().state.isLocked;
final isLocked =
context.watch<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return IgnorePointer(
ignoring: isLocked,

View File

@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@ -182,6 +183,8 @@ class _GridPageContentState extends State<GridPageContent> {
@override
Widget build(BuildContext context) {
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return BlocListener<GridBloc, GridState>(
listenWhen: (previous, current) =>
previous.createdRow != current.createdRow,
@ -215,7 +218,7 @@ class _GridPageContentState extends State<GridPageContent> {
),
],
),
if (!widget.shrinkWrap)
if (!widget.shrinkWrap && !isLocked)
Positioned(
bottom: 16,
right: 16,
@ -356,7 +359,7 @@ class _GridRows extends StatelessWidget {
final databaseController = context.read<GridBloc>().databaseController;
final child = MobileGridRow(
Widget child = MobileGridRow(
key: ValueKey(rowMeta.id),
rowId: rowId,
isDraggable: isDraggable,
@ -373,12 +376,20 @@ class _GridRows extends StatelessWidget {
);
if (animation != null) {
return SizeTransition(
child = SizeTransition(
sizeFactor: animation,
child: child,
);
}
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
if (isLocked) {
child = IgnorePointer(
child: child,
);
}
return child;
}
}

View File

@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart';
import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -39,6 +40,8 @@ class _MobileGridHeaderState extends State<MobileGridHeader> {
Widget build(BuildContext context) {
final fieldController =
context.read<GridBloc>().databaseController.fieldController;
final isLocked =
context.read<ViewLockStatusBloc?>()?.state.isLocked ?? false;
return BlocProvider(
create: (context) {
return GridHeaderBloc(
@ -76,12 +79,15 @@ class _MobileGridHeaderState extends State<MobileGridHeader> {
);
},
),
SizedBox(
height: _kGridHeaderHeight,
child: _GridHeader(
viewId: widget.viewId,
fieldController: fieldController,
scrollController: widget.reorderableController,
IgnorePointer(
ignoring: isLocked,
child: SizedBox(
height: _kGridHeaderHeight,
child: _GridHeader(
viewId: widget.viewId,
fieldController: fieldController,
scrollController: widget.reorderableController,
),
),
),
],

View File

@ -88,8 +88,6 @@ class DatabaseTabBarView extends StatelessWidget {
child: BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
builder: (innerContext, state) {
final layout = state.tabBars[state.selectedIndex].layout;
final isLocked =
context.read<ViewBloc?>()?.state.view.isLocked ?? false;
final Widget child = Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -126,10 +124,6 @@ class DatabaseTabBarView extends StatelessWidget {
],
);
if (isLocked) {
return IgnorePointer(child: child);
}
return child;
},
),

View File

@ -323,6 +323,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
child: AppFlowyEditor(
editorState: widget.editorState,
editable: !isViewDeleted && !isLocked,
disableSelectionService: UniversalPlatform.isMobile && isLocked,
disableKeyboardService: UniversalPlatform.isMobile && isLocked,
editorScrollController: editorScrollController,
// setup the auto focus parameters
autoFocus: widget.autoFocus ?? autoFocus,
@ -348,6 +350,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage>
contextMenuItems: customContextMenuItems,
// customize the header and footer.
header: widget.header,
footer: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {

View File

@ -81,9 +81,9 @@ class ViewTitleBar extends StatelessWidget {
},
builder: (context, state) {
if (state.isLocked) {
return _Lock();
return LockedPageStatus();
} else if (!state.isLocked && state.lockCounter > 0) {
return _ReLock();
return ReLockedPageStatus();
}
return const SizedBox.shrink();
},
@ -386,8 +386,8 @@ class _ViewTitleState extends State<ViewTitle> {
}
}
class _Lock extends StatelessWidget {
const _Lock();
class LockedPageStatus extends StatelessWidget {
const LockedPageStatus({super.key});
@override
Widget build(BuildContext context) {
@ -424,8 +424,8 @@ class _Lock extends StatelessWidget {
}
}
class _ReLock extends StatelessWidget {
const _ReLock();
class ReLockedPageStatus extends StatelessWidget {
const ReLockedPageStatus({super.key});
@override
Widget build(BuildContext context) {

View File

@ -90,8 +90,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "9b5c461"
resolved-ref: "9b5c46153a769affc9e5d85ff91115796e97bce8"
ref: "6f5c957"
resolved-ref: "6f5c957049c90d942e56314171c7630b3d6d858d"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "5.0.0"

View File

@ -178,7 +178,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "9b5c461"
ref: "6f5c957"
appflowy_editor_plugins:
git: