Merge branch 'main' into update-client-api

This commit is contained in:
Zack Fu Zi Xiang 2024-07-29 09:51:21 +08:00
commit af1769164a
No known key found for this signature in database
38 changed files with 846 additions and 309 deletions

View File

@ -175,7 +175,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4

View File

@ -23,6 +23,9 @@ class DocumentPageStyleBloc
await event.when( await event.when(
initial: () async { initial: () async {
try { try {
if (view.id.isEmpty) {
return;
}
final layoutObject = final layoutObject =
await ViewBackendService.getView(view.id).fold( await ViewBackendService.getView(view.id).fold(
(s) => jsonDecode(s.extra), (s) => jsonDecode(s.extra),

View File

@ -0,0 +1,54 @@
import 'package:appflowy/shared/feedback_gesture_detector.dart';
import 'package:flutter/material.dart';
class AnimatedGestureDetector extends StatefulWidget {
const AnimatedGestureDetector({
super.key,
this.scaleFactor = 0.98,
this.feedback = true,
this.duration = const Duration(milliseconds: 100),
this.alignment = Alignment.center,
this.behavior = HitTestBehavior.opaque,
required this.onTapUp,
required this.child,
});
final Widget child;
final double scaleFactor;
final Duration duration;
final Alignment alignment;
final bool feedback;
final HitTestBehavior behavior;
final VoidCallback onTapUp;
@override
State<AnimatedGestureDetector> createState() =>
_AnimatedGestureDetectorState();
}
class _AnimatedGestureDetectorState extends State<AnimatedGestureDetector> {
double scale = 1.0;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: widget.behavior,
onTapUp: (details) {
setState(() => scale = 1.0);
HapticFeedbackType.vibrate.call();
widget.onTapUp();
},
onTapDown: (details) {
setState(() => scale = widget.scaleFactor);
},
child: AnimatedScale(
scale: scale,
alignment: widget.alignment,
duration: widget.duration,
child: widget.child,
),
);
}
}

View File

@ -24,9 +24,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0, height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.icon_document_s, FlowySvgs.icon_document_s,
size: Size.square(18), size: Size.square(20),
), ),
showTopBorder: false, showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Document), onTap: () => onAction(ViewLayoutPB.Document),
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
@ -34,9 +35,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0, height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.icon_grid_s, FlowySvgs.icon_grid_s,
size: Size.square(18), size: Size.square(20),
), ),
showTopBorder: false, showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Grid), onTap: () => onAction(ViewLayoutPB.Grid),
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
@ -44,9 +46,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0, height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.icon_board_s, FlowySvgs.icon_board_s,
size: Size.square(18), size: Size.square(20),
), ),
showTopBorder: false, showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Board), onTap: () => onAction(ViewLayoutPB.Board),
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
@ -54,9 +57,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0, height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.icon_calendar_s, FlowySvgs.icon_calendar_s,
size: Size.square(18), size: Size.square(20),
), ),
showTopBorder: false, showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Calendar), onTap: () => onAction(ViewLayoutPB.Calendar),
), ),
FlowyOptionTile.text( FlowyOptionTile.text(
@ -64,9 +68,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0, height: 52.0,
leftIcon: const FlowySvg( leftIcon: const FlowySvg(
FlowySvgs.chat_ai_page_s, FlowySvgs.chat_ai_page_s,
size: Size.square(18), size: Size.square(20),
), ),
showTopBorder: false, showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Chat), onTap: () => onAction(ViewLayoutPB.Chat),
), ),
], ],

View File

@ -1,4 +1,3 @@
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/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
@ -6,6 +5,7 @@ import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -109,16 +109,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
await _showConfirmDialog( await _showConfirmDialog(
onDelete: () { onDelete: () {
recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId])); recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId]));
fToast.showToast(
child: const _RemoveToast(),
positionedToastBuilder: (context, child) {
return Positioned.fill(
top: 450,
child: child,
);
},
);
}, },
); );
} }
@ -136,38 +126,14 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
), ),
onRightButtonPressed: (context) { onRightButtonPressed: (context) {
onDelete(); onDelete();
Navigator.pop(context); Navigator.pop(context);
showToastNotification(
context,
message: LocaleKeys.sideBar_removeSuccess.tr(),
);
}, },
); );
} }
} }
class _RemoveToast extends StatelessWidget {
const _RemoveToast();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: const Color(0xE5171717),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.success_s,
blendMode: null,
),
const HSpace(8.0),
FlowyText.regular(
LocaleKeys.sideBar_removeSuccess.tr(),
fontSize: 16.0,
color: Colors.white,
),
],
),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; 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/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -40,18 +41,32 @@ enum MobilePaneActionType {
backgroundColor: const Color(0xFFFA217F), backgroundColor: const Color(0xFFFA217F),
svg: FlowySvgs.favorite_section_remove_from_favorite_s, svg: FlowySvgs.favorite_section_remove_from_favorite_s,
size: 24.0, size: 24.0,
onPressed: (context) => context onPressed: (context) {
.read<FavoriteBloc>() showToastNotification(
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)), context,
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
);
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
},
); );
case MobilePaneActionType.addToFavorites: case MobilePaneActionType.addToFavorites:
return MobileSlideActionButton( return MobileSlideActionButton(
backgroundColor: const Color(0xFF00C8FF), backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.favorite_s, svg: FlowySvgs.favorite_s,
size: 24.0, size: 24.0,
onPressed: (context) => context onPressed: (context) {
.read<FavoriteBloc>() showToastNotification(
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)), context,
message: LocaleKeys.button_favoriteSuccessfully.tr(),
);
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
},
); );
case MobilePaneActionType.add: case MobilePaneActionType.add:
return MobileSlideActionButton( return MobileSlideActionButton(
@ -69,6 +84,7 @@ enum MobilePaneActionType {
showDragHandle: true, showDragHandle: true,
showCloseButton: true, showCloseButton: true,
useRootNavigator: true, useRootNavigator: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
builder: (sheetContext) { builder: (sheetContext) {
return AddNewPageWidgetBottomSheet( return AddNewPageWidgetBottomSheet(
@ -145,8 +161,6 @@ enum MobilePaneActionType {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites ? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites, : MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider, MobileViewItemBottomSheetBodyAction.divider,
if (view.layout != ViewLayoutPB.Chat)
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider, MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent, MobileViewItemBottomSheetBodyAction.removeFromRecent,
]; ];
@ -156,7 +170,6 @@ enum MobilePaneActionType {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites ? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites, : MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider, MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
]; ];
} }
} }
@ -181,12 +194,13 @@ ActionPane buildEndActionPane(
bool needSpace = true, bool needSpace = true,
MobilePageCardType? cardType, MobilePageCardType? cardType,
FolderSpaceType? spaceType, FolderSpaceType? spaceType,
required double spaceRatio,
}) { }) {
return ActionPane( return ActionPane(
motion: const ScrollMotion(), motion: const ScrollMotion(),
extentRatio: actions.length / 5, extentRatio: actions.length / spaceRatio,
children: [ children: [
if (needSpace) const HSpace(20), if (needSpace) const HSpace(60),
...actions.map( ...actions.map(
(action) => action.actionButton( (action) => action.actionButton(
context, context,

View File

@ -70,6 +70,7 @@ Future<T?> showMobileBottomSheet<T>(
backgroundColor ??= Theme.of(context).brightness == Brightness.light backgroundColor ??= Theme.of(context).brightness == Brightness.light
? const Color(0xFFF7F8FB) ? const Color(0xFFF7F8FB)
: const Color(0xFF23262B); : const Color(0xFF23262B);
barrierColor ??= Colors.black.withOpacity(0.3);
return showModalBottomSheet<T>( return showModalBottomSheet<T>(
context: context, context: context,
@ -226,10 +227,14 @@ class BottomSheetHeader extends StatelessWidget {
), ),
), ),
Align( Align(
child: FlowyText( child: Container(
title, constraints: const BoxConstraints(maxWidth: 250),
fontSize: 16.0, child: FlowyText(
fontWeight: FontWeight.w500, title,
fontSize: 17.0,
fontWeight: FontWeight.w500,
overflow: TextOverflow.ellipsis,
),
), ),
), ),
if (showDoneButton) if (showDoneButton)

View File

@ -100,8 +100,6 @@ class _FavoriteViews extends StatelessWidget {
child: ListView.separated( child: ListView.separated(
key: const PageStorageKey('favorite_views_page_storage_key'), key: const PageStorageKey('favorite_views_page_storage_key'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding + bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom, MediaQuery.of(context).padding.bottom,
), ),

View File

@ -72,6 +72,7 @@ class MobileFavoriteFolder extends StatelessWidget {
MobilePaneActionType.more, MobilePaneActionType.more,
], ],
spaceType: FolderSpaceType.favorite, spaceType: FolderSpaceType.favorite,
spaceRatio: 5,
), ),
), ),
), ),

View File

@ -29,8 +29,6 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
top: HomeSpaceViewSizes.mVerticalPadding, top: HomeSpaceViewSizes.mVerticalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding + bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom, MediaQuery.of(context).padding.bottom,

View File

@ -9,6 +9,7 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -91,7 +92,12 @@ class MobileFolders extends StatelessWidget {
children: [ children: [
..._buildSpaceOrSection(context, state), ..._buildSpaceOrSection(context, state),
const VSpace(4.0), const VSpace(4.0),
const _TrashButton(), const Padding(
padding: EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
),
child: _TrashButton(),
),
], ],
), ),
); );

View File

@ -1,5 +1,6 @@
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/base/gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
@ -113,8 +114,9 @@ class _MobileWorkspace extends StatelessWidget {
if (currentWorkspace == null) { if (currentWorkspace == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return GestureDetector( return AnimatedGestureDetector(
onTap: () { alignment: Alignment.centerLeft,
onTapUp: () {
context.read<UserWorkspaceBloc>().add( context.read<UserWorkspaceBloc>().add(
const UserWorkspaceEvent.fetchWorkspaces(), const UserWorkspaceEvent.fetchWorkspaces(),
); );
@ -143,7 +145,7 @@ class _MobileWorkspace extends StatelessWidget {
: const HSpace(8), : const HSpace(8),
FlowyText.semibold( FlowyText.semibold(
currentWorkspace.name, currentWorkspace.name,
fontSize: 16.0, fontSize: 20.0,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
], ],
@ -162,9 +164,10 @@ class _MobileWorkspace extends StatelessWidget {
showHeader: true, showHeader: true,
showDragHandle: true, showDragHandle: true,
showCloseButton: true, showCloseButton: true,
useRootNavigator: true,
title: LocaleKeys.workspace_menuTitle.tr(), title: LocaleKeys.workspace_menuTitle.tr(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) { builder: (sheetContext) {
return BlocProvider.value( return BlocProvider.value(
value: context.read<UserWorkspaceBloc>(), value: context.read<UserWorkspaceBloc>(),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>( child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
@ -179,7 +182,7 @@ class _MobileWorkspace extends StatelessWidget {
currentWorkspace: currentWorkspace, currentWorkspace: currentWorkspace,
workspaces: workspaces, workspaces: workspaces,
onWorkspaceSelected: (workspace) { onWorkspaceSelected: (workspace) {
context.pop(); Navigator.of(sheetContext).pop();
if (workspace == currentWorkspace) { if (workspace == currentWorkspace) {
return; return;

View File

@ -72,8 +72,6 @@ class _RecentViews extends StatelessWidget {
child: ListView.separated( child: ListView.separated(
key: const PageStorageKey('recent_views_page_storage_key'), key: const PageStorageKey('recent_views_page_storage_key'),
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding + bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom, MediaQuery.of(context).padding.bottom,
), ),

View File

@ -82,6 +82,7 @@ class MobileSectionFolder extends StatelessWidget {
], ],
spaceType: spaceType, spaceType: spaceType,
needSpace: false, needSpace: false,
spaceRatio: 5,
); );
}, },
), ),

View File

@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart'; import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/mobile/presentation/base/gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/appflowy_network_image.dart';
@ -15,6 +16,7 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart'; import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
@ -76,13 +78,14 @@ class MobileViewPage extends StatelessWidget {
: MobilePaneActionType.addToFavorites, : MobilePaneActionType.addToFavorites,
], ],
cardType: type, cardType: type,
spaceRatio: 4,
), ),
child: GestureDetector( child: AnimatedGestureDetector(
behavior: HitTestBehavior.opaque, onTapUp: () => context.pushView(view),
onTapUp: (_) => context.pushView(view),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
Expanded(child: _buildDescription(context, state)), Expanded(child: _buildDescription(context, state)),
const HSpace(20.0), const HSpace(20.0),
SizedBox( SizedBox(
@ -90,6 +93,7 @@ class MobileViewPage extends StatelessWidget {
height: 60, height: 60,
child: _buildCover(context, state), child: _buildCover(context, state),
), ),
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
], ],
), ),
), ),
@ -211,7 +215,7 @@ class MobileViewPage extends StatelessWidget {
Widget _buildLastViewed(BuildContext context) { Widget _buildLastViewed(BuildContext context) {
final textColor = Theme.of(context).isLightMode final textColor = Theme.of(context).isLightMode
? const Color(0xFF171717) ? const Color(0x7F171717)
: Colors.white.withOpacity(0.45); : Colors.white.withOpacity(0.45);
if (timestamp == null) { if (timestamp == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();

View File

@ -50,23 +50,17 @@ class _MobileSpaceState extends State<MobileSpace> {
MobileSpaceHeader( MobileSpaceHeader(
isExpanded: state.isExpanded, isExpanded: state.isExpanded,
space: currentSpace, space: currentSpace,
onAdded: () { onAdded: () => _showCreatePageMenu(currentSpace),
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document,
index: 0,
),
);
context.read<SpaceBloc>().add(
SpaceEvent.expand(currentSpace, true),
);
},
onPressed: () => _showSpaceMenu(context), onPressed: () => _showSpaceMenu(context),
), ),
_Pages( Padding(
key: ValueKey(currentSpace.id), padding: const EdgeInsets.only(
space: currentSpace, left: HomeSpaceViewSizes.mHorizontalPadding,
),
child: _Pages(
key: ValueKey(currentSpace.id),
space: currentSpace,
),
), ),
], ],
); );
@ -82,6 +76,7 @@ class _MobileSpaceState extends State<MobileSpace> {
showDragHandle: true, showDragHandle: true,
showCloseButton: true, showCloseButton: true,
showDoneButton: true, showDoneButton: true,
useRootNavigator: true,
title: LocaleKeys.space_title.tr(), title: LocaleKeys.space_title.tr(),
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) { builder: (_) {
@ -104,6 +99,38 @@ class _MobileSpaceState extends State<MobileSpace> {
), ),
); );
} }
void _showCreatePageMenu(ViewPB space) {
final title = space.name;
showMobileBottomSheet(
context,
showHeader: true,
title: title,
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (sheetContext) {
return AddNewPageWidgetBottomSheet(
view: space,
onAction: (layout) {
Navigator.of(sheetContext).pop();
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: layout,
index: 0,
),
);
context.read<SpaceBloc>().add(
SpaceEvent.expand(space, true),
);
},
);
},
);
}
} }
class _Pages extends StatelessWidget { class _Pages extends StatelessWidget {
@ -148,7 +175,7 @@ class _Pages extends StatelessWidget {
MobilePaneActionType.add, MobilePaneActionType.add,
], ],
spaceType: spaceType, spaceType: spaceType,
needSpace: false, spaceRatio: 4,
); );
}, },
), ),

View File

@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -30,6 +31,7 @@ class MobileSpaceHeader extends StatelessWidget {
height: 48, height: 48,
child: Row( child: Row(
children: [ children: [
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
SpaceIcon( SpaceIcon(
dimension: 24, dimension: 24,
space: space, space: space,
@ -49,8 +51,15 @@ class MobileSpaceHeader extends StatelessWidget {
GestureDetector( GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: onAdded, onTap: onAdded,
child: const FlowySvg( child: Container(
FlowySvgs.m_space_add_s, // expand the touch area
margin: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
vertical: 8.0,
),
child: const FlowySvg(
FlowySvgs.m_space_add_s,
),
), ),
), ),
], ],

View File

@ -59,6 +59,7 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
children: [ children: [
FlowyText.medium( FlowyText.medium(
space.name, space.name,
fontSize: 16.0,
), ),
const HSpace(6.0), const HSpace(6.0),
if (space.spacePermission == SpacePermission.private) if (space.spacePermission == SpacePermission.private)
@ -68,16 +69,17 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
), ),
], ],
), ),
margin: const EdgeInsets.symmetric(horizontal: 12.0),
iconPadding: 10, iconPadding: 10,
leftIcon: SpaceIcon( leftIcon: SpaceIcon(
dimension: 24, dimension: 24,
space: space, space: space,
cornerRadius: 6.0, cornerRadius: 6.0,
), ),
leftIconSize: const Size.square(20), leftIconSize: const Size.square(24),
rightIcon: isSelected rightIcon: isSelected
? const FlowySvg( ? const FlowySvg(
FlowySvgs.workspace_selected_s, FlowySvgs.m_blue_check_s,
blendMode: null, blendMode: null,
) )
: null, : null,

View File

@ -138,17 +138,20 @@ class _WorkspaceMenuItem extends StatelessWidget {
height: 60, height: 60,
showTopBorder: showTopBorder, showTopBorder: showTopBorder,
showBottomBorder: false, showBottomBorder: false,
leftIcon: WorkspaceIcon( leftIcon: Padding(
enableEdit: false, padding: const EdgeInsets.symmetric(horizontal: 4.0),
iconSize: 26, child: WorkspaceIcon(
fontSize: 16.0, enableEdit: false,
workspace: workspace, iconSize: 26,
onSelected: (result) => context.read<UserWorkspaceBloc>().add( fontSize: 16.0,
UserWorkspaceEvent.updateWorkspaceIcon( workspace: workspace,
workspace.workspaceId, onSelected: (result) => context.read<UserWorkspaceBloc>().add(
result.emoji, UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
), ),
), ),
), ),
trailing: workspace.workspaceId == currentWorkspace.workspaceId trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg( ? const FlowySvg(

View File

@ -50,6 +50,9 @@ class MobileBottomNavigationBar extends StatelessWidget {
final backgroundColor = isLightMode final backgroundColor = isLightMode
? Colors.white.withOpacity(0.95) ? Colors.white.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95); : const Color(0xFF23262B).withOpacity(0.95);
final borderColor = isLightMode
? const Color(0x141F2329)
: const Color(0xFF23262B).withOpacity(0.5);
return Scaffold( return Scaffold(
body: navigationShell, body: navigationShell,
extendBody: true, extendBody: true,
@ -62,9 +65,7 @@ class MobileBottomNavigationBar extends StatelessWidget {
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
border: isLightMode border: isLightMode
? Border( ? Border(top: BorderSide(color: borderColor))
top: BorderSide(color: Theme.of(context).dividerColor),
)
: null, : null,
color: backgroundColor, color: backgroundColor,
), ),

View File

@ -300,7 +300,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
) )
: Opacity( : Opacity(
opacity: 0.7, opacity: 0.7,
child: widget.view.defaultIcon(), child: widget.view.defaultIcon(size: const Size.square(18)),
); );
return SizedBox(width: 18.0, child: icon); return SizedBox(width: 18.0, child: icon);
} }

View File

@ -13,9 +13,9 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
: listener = LocalLLMListener(), : listener = LocalLLMListener(),
super(const ChatInputState(aiType: _AppFlowyAI())) { super(const ChatInputState(aiType: _AppFlowyAI())) {
listener.start( listener.start(
stateCallback: (pluginState) { chatStateCallback: (aiState) {
if (!isClosed) { if (!isClosed) {
add(ChatInputEvent.updateState(pluginState)); add(ChatInputEvent.updateState(aiState));
} }
}, },
); );
@ -37,18 +37,26 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
) async { ) async {
await event.when( await event.when(
started: () async { started: () async {
final result = await ChatEventGetLocalAIPluginState().send(); final result = await ChatEventGetLocalAIChatState().send();
result.fold( result.fold(
(pluginState) { (aiState) {
if (!isClosed) { if (!isClosed) {
add(ChatInputEvent.updateState(pluginState)); add(
ChatInputEvent.updateState(aiState),
);
} }
}, },
(err) => Log.error(err.toString()), (err) {
Log.error(err.toString());
},
); );
}, },
updateState: (LocalAIPluginStatePB aiPluginState) { updateState: (aiState) {
emit(const ChatInputState(aiType: _AppFlowyAI())); if (aiState.enabled) {
emit(const ChatInputState(aiType: _LocalAI()));
} else {
emit(const ChatInputState(aiType: _AppFlowyAI()));
}
}, },
); );
} }
@ -57,8 +65,8 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
@freezed @freezed
class ChatInputEvent with _$ChatInputEvent { class ChatInputEvent with _$ChatInputEvent {
const factory ChatInputEvent.started() = _Started; const factory ChatInputEvent.started() = _Started;
const factory ChatInputEvent.updateState(LocalAIPluginStatePB aiPluginState) = const factory ChatInputEvent.updateState(LocalAIChatPB aiState) =
_UpdatePluginState; _UpdateAIState;
} }
@freezed @freezed

View File

@ -23,7 +23,7 @@ import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat; import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'presentation/chat_input.dart'; import 'presentation/chat_input/chat_input.dart';
import 'presentation/chat_popmenu.dart'; import 'presentation/chat_popmenu.dart';
import 'presentation/chat_theme.dart'; import 'presentation/chat_theme.dart';
import 'presentation/chat_user_invalid_message.dart'; import 'presentation/chat_user_invalid_message.dart';
@ -82,7 +82,9 @@ class AIChatPage extends StatelessWidget {
userProfile: userProfile, userProfile: userProfile,
)..add(const ChatEvent.initialLoad()), )..add(const ChatEvent.initialLoad()),
), ),
BlocProvider(create: (_) => ChatInputBloc()), BlocProvider(
create: (_) => ChatInputBloc()..add(const ChatInputEvent.started()),
),
], ],
child: BlocListener<ChatFileBloc, ChatFileState>( child: BlocListener<ChatFileBloc, ChatFileState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
@ -391,36 +393,39 @@ class _ChatContentPageState extends State<_ChatContentPage> {
padding: AIChatUILayout.safeAreaInsets(context), padding: AIChatUILayout.safeAreaInsets(context),
child: BlocBuilder<ChatInputBloc, ChatInputState>( child: BlocBuilder<ChatInputBloc, ChatInputState>(
builder: (context, state) { builder: (context, state) {
return state.aiType.when( final hintText = state.aiType.when(
appflowyAI: () => Column( appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
children: [ localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
BlocSelector<ChatBloc, ChatState, LoadingState>( );
selector: (state) => state.streamingStatus,
builder: (context, state) { return Column(
return ChatInput( children: [
chatId: widget.view.id, BlocSelector<ChatBloc, ChatState, LoadingState>(
onSendPressed: (message) => selector: (state) => state.streamingStatus,
onSendPressed(context, message.text), builder: (context, state) {
isStreaming: state != const LoadingState.finish(), return ChatInput(
onStopStreaming: () { chatId: widget.view.id,
context onSendPressed: (message) =>
.read<ChatBloc>() onSendPressed(context, message.text),
.add(const ChatEvent.stopStream()); isStreaming: state != const LoadingState.finish(),
}, onStopStreaming: () {
); context
}, .read<ChatBloc>()
.add(const ChatEvent.stopStream());
},
hintText: hintText,
);
},
),
const VSpace(6),
Opacity(
opacity: 0.6,
child: FlowyText(
LocaleKeys.chat_aiMistakePrompt.tr(),
fontSize: 12,
), ),
const VSpace(6), ),
Opacity( ],
opacity: 0.6,
child: FlowyText(
LocaleKeys.chat_aiMistakePrompt.tr(),
fontSize: 12,
),
),
],
),
localAI: () => const SizedBox.shrink(),
); );
}, },
), ),

View File

@ -4,7 +4,7 @@ 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/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/shared/markdown_to_document.dart';

View File

@ -0,0 +1,237 @@
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
abstract class ChatActionMenuItem {
String get title;
}
abstract class ChatActionHandler {
List<ChatActionMenuItem> get items;
void onEnter();
void onSelected(ChatActionMenuItem item);
void onExit();
}
abstract class ChatAnchor {
GlobalKey get anchorKey;
LayerLink get layerLink;
}
const int _itemHeight = 34;
const int _itemVerticalPadding = 4;
class ChatActionsMenu {
ChatActionsMenu({
required this.anchor,
required this.context,
required this.handler,
required this.style,
});
final BuildContext context;
final ChatAnchor anchor;
final ChatActionsMenuStyle style;
final ChatActionHandler handler;
OverlayEntry? _overlayEntry;
void dismiss() {
_overlayEntry?.remove();
_overlayEntry = null;
handler.onExit();
}
void show() {
WidgetsBinding.instance.addPostFrameCallback((_) => _show());
}
void _show() {
if (_overlayEntry != null) {
dismiss();
}
if (anchor.anchorKey.currentContext == null) {
return;
}
handler.onEnter();
final height = handler.items.length * (_itemHeight + _itemVerticalPadding);
_overlayEntry = OverlayEntry(
builder: (context) => Stack(
children: [
CompositedTransformFollower(
link: anchor.layerLink,
showWhenUnlinked: false,
offset: Offset(0, -height - 4),
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 200,
maxWidth: 200,
maxHeight: 200,
),
child: DecoratedBox(
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6.0),
),
child: ActionList(
handler: handler,
onDismiss: () => dismiss(),
),
),
),
),
),
],
),
);
Overlay.of(context).insert(_overlayEntry!);
}
}
class _ActionItem extends StatelessWidget {
const _ActionItem({
required this.item,
required this.onTap,
required this.isSelected,
});
final ChatActionMenuItem item;
final VoidCallback? onTap;
final bool isSelected;
@override
Widget build(BuildContext context) {
return Container(
height: _itemHeight.toDouble(),
padding: const EdgeInsets.symmetric(vertical: _itemVerticalPadding / 2.0),
decoration: BoxDecoration(
color: isSelected
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(4.0),
),
child: FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 6),
iconPadding: 10.0,
text: FlowyText.regular(
item.title,
),
onTap: onTap,
),
);
}
}
class ActionList extends StatefulWidget {
const ActionList({super.key, required this.handler, required this.onDismiss});
final ChatActionHandler handler;
final VoidCallback? onDismiss;
@override
State<ActionList> createState() => _ActionListState();
}
class _ActionListState extends State<ActionList> {
final FocusScopeNode _focusNode =
FocusScopeNode(debugLabel: 'ChatActionsMenu');
int _selectedIndex = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void _handleKeyPress(event) {
setState(() {
// ignore: deprecated_member_use
if (event is KeyDownEvent || event is RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
_selectedIndex = (_selectedIndex + 1) % widget.handler.items.length;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
_selectedIndex = (_selectedIndex - 1 + widget.handler.items.length) %
widget.handler.items.length;
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
widget.handler.onSelected(widget.handler.items[_selectedIndex]);
widget.onDismiss?.call();
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
widget.onDismiss?.call();
}
}
});
}
@override
Widget build(BuildContext context) {
return FocusScope(
node: _focusNode,
onKey: (node, event) {
_handleKeyPress(event);
return KeyEventResult.handled;
},
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(8),
children: widget.handler.items.asMap().entries.map((entry) {
final index = entry.key;
final ChatActionMenuItem item = entry.value;
return _ActionItem(
item: item,
onTap: () {
widget.handler.onSelected(item);
widget.onDismiss?.call();
},
isSelected: _selectedIndex == index,
);
}).toList(),
),
);
}
}
class ChatActionsMenuStyle {
ChatActionsMenuStyle({
required this.backgroundColor,
required this.groupTextColor,
required this.menuItemTextColor,
required this.menuItemSelectedColor,
required this.menuItemSelectedTextColor,
});
const ChatActionsMenuStyle.light()
: backgroundColor = Colors.white,
groupTextColor = const Color(0xFF555555),
menuItemTextColor = const Color(0xFF333333),
menuItemSelectedColor = const Color(0xFFE0F8FF),
menuItemSelectedTextColor = const Color.fromARGB(255, 56, 91, 247);
const ChatActionsMenuStyle.dark()
: backgroundColor = const Color(0xFF282E3A),
groupTextColor = const Color(0xFFBBC3CD),
menuItemTextColor = const Color(0xFFBBC3CD),
menuItemSelectedColor = const Color(0xFF00BCF0),
menuItemSelectedTextColor = const Color(0xFF131720);
final Color backgroundColor;
final Color groupTextColor;
final Color menuItemTextColor;
final Color menuItemSelectedColor;
final Color menuItemSelectedTextColor;
}

View File

@ -0,0 +1,48 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/material.dart';
class ChatInputAccessoryButton extends StatelessWidget {
const ChatInputAccessoryButton({
required this.onSendPressed,
required this.onStopStreaming,
required this.isStreaming,
super.key,
});
final void Function() onSendPressed;
final void Function() onStopStreaming;
final bool isStreaming;
@override
Widget build(BuildContext context) {
if (isStreaming) {
return FlowyIconButton(
width: 36,
icon: FlowySvg(
FlowySvgs.ai_stream_stop_s,
size: const Size.square(28),
color: Theme.of(context).colorScheme.primary,
),
onPressed: onStopStreaming,
radius: BorderRadius.circular(18),
fillColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
);
} else {
return FlowyIconButton(
width: 36,
fillColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
radius: BorderRadius.circular(18),
icon: FlowySvg(
FlowySvgs.send_s,
size: const Size.square(24),
color: Theme.of(context).colorScheme.primary,
),
onPressed: onSendPressed,
);
}
}
}

View File

@ -0,0 +1,76 @@
import 'package:appflowy/plugins/ai_chat/presentation/chat_inline_action_menu.dart';
import 'package:flutter/material.dart';
class ChatTextFieldInterceptor {
String previosText = "";
ChatActionHandler? onTextChanged(
String text,
TextEditingController textController,
FocusNode textFieldFocusNode,
) {
if (previosText == "/" && text == "/ ") {
final handler = IndexActionHandler(
textController: textController,
textFieldFocusNode: textFieldFocusNode,
) as ChatActionHandler;
return handler;
}
previosText = text;
return null;
}
}
class FixGrammarMenuItem extends ChatActionMenuItem {
@override
String get title => "Fix Grammar";
}
class ImproveWritingMenuItem extends ChatActionMenuItem {
@override
String get title => "Improve Writing";
}
class ChatWithFileMenuItem extends ChatActionMenuItem {
@override
String get title => "Chat With PDF";
}
class IndexActionHandler extends ChatActionHandler {
IndexActionHandler({
required this.textController,
required this.textFieldFocusNode,
});
final TextEditingController textController;
final FocusNode textFieldFocusNode;
@override
List<ChatActionMenuItem> get items => [
ChatWithFileMenuItem(),
FixGrammarMenuItem(),
ImproveWritingMenuItem(),
];
@override
void onSelected(ChatActionMenuItem item) {
textController.clear();
WidgetsBinding.instance.addPostFrameCallback(
(_) => textFieldFocusNode.requestFocus(),
);
}
@override
void onExit() {
if (!textFieldFocusNode.hasFocus) {
textFieldFocusNode.requestFocus();
}
}
@override
void onEnter() {
if (textFieldFocusNode.hasFocus) {
textFieldFocusNode.unfocus();
}
}
}

View File

@ -1,14 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_inline_action_menu.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'chat_accessory_button.dart';
class ChatInput extends StatefulWidget { class ChatInput extends StatefulWidget {
/// Creates [ChatInput] widget. /// Creates [ChatInput] widget.
const ChatInput({ const ChatInput({
@ -20,6 +19,7 @@ class ChatInput extends StatefulWidget {
this.options = const InputOptions(), this.options = const InputOptions(),
required this.isStreaming, required this.isStreaming,
required this.onStopStreaming, required this.onStopStreaming,
required this.hintText,
}); });
final bool? isAttachmentUploading; final bool? isAttachmentUploading;
@ -29,6 +29,7 @@ class ChatInput extends StatefulWidget {
final InputOptions options; final InputOptions options;
final String chatId; final String chatId;
final bool isStreaming; final bool isStreaming;
final String hintText;
@override @override
State<ChatInput> createState() => _ChatInputState(); State<ChatInput> createState() => _ChatInputState();
@ -36,6 +37,11 @@ class ChatInput extends StatefulWidget {
/// [ChatInput] widget state. /// [ChatInput] widget state.
class _ChatInputState extends State<ChatInput> { class _ChatInputState extends State<ChatInput> {
final GlobalKey _textFieldKey = GlobalKey();
final LayerLink _layerLink = LayerLink();
// final ChatTextFieldInterceptor _textFieldInterceptor =
// ChatTextFieldInterceptor();
late final _inputFocusNode = FocusNode( late final _inputFocusNode = FocusNode(
onKeyEvent: (node, event) { onKeyEvent: (node, event) {
if (event.physicalKey == PhysicalKeyboardKey.enter && if (event.physicalKey == PhysicalKeyboardKey.enter &&
@ -59,9 +65,9 @@ class _ChatInputState extends State<ChatInput> {
} }
}, },
); );
late TextEditingController _textController;
bool _sendButtonVisible = false; bool _sendButtonVisible = false;
late TextEditingController _textController;
@override @override
void initState() { void initState() {
@ -71,33 +77,15 @@ class _ChatInputState extends State<ChatInput> {
_handleSendButtonVisibilityModeChange(); _handleSendButtonVisibilityModeChange();
} }
void _handleSendButtonVisibilityModeChange() { @override
_textController.removeListener(_handleTextControllerChange); void dispose() {
_sendButtonVisible = _inputFocusNode.dispose();
_textController.text.trim() != '' || widget.isStreaming; _textController.dispose();
_textController.addListener(_handleTextControllerChange); super.dispose();
} }
void _handleSendPressed() { @override
final trimmedText = _textController.text.trim(); Widget build(BuildContext context) {
if (trimmedText != '') {
final partialText = types.PartialText(text: trimmedText);
widget.onSendPressed(partialText);
_textController.clear();
}
}
void _handleTextControllerChange() {
if (_textController.value.isComposingRangeValid) {
return;
}
setState(() {
_sendButtonVisible = _textController.text.trim() != '';
});
}
Widget _inputBuilder() {
const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6); const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6); const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
const inputPadding = EdgeInsets.all(6); const inputPadding = EdgeInsets.all(6);
@ -128,27 +116,78 @@ class _ChatInputState extends State<ChatInput> {
); );
} }
Padding _inputTextField(EdgeInsets textPadding) { void _handleSendButtonVisibilityModeChange() {
return Padding( _textController.removeListener(_handleTextControllerChange);
padding: textPadding, _sendButtonVisible =
child: TextField( _textController.text.trim() != '' || widget.isStreaming;
controller: _textController, _textController.addListener(_handleTextControllerChange);
focusNode: _inputFocusNode, }
decoration: InputDecoration(
border: InputBorder.none, void _handleSendPressed() {
hintText: LocaleKeys.chat_inputMessageHint.tr(), final trimmedText = _textController.text.trim();
hintStyle: TextStyle( if (trimmedText != '') {
color: AFThemeExtension.of(context).textColor.withOpacity(0.5), final partialText = types.PartialText(text: trimmedText);
widget.onSendPressed(partialText);
_textController.clear();
}
}
void _handleTextControllerChange() {
if (_textController.value.isComposingRangeValid) {
return;
}
setState(() {
_sendButtonVisible = _textController.text.trim() != '';
});
}
Widget _inputTextField(EdgeInsets textPadding) {
return CompositedTransformTarget(
link: _layerLink,
child: Padding(
padding: textPadding,
child: TextField(
key: _textFieldKey,
controller: _textController,
focusNode: _inputFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.hintText,
hintStyle: TextStyle(
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
),
), ),
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
),
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
maxLines: 10,
minLines: 1,
// onChanged: (text) {
// final handler = _textFieldInterceptor.onTextChanged(
// text,
// _textController,
// _inputFocusNode,
// );
// // If the handler is not null, it means that the text has been
// // recognized as a command.
// if (handler != null) {
// ChatActionsMenu(
// anchor: ChatInputAnchor(
// anchorKey: _textFieldKey,
// layerLink: _layerLink,
// ),
// handler: handler,
// context: context,
// style: Theme.of(context).brightness == Brightness.dark
// ? const ChatActionsMenuStyle.dark()
// : const ChatActionsMenuStyle.light(),
// ).show();
// }
// },
), ),
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
),
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
maxLines: 10,
minLines: 1,
onChanged: (_) {},
), ),
); );
} }
@ -162,16 +201,14 @@ class _ChatInputState extends State<ChatInput> {
visible: _sendButtonVisible, visible: _sendButtonVisible,
child: Padding( child: Padding(
padding: buttonPadding, padding: buttonPadding,
child: AccessoryButton( child: ChatInputAccessoryButton(
onSendPressed: () { onSendPressed: () {
if (!widget.isStreaming) { if (!widget.isStreaming) {
widget.onStopStreaming(); widget.onStopStreaming();
_handleSendPressed(); _handleSendPressed();
} }
}, },
onStopStreaming: () { onStopStreaming: () => widget.onStopStreaming(),
widget.onStopStreaming();
},
isStreaming: widget.isStreaming, isStreaming: widget.isStreaming,
), ),
), ),
@ -184,64 +221,20 @@ class _ChatInputState extends State<ChatInput> {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
_handleSendButtonVisibilityModeChange(); _handleSendButtonVisibilityModeChange();
} }
@override
void dispose() {
_inputFocusNode.dispose();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () => _inputFocusNode.requestFocus(),
child: _inputBuilder(),
);
} }
final isMobile = defaultTargetPlatform == TargetPlatform.android || final isMobile = defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS; defaultTargetPlatform == TargetPlatform.iOS;
class AccessoryButton extends StatelessWidget { class ChatInputAnchor extends ChatAnchor {
const AccessoryButton({ ChatInputAnchor({
required this.onSendPressed, required this.anchorKey,
required this.onStopStreaming, required this.layerLink,
required this.isStreaming,
super.key,
}); });
final void Function() onSendPressed; @override
final void Function() onStopStreaming; final GlobalKey<State<StatefulWidget>> anchorKey;
final bool isStreaming;
@override @override
Widget build(BuildContext context) { final LayerLink layerLink;
if (isStreaming) {
return FlowyIconButton(
width: 36,
icon: FlowySvg(
FlowySvgs.ai_stream_stop_s,
size: const Size.square(28),
color: Theme.of(context).colorScheme.primary,
),
onPressed: onStopStreaming,
radius: BorderRadius.circular(18),
fillColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
);
} else {
return FlowyIconButton(
width: 36,
fillColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
radius: BorderRadius.circular(18),
icon: FlowySvg(
FlowySvgs.send_s,
size: const Size.square(24),
color: Theme.of(context).colorScheme.primary,
),
onPressed: onSendPressed,
);
}
}
} }

View File

@ -5,7 +5,7 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'chat_input.dart'; import 'chat_input/chat_input.dart';
class ChatWelcomePage extends StatelessWidget { class ChatWelcomePage extends StatelessWidget {
ChatWelcomePage({required this.onSelectedQuestion, super.key}); ChatWelcomePage({required this.onSelectedQuestion, super.key});

View File

@ -1,11 +1,14 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:markdown_widget/markdown_widget.dart'; import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart'; import 'selectable_highlight.dart';
@ -30,7 +33,11 @@ class AIMarkdownText extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
switch (type) { switch (type) {
case AIMarkdownType.appflowyEditor: case AIMarkdownType.appflowyEditor:
return _AppFlowyEditorMarkdown(markdown: markdown); return BlocProvider(
create: (context) => DocumentPageStyleBloc(view: ViewPB())
..add(const DocumentPageStyleEvent.initial()),
child: _AppFlowyEditorMarkdown(markdown: markdown),
);
case AIMarkdownType.markdownWidget: case AIMarkdownType.markdownWidget:
return _ThirdPartyMarkdown(markdown: markdown); return _ThirdPartyMarkdown(markdown: markdown);
} }

View File

@ -90,19 +90,22 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
); );
} }
return Stack( return Padding(
children: [ padding: const EdgeInsets.only(bottom: 16),
_buildCover(context, state), child: Stack(
Positioned( children: [
left: 0, _buildCover(context, state),
right: 0, Positioned(
bottom: 0, left: 0,
child: Padding( right: 0,
padding: const EdgeInsets.symmetric(vertical: 24.0), bottom: 0,
child: iconAndTitle, child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: iconAndTitle,
),
), ),
), ],
], ),
); );
}, },
), ),

View File

@ -4,7 +4,6 @@ import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
typedef FeatureFlagMap = Map<FeatureFlag, bool>; typedef FeatureFlagMap = Map<FeatureFlag, bool>;

View File

@ -1,8 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/feature_flags.dart';
@ -24,8 +21,11 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.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 'package:go_router/go_router.dart';
import 'package:toastification/toastification.dart';
import 'prelude.dart'; import 'prelude.dart';
@ -197,31 +197,33 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>( child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) { builder: (context, state) {
_setSystemOverlayStyle(state); _setSystemOverlayStyle(state);
return MaterialApp.router( return ToastificationWrapper(
builder: (context, child) => MediaQuery( child: MaterialApp.router(
// use the 1.0 as the textScaleFactor to avoid the text size builder: (context, child) => MediaQuery(
// affected by the system setting. // use the 1.0 as the textScaleFactor to avoid the text size
data: MediaQuery.of(context).copyWith( // affected by the system setting.
textScaler: TextScaler.linear(state.textScaleFactor), data: MediaQuery.of(context).copyWith(
), textScaler: TextScaler.linear(state.textScaleFactor),
child: overlayManagerBuilder( ),
context, child: overlayManagerBuilder(
!PlatformExtension.isMobile && FeatureFlag.search.isOn context,
? CommandPalette( !PlatformExtension.isMobile && FeatureFlag.search.isOn
notifier: _commandPaletteNotifier, ? CommandPalette(
child: child, notifier: _commandPaletteNotifier,
) child: child,
: child, )
: child,
),
), ),
debugShowCheckedModeBanner: false,
theme: state.lightTheme,
darkTheme: state.darkTheme,
themeMode: state.themeMode,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: state.locale,
routerConfig: routerConfig,
), ),
debugShowCheckedModeBanner: false,
theme: state.lightTheme,
darkTheme: state.darkTheme,
themeMode: state.themeMode,
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: state.locale,
routerConfig: routerConfig,
); );
}, },
), ),

View File

@ -48,7 +48,7 @@ class ViewExtKeys {
} }
extension ViewExtension on ViewPB { extension ViewExtension on ViewPB {
Widget defaultIcon() => FlowySvg( Widget defaultIcon({Size? size}) => FlowySvg(
switch (layout) { switch (layout) {
ViewLayoutPB.Board => FlowySvgs.icon_board_s, ViewLayoutPB.Board => FlowySvgs.icon_board_s,
ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s, ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s,
@ -57,6 +57,7 @@ extension ViewExtension on ViewPB {
ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s, ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s,
_ => FlowySvgs.document_s, _ => FlowySvgs.document_s,
}, },
size: size,
); );
PluginType get pluginType => switch (layout) { PluginType get pluginType => switch (layout) {

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.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/startup/tasks/app_widget.dart'; import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
@ -11,6 +11,7 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:toastification/toastification.dart'; import 'package:toastification/toastification.dart';
export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
@ -303,6 +304,18 @@ void showToastNotification(
String? description, String? description,
ToastificationType type = ToastificationType.success, ToastificationType type = ToastificationType.success,
}) { }) {
if (PlatformExtension.isMobile) {
toastification.showCustom(
alignment: Alignment.bottomCenter,
autoCloseDuration: const Duration(milliseconds: 3000),
builder: (_, __) => _MToast(
message: message,
type: type,
),
);
return;
}
toastification.show( toastification.show(
context: context, context: context,
type: type, type: type,
@ -329,6 +342,50 @@ void showToastNotification(
); );
} }
class _MToast extends StatelessWidget {
const _MToast({
required this.message,
this.type = ToastificationType.success,
});
final String message;
final ToastificationType type;
@override
Widget build(BuildContext context) {
// only support success type
assert(type == ToastificationType.success);
return Container(
alignment: Alignment.bottomCenter,
padding: const EdgeInsets.only(bottom: 100),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: const Color(0xE5171717),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.success_s,
blendMode: null,
),
const HSpace(8.0),
FlowyText.regular(
message,
fontSize: 16.0,
figmaLineHeight: 18.0,
color: Colors.white,
),
],
),
),
);
}
}
Future<void> showConfirmDeletionDialog({ Future<void> showConfirmDeletionDialog({
required BuildContext context, required BuildContext context,
required String name, required String name,

View File

@ -53,8 +53,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "268aae9" ref: aac7729
resolved-ref: "268aae905b18efc8a3a9c88dc75ebd19b314bd43" resolved-ref: aac77292a1a175fd7450eef30167032d3cec7fea
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git source: git
version: "3.1.0" version: "3.1.0"
@ -1670,10 +1670,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: qr name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
realtime_client: realtime_client:
dependency: transitive dependency: transitive
description: description:
@ -2443,5 +2443,5 @@ packages:
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
sdks: sdks:
dart: ">=3.3.0 <4.0.0" dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0" flutter: ">=3.22.0"

View File

@ -194,7 +194,7 @@ dependency_overrides:
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "268aae9" ref: "aac7729"
appflowy_editor_plugins: appflowy_editor_plugins:
git: git:

View File

@ -159,6 +159,7 @@
"chat": { "chat": {
"newChat": "AI Chat", "newChat": "AI Chat",
"inputMessageHint": "Message @:appName AI", "inputMessageHint": "Message @:appName AI",
"inputLocalAIMessageHint": "Message @:appName Local AI",
"unsupportedCloudPrompt": "This feature is only available when using @:appName Cloud", "unsupportedCloudPrompt": "This feature is only available when using @:appName Cloud",
"relatedQuestion": "Related", "relatedQuestion": "Related",
"serverUnavailable": "Service Temporarily Unavailable. Please try again later.", "serverUnavailable": "Service Temporarily Unavailable. Please try again later.",
@ -276,7 +277,7 @@
"justNow": "just now", "justNow": "just now",
"minutesAgo": "{count} minutes ago", "minutesAgo": "{count} minutes ago",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"favoriteAt": "Favorited at", "favoriteAt": "Favorited",
"emptyRecent": "No Recent Documents", "emptyRecent": "No Recent Documents",
"emptyRecentDescription": "As you view documents, they will appear here for easy retrieval", "emptyRecentDescription": "As you view documents, they will appear here for easy retrieval",
"emptyFavorite": "No Favorite Documents", "emptyFavorite": "No Favorite Documents",
@ -337,6 +338,8 @@
"removeFromFavorites": "Remove from favorites", "removeFromFavorites": "Remove from favorites",
"removeFromRecent": "Remove from recent", "removeFromRecent": "Remove from recent",
"addToFavorites": "Add to favorites", "addToFavorites": "Add to favorites",
"favoriteSuccessfully": "Favorited success",
"unfavoriteSuccessfully": "Unfavorited success",
"rename": "Rename", "rename": "Rename",
"helpCenter": "Help Center", "helpCenter": "Help Center",
"add": "Add", "add": "Add",