Compare commits

...
Author SHA1 Message Date
Lucas.Xu 81532d014e chore: use light haptic feedback (#5838)
* chore: use light haptic feedback

* chore: optimize tabbar & side action animation
2024-07-30 19:53:11 +08:00
Nathan.fooo 735a09b333 chore: remove local ai (#5837)
* chore: remove local ai

* chore: flutter analyze
2024-07-30 19:44:08 +08:00
nathan d52a04a9e3 chore: update chat input hint 2024-07-30 17:40:58 +08:00
Lucas.Xu a14ced458c chore: bump version 0.6.6 (#5833) 2024-07-30 17:37:28 +08:00
Nathan.fooo 5250a151c8 feat: Chat app download (#5835)
* chore: downlaod chat app

* chore: download app

* chore: disable local ai in billing

* chore: remove watcher

* chore: flutter analyzer
2024-07-30 17:32:30 +08:00
Mathias Mogensen d5a5a64fcf fix: photo gallery improvements + launch review fixes (#5830)
* fix: photo gallery improvements

* fix: improve when to show billing/plan pages

* feat: add grid photo gallery layout

* fix: close inline actions menu
2024-07-30 15:28:40 +08:00
Lucas.Xu 175c90e379 chore: update collab 93ec9a38722d5ed0628e6d03a161e2de172967f3 (#5832) 2024-07-30 15:28:21 +08:00
Lucas.Xu 115ae27ab1 fix: missing system font display (#5825)
* fix: missing system font display

* fix: integration test

* fix: cloud integration test
2024-07-29 14:30:11 +08:00
Chen ☁️☁️ e908892b4a chore: update simplified Chinese translate (#5798) 2024-07-29 10:00:05 +08:00
hasanbeder 948d61a9e0 chore: update tr-TR.json (#5822)
chore: Update Turkish translations
2024-07-29 09:59:25 +08:00
Lucas.Xu b5c4786896 fix: 0.6.6 launch review issues (#5824)
* fix: only rebuild the sidebar header when needed

* fix: skip the null or empty awareness data

* fix: ai writer setState error

* chore: add arrow down icon when hovering on workspace button

* chore: update workspace button tooltip sytle

* chore: only show collapse all subpages button when then page is expanded

* chore: update pin color

* chore: fix integration test

* Revert "chore: fix integration test"

This reverts commit e02b237423.
2024-07-29 09:57:58 +08:00
Nathan.fooo d24f1c566a fix: chat bugs (#5823)
* chore: implement slash menu

* chore: show popup

* chore: single column

* chore: update appflowy editor

* chore: fix warns
2024-07-27 23:47:08 +08:00
Lucas.Xu ddf68b010d chore: improve UI design on mobile (#5816) 2024-07-27 21:05:51 +08:00
107 changed files with 3695 additions and 1325 deletions

No files matched your search

+8
View File
@@ -1,4 +1,12 @@
# Release Notes
## Version 0.6.6 - 30/07/2024
### New Features
- Upgrade your workspace to a premium plan to unlock more features and storage.
- Image galleries and drag-and-drop image support in documents.
### Bug Fixes
- Fix minor UI issues on Desktop and Mobile.
## Version 0.6.5 - 24/07/2024
### New Features
- Publish a Database to the Web
@@ -8,9 +8,10 @@ import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_layouts.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
@@ -1,9 +1,6 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
@@ -17,6 +14,8 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/uplo
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -172,7 +171,17 @@ class EditorOperations {
///
/// Must call [showSlashMenu] first.
Future<void> tapSlashMenuItemWithName(String name) async {
final slashMenu = find
.ancestor(
of: find.byType(SelectionMenuItemWidget),
matching: find.byWidgetPredicate(
(widget) => widget is Scrollable,
),
)
.first;
final slashMenuItem = find.text(name, findRichText: true);
await tester.scrollUntilVisible(slashMenuItem, 200, scrollable: slashMenu);
// await tester.ensureVisible(slashMenuItem);
await tester.tapButton(slashMenuItem);
}
@@ -12,17 +12,23 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
import 'base.dart';
import 'common_operations.dart';
extension AppFlowySettings on WidgetTester {
/// Open settings page
Future<void> openSettings() async {
final settingsDialog = find.byType(SettingsDialog);
// tap empty area to close the settings page
while (settingsDialog.evaluate().isNotEmpty) {
await tapAt(Offset.zero);
await pumpAndSettle();
}
final settingsButton = find.byType(UserSettingButton);
expect(settingsButton, findsOneWidget);
await tapButton(settingsButton);
final settingsDialog = find.byType(SettingsDialog);
expect(settingsDialog, findsOneWidget);
return;
}
+1 -1
View File
@@ -175,7 +175,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@@ -23,6 +23,9 @@ class DocumentPageStyleBloc
await event.when(
initial: () async {
try {
if (view.id.isEmpty) {
return;
}
final layoutObject =
await ViewBackendService.getView(view.id).fold(
(s) => jsonDecode(s.extra),
@@ -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.light.call();
widget.onTapUp();
},
onTapDown: (details) {
setState(() => scale = widget.scaleFactor);
},
child: AnimatedScale(
scale: scale,
alignment: widget.alignment,
duration: widget.duration,
child: widget.child,
),
);
}
}
@@ -24,9 +24,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_document_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Document),
),
FlowyOptionTile.text(
@@ -34,9 +35,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_grid_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Grid),
),
FlowyOptionTile.text(
@@ -44,9 +46,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_board_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Board),
),
FlowyOptionTile.text(
@@ -54,9 +57,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_calendar_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Calendar),
),
FlowyOptionTile.text(
@@ -64,9 +68,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.chat_ai_page_s,
size: Size.square(18),
size: Size.square(20),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Chat),
),
],
@@ -1,4 +1,3 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.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/recent/recent_views_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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -109,16 +109,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
await _showConfirmDialog(
onDelete: () {
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) {
onDelete();
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,
),
],
),
);
}
}
@@ -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/sidebar/folder/folder_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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -40,18 +41,32 @@ enum MobilePaneActionType {
backgroundColor: const Color(0xFFFA217F),
svg: FlowySvgs.favorite_section_remove_from_favorite_s,
size: 24.0,
onPressed: (context) => context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
);
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
},
);
case MobilePaneActionType.addToFavorites:
return MobileSlideActionButton(
backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.favorite_s,
size: 24.0,
onPressed: (context) => context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
onPressed: (context) {
showToastNotification(
context,
message: LocaleKeys.button_favoriteSuccessfully.tr(),
);
context
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
},
);
case MobilePaneActionType.add:
return MobileSlideActionButton(
@@ -69,6 +84,7 @@ enum MobilePaneActionType {
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
showDivider: false,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (sheetContext) {
return AddNewPageWidgetBottomSheet(
@@ -145,8 +161,6 @@ enum MobilePaneActionType {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
if (view.layout != ViewLayoutPB.Chat)
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent,
];
@@ -156,7 +170,6 @@ enum MobilePaneActionType {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
];
}
}
@@ -181,12 +194,13 @@ ActionPane buildEndActionPane(
bool needSpace = true,
MobilePageCardType? cardType,
FolderSpaceType? spaceType,
required double spaceRatio,
}) {
return ActionPane(
motion: const ScrollMotion(),
extentRatio: actions.length / 5,
extentRatio: actions.length / spaceRatio,
children: [
if (needSpace) const HSpace(20),
if (needSpace) const HSpace(60),
...actions.map(
(action) => action.actionButton(
context,
@@ -70,6 +70,7 @@ Future<T?> showMobileBottomSheet<T>(
backgroundColor ??= Theme.of(context).brightness == Brightness.light
? const Color(0xFFF7F8FB)
: const Color(0xFF23262B);
barrierColor ??= Colors.black.withOpacity(0.3);
return showModalBottomSheet<T>(
context: context,
@@ -226,10 +227,14 @@ class BottomSheetHeader extends StatelessWidget {
),
),
Align(
child: FlowyText(
title,
fontSize: 16.0,
fontWeight: FontWeight.w500,
child: Container(
constraints: const BoxConstraints(maxWidth: 250),
child: FlowyText(
title,
fontSize: 17.0,
fontWeight: FontWeight.w500,
overflow: TextOverflow.ellipsis,
),
),
),
if (showDoneButton)
@@ -100,8 +100,6 @@ class _FavoriteViews extends StatelessWidget {
child: ListView.separated(
key: const PageStorageKey('favorite_views_page_storage_key'),
padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
@@ -72,6 +72,7 @@ class MobileFavoriteFolder extends StatelessWidget {
MobilePaneActionType.more,
],
spaceType: FolderSpaceType.favorite,
spaceRatio: 5,
),
),
),
@@ -29,8 +29,6 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
top: HomeSpaceViewSizes.mVerticalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
@@ -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/space/space_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:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -91,7 +92,12 @@ class MobileFolders extends StatelessWidget {
children: [
..._buildSpaceOrSection(context, state),
const VSpace(4.0),
const _TrashButton(),
const Padding(
padding: EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
),
child: _TrashButton(),
),
],
),
);
@@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.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/home/mobile_home_setting_page.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
@@ -113,8 +114,9 @@ class _MobileWorkspace extends StatelessWidget {
if (currentWorkspace == null) {
return const SizedBox.shrink();
}
return GestureDetector(
onTap: () {
return AnimatedGestureDetector(
alignment: Alignment.centerLeft,
onTapUp: () {
context.read<UserWorkspaceBloc>().add(
const UserWorkspaceEvent.fetchWorkspaces(),
);
@@ -143,7 +145,7 @@ class _MobileWorkspace extends StatelessWidget {
: const HSpace(8),
FlowyText.semibold(
currentWorkspace.name,
fontSize: 16.0,
fontSize: 20.0,
overflow: TextOverflow.ellipsis,
),
],
@@ -162,9 +164,10 @@ class _MobileWorkspace extends StatelessWidget {
showHeader: true,
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
title: LocaleKeys.workspace_menuTitle.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
builder: (sheetContext) {
return BlocProvider.value(
value: context.read<UserWorkspaceBloc>(),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
@@ -179,7 +182,7 @@ class _MobileWorkspace extends StatelessWidget {
currentWorkspace: currentWorkspace,
workspaces: workspaces,
onWorkspaceSelected: (workspace) {
context.pop();
Navigator.of(sheetContext).pop();
if (workspace == currentWorkspace) {
return;
@@ -72,8 +72,6 @@ class _RecentViews extends StatelessWidget {
child: ListView.separated(
key: const PageStorageKey('recent_views_page_storage_key'),
padding: EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
right: HomeSpaceViewSizes.mHorizontalPadding,
bottom: HomeSpaceViewSizes.mVerticalPadding +
MediaQuery.of(context).padding.bottom,
),
@@ -82,6 +82,7 @@ class MobileSectionFolder extends StatelessWidget {
],
spaceType: spaceType,
needSpace: false,
spaceRatio: 5,
);
},
),
@@ -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/page_style/document_page_style_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/plugins/document/presentation/editor_plugins/plugins.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/time_format_ext.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-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -76,13 +78,14 @@ class MobileViewPage extends StatelessWidget {
: MobilePaneActionType.addToFavorites,
],
cardType: type,
spaceRatio: 4,
),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapUp: (_) => context.pushView(view),
child: AnimatedGestureDetector(
onTapUp: () => context.pushView(view),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
Expanded(child: _buildDescription(context, state)),
const HSpace(20.0),
SizedBox(
@@ -90,6 +93,7 @@ class MobileViewPage extends StatelessWidget {
height: 60,
child: _buildCover(context, state),
),
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
],
),
),
@@ -211,7 +215,7 @@ class MobileViewPage extends StatelessWidget {
Widget _buildLastViewed(BuildContext context) {
final textColor = Theme.of(context).isLightMode
? const Color(0xFF171717)
? const Color(0x7F171717)
: Colors.white.withOpacity(0.45);
if (timestamp == null) {
return const SizedBox.shrink();
@@ -50,23 +50,17 @@ class _MobileSpaceState extends State<MobileSpace> {
MobileSpaceHeader(
isExpanded: state.isExpanded,
space: currentSpace,
onAdded: () {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout: ViewLayoutPB.Document,
index: 0,
),
);
context.read<SpaceBloc>().add(
SpaceEvent.expand(currentSpace, true),
);
},
onAdded: () => _showCreatePageMenu(currentSpace),
onPressed: () => _showSpaceMenu(context),
),
_Pages(
key: ValueKey(currentSpace.id),
space: currentSpace,
Padding(
padding: const EdgeInsets.only(
left: HomeSpaceViewSizes.mHorizontalPadding,
),
child: _Pages(
key: ValueKey(currentSpace.id),
space: currentSpace,
),
),
],
);
@@ -82,6 +76,7 @@ class _MobileSpaceState extends State<MobileSpace> {
showDragHandle: true,
showCloseButton: true,
showDoneButton: true,
useRootNavigator: true,
title: LocaleKeys.space_title.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
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 {
@@ -140,15 +167,16 @@ class _Pages extends StatelessWidget {
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
final actions = [
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
];
return buildEndActionPane(
context,
[
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
],
actions,
spaceType: spaceType,
needSpace: false,
spaceRatio: actions.length == 1 ? 3 : 4,
);
},
),
@@ -1,4 +1,5 @@
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_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -30,6 +31,7 @@ class MobileSpaceHeader extends StatelessWidget {
height: 48,
child: Row(
children: [
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
SpaceIcon(
dimension: 24,
space: space,
@@ -49,8 +51,15 @@ class MobileSpaceHeader extends StatelessWidget {
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onAdded,
child: const FlowySvg(
FlowySvgs.m_space_add_s,
child: Container(
// expand the touch area
margin: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
vertical: 8.0,
),
child: const FlowySvg(
FlowySvgs.m_space_add_s,
),
),
),
],
@@ -59,6 +59,7 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
children: [
FlowyText.medium(
space.name,
fontSize: 16.0,
),
const HSpace(6.0),
if (space.spacePermission == SpacePermission.private)
@@ -68,16 +69,17 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
),
],
),
margin: const EdgeInsets.symmetric(horizontal: 12.0),
iconPadding: 10,
leftIcon: SpaceIcon(
dimension: 24,
space: space,
cornerRadius: 6.0,
),
leftIconSize: const Size.square(20),
leftIconSize: const Size.square(24),
rightIcon: isSelected
? const FlowySvg(
FlowySvgs.workspace_selected_s,
FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
@@ -23,10 +23,12 @@ class MobileSpaceTabBar extends StatelessWidget {
final labelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 16.0,
height: 20 / 16,
);
final unselectedLabelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 15.0,
height: 20 / 15,
);
return Container(
@@ -138,17 +138,20 @@ class _WorkspaceMenuItem extends StatelessWidget {
height: 60,
showTopBorder: showTopBorder,
showBottomBorder: false,
leftIcon: WorkspaceIcon(
enableEdit: false,
iconSize: 26,
fontSize: 16.0,
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
leftIcon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: WorkspaceIcon(
enableEdit: false,
iconSize: 26,
fontSize: 16.0,
workspace: workspace,
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
UserWorkspaceEvent.updateWorkspaceIcon(
workspace.workspaceId,
result.emoji,
),
),
),
),
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
@@ -50,6 +50,9 @@ class MobileBottomNavigationBar extends StatelessWidget {
final backgroundColor = isLightMode
? Colors.white.withOpacity(0.95)
: const Color(0xFF23262B).withOpacity(0.95);
final borderColor = isLightMode
? const Color(0x141F2329)
: const Color(0xFF23262B).withOpacity(0.5);
return Scaffold(
body: navigationShell,
extendBody: true,
@@ -62,9 +65,7 @@ class MobileBottomNavigationBar extends StatelessWidget {
child: DecoratedBox(
decoration: BoxDecoration(
border: isLightMode
? Border(
top: BorderSide(color: Theme.of(context).dividerColor),
)
? Border(top: BorderSide(color: borderColor))
: null,
color: backgroundColor,
),
@@ -300,7 +300,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
)
: Opacity(
opacity: 0.7,
child: widget.view.defaultIcon(),
child: widget.view.defaultIcon(size: const Size.square(18)),
);
return SizedBox(width: 18.0, child: icon);
}
@@ -13,9 +13,9 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
: listener = LocalLLMListener(),
super(const ChatInputState(aiType: _AppFlowyAI())) {
listener.start(
stateCallback: (pluginState) {
chatStateCallback: (aiState) {
if (!isClosed) {
add(ChatInputEvent.updateState(pluginState));
add(ChatInputEvent.updateState(aiState));
}
},
);
@@ -37,18 +37,26 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
) async {
await event.when(
started: () async {
final result = await ChatEventGetLocalAIPluginState().send();
final result = await ChatEventGetLocalAIChatState().send();
result.fold(
(pluginState) {
(aiState) {
if (!isClosed) {
add(ChatInputEvent.updateState(pluginState));
add(
ChatInputEvent.updateState(aiState),
);
}
},
(err) => Log.error(err.toString()),
(err) {
Log.error(err.toString());
},
);
},
updateState: (LocalAIPluginStatePB aiPluginState) {
emit(const ChatInputState(aiType: _AppFlowyAI()));
updateState: (aiState) {
if (aiState.pluginState.state == RunningStatePB.Running) {
emit(const ChatInputState(aiType: _LocalAI()));
} else {
emit(const ChatInputState(aiType: _AppFlowyAI()));
}
},
);
}
@@ -57,8 +65,8 @@ class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
@freezed
class ChatInputEvent with _$ChatInputEvent {
const factory ChatInputEvent.started() = _Started;
const factory ChatInputEvent.updateState(LocalAIPluginStatePB aiPluginState) =
_UpdatePluginState;
const factory ChatInputEvent.updateState(LocalAIChatPB aiState) =
_UpdateAIState;
}
@freezed
@@ -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_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_theme.dart';
import 'presentation/chat_user_invalid_message.dart';
@@ -82,7 +82,9 @@ class AIChatPage extends StatelessWidget {
userProfile: userProfile,
)..add(const ChatEvent.initialLoad()),
),
BlocProvider(create: (_) => ChatInputBloc()),
BlocProvider(
create: (_) => ChatInputBloc()..add(const ChatInputEvent.started()),
),
],
child: BlocListener<ChatFileBloc, ChatFileState>(
listenWhen: (previous, current) =>
@@ -391,36 +393,39 @@ class _ChatContentPageState extends State<_ChatContentPage> {
padding: AIChatUILayout.safeAreaInsets(context),
child: BlocBuilder<ChatInputBloc, ChatInputState>(
builder: (context, state) {
return state.aiType.when(
appflowyAI: () => Column(
children: [
BlocSelector<ChatBloc, ChatState, LoadingState>(
selector: (state) => state.streamingStatus,
builder: (context, state) {
return ChatInput(
chatId: widget.view.id,
onSendPressed: (message) =>
onSendPressed(context, message.text),
isStreaming: state != const LoadingState.finish(),
onStopStreaming: () {
context
.read<ChatBloc>()
.add(const ChatEvent.stopStream());
},
);
},
final hintText = state.aiType.when(
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
);
return Column(
children: [
BlocSelector<ChatBloc, ChatState, LoadingState>(
selector: (state) => state.streamingStatus,
builder: (context, state) {
return ChatInput(
chatId: widget.view.id,
onSendPressed: (message) =>
onSendPressed(context, message.text),
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(),
),
],
);
},
),
@@ -4,7 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.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/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/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
@@ -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;
}
@@ -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,
);
}
}
}
@@ -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();
}
}
}
@@ -1,14 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_inline_action_menu.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/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'chat_accessory_button.dart';
class ChatInput extends StatefulWidget {
/// Creates [ChatInput] widget.
const ChatInput({
@@ -20,6 +19,7 @@ class ChatInput extends StatefulWidget {
this.options = const InputOptions(),
required this.isStreaming,
required this.onStopStreaming,
required this.hintText,
});
final bool? isAttachmentUploading;
@@ -29,6 +29,7 @@ class ChatInput extends StatefulWidget {
final InputOptions options;
final String chatId;
final bool isStreaming;
final String hintText;
@override
State<ChatInput> createState() => _ChatInputState();
@@ -36,6 +37,11 @@ class ChatInput extends StatefulWidget {
/// [ChatInput] widget state.
class _ChatInputState extends State<ChatInput> {
final GlobalKey _textFieldKey = GlobalKey();
final LayerLink _layerLink = LayerLink();
// final ChatTextFieldInterceptor _textFieldInterceptor =
// ChatTextFieldInterceptor();
late final _inputFocusNode = FocusNode(
onKeyEvent: (node, event) {
if (event.physicalKey == PhysicalKeyboardKey.enter &&
@@ -59,9 +65,9 @@ class _ChatInputState extends State<ChatInput> {
}
},
);
late TextEditingController _textController;
bool _sendButtonVisible = false;
late TextEditingController _textController;
@override
void initState() {
@@ -71,33 +77,15 @@ class _ChatInputState extends State<ChatInput> {
_handleSendButtonVisibilityModeChange();
}
void _handleSendButtonVisibilityModeChange() {
_textController.removeListener(_handleTextControllerChange);
_sendButtonVisible =
_textController.text.trim() != '' || widget.isStreaming;
_textController.addListener(_handleTextControllerChange);
@override
void dispose() {
_inputFocusNode.dispose();
_textController.dispose();
super.dispose();
}
void _handleSendPressed() {
final trimmedText = _textController.text.trim();
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() {
@override
Widget build(BuildContext context) {
const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
const inputPadding = EdgeInsets.all(6);
@@ -128,27 +116,78 @@ class _ChatInputState extends State<ChatInput> {
);
}
Padding _inputTextField(EdgeInsets textPadding) {
return Padding(
padding: textPadding,
child: TextField(
controller: _textController,
focusNode: _inputFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
hintText: LocaleKeys.chat_inputMessageHint.tr(),
hintStyle: TextStyle(
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
void _handleSendButtonVisibilityModeChange() {
_textController.removeListener(_handleTextControllerChange);
_sendButtonVisible =
_textController.text.trim() != '' || widget.isStreaming;
_textController.addListener(_handleTextControllerChange);
}
void _handleSendPressed() {
final trimmedText = _textController.text.trim();
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 _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,
child: Padding(
padding: buttonPadding,
child: AccessoryButton(
child: ChatInputAccessoryButton(
onSendPressed: () {
if (!widget.isStreaming) {
widget.onStopStreaming();
_handleSendPressed();
}
},
onStopStreaming: () {
widget.onStopStreaming();
},
onStopStreaming: () => widget.onStopStreaming(),
isStreaming: widget.isStreaming,
),
),
@@ -184,64 +221,20 @@ class _ChatInputState extends State<ChatInput> {
super.didUpdateWidget(oldWidget);
_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 ||
defaultTargetPlatform == TargetPlatform.iOS;
class AccessoryButton extends StatelessWidget {
const AccessoryButton({
required this.onSendPressed,
required this.onStopStreaming,
required this.isStreaming,
super.key,
class ChatInputAnchor extends ChatAnchor {
ChatInputAnchor({
required this.anchorKey,
required this.layerLink,
});
final void Function() onSendPressed;
final void Function() onStopStreaming;
final bool isStreaming;
@override
final GlobalKey<State<StatefulWidget>> anchorKey;
@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,
);
}
}
final LayerLink layerLink;
}
@@ -5,7 +5,7 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'chat_input.dart';
import 'chat_input/chat_input.dart';
class ChatWelcomePage extends StatelessWidget {
ChatWelcomePage({required this.onSelectedQuestion, super.key});
@@ -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_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/markdown_to_document.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:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
@@ -30,7 +33,11 @@ class AIMarkdownText extends StatelessWidget {
Widget build(BuildContext context) {
switch (type) {
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:
return _ThirdPartyMarkdown(markdown: markdown);
}
@@ -90,19 +90,22 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
);
}
return Stack(
children: [
_buildCover(context, state),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: iconAndTitle,
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Stack(
children: [
_buildCover(context, state),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: iconAndTitle,
),
),
),
],
],
),
);
},
),
@@ -0,0 +1,42 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flowy_infra/size.dart';
@visibleForTesting
class ImageRender extends StatelessWidget {
const ImageRender({
super.key,
required this.image,
this.userProfile,
this.fit = BoxFit.cover,
this.borderRadius = Corners.s6Border,
});
final ImageBlockData image;
final UserProfilePB? userProfile;
final BoxFit fit;
final BorderRadius? borderRadius;
@override
Widget build(BuildContext context) {
final child = switch (image.type) {
CustomImageType.internal || CustomImageType.external => FlowyNetworkImage(
url: image.url,
userProfilePB: userProfile,
fit: fit,
),
CustomImageType.local => Image.file(File(image.url), fit: fit),
};
return Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(borderRadius: borderRadius),
child: child,
);
}
}
@@ -7,13 +7,13 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage;
import 'package:collection/collection.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -25,25 +25,10 @@ import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:provider/provider.dart';
import '../image_render.dart';
const _thumbnailItemSize = 100.0;
abstract class ImageBlockMultiLayout extends StatefulWidget {
const ImageBlockMultiLayout({
super.key,
required this.node,
required this.editorState,
required this.images,
required this.indexNotifier,
required this.isLocalMode,
});
final Node node;
final EditorState editorState;
final List<ImageBlockData> images;
final ValueNotifier<int> indexNotifier;
final bool isLocalMode;
}
class ImageBrowserLayout extends ImageBlockMultiLayout {
const ImageBrowserLayout({
super.key,
@@ -97,115 +82,118 @@ class _ImageBrowserLayoutState extends State<ImageBrowserLayout> {
(constraints.maxWidth / (_thumbnailItemSize + 4)).floor();
final items = widget.images.take(maxItems).toList();
return Wrap(
children: items.mapIndexed((index, image) {
final isLast = items.last == image;
final amountLeft = widget.images.length - items.length;
if (isLast && amountLeft > 0) {
return Center(
child: Wrap(
children: items.mapIndexed((index, image) {
final isLast = items.last == image;
final amountLeft = widget.images.length - items.length;
if (isLast && amountLeft > 0) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _openInteractiveViewer(
context,
maxItems - 1,
),
child: Container(
width: _thumbnailItemSize,
height: _thumbnailItemSize,
padding: const EdgeInsets.all(2),
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: Corners.s8Border,
border: Border.all(
width: 2,
color: Theme.of(context).dividerColor,
),
),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: Corners.s6Border,
image: image.type == CustomImageType.local
? DecorationImage(
image: FileImage(File(image.url)),
fit: BoxFit.cover,
opacity: 0.5,
)
: null,
),
child: Stack(
children: [
if (image.type != CustomImageType.local)
Positioned.fill(
child: Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(
borderRadius: Corners.s6Border,
),
child: FlowyNetworkImage(
url: image.url,
userProfilePB: _userProfile,
),
),
),
DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
),
child: Center(
child: FlowyText(
'+$amountLeft',
color: AFThemeExtension.of(context)
.strongText,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
);
}
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => _openInteractiveViewer(
context,
maxItems - 1,
),
child: Container(
width: _thumbnailItemSize,
height: _thumbnailItemSize,
padding: const EdgeInsets.all(2),
margin: const EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: Corners.s8Border,
border: Border.all(
width: 2,
color: Theme.of(context).dividerColor,
),
),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: Corners.s6Border,
image: image.type == CustomImageType.local
? DecorationImage(
image: FileImage(File(image.url)),
fit: BoxFit.cover,
opacity: 0.5,
)
: null,
),
child: Stack(
children: [
if (image.type != CustomImageType.local)
Positioned.fill(
child: Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(
borderRadius: Corners.s6Border,
),
child: FlowyNetworkImage(
url: image.url,
userProfilePB: _userProfile,
),
),
),
DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
),
child: Center(
child: FlowyText(
'+$amountLeft',
color: AFThemeExtension.of(context)
.strongText,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
onTap: () => widget.onIndexChanged(index),
child: ThumbnailItem(
images: widget.images,
index: index,
selectedIndex: widget.indexNotifier.value,
userProfile: _userProfile,
onDeleted: () async {
final transaction =
widget.editorState.transaction;
final images = widget.images.toList();
images.removeAt(index);
transaction.updateNode(
widget.node,
{
MultiImageBlockKeys.images:
images.map((e) => e.toJson()).toList(),
MultiImageBlockKeys.layout: widget.node
.attributes[MultiImageBlockKeys.layout],
},
);
await widget.editorState.apply(transaction);
widget.onIndexChanged(
widget.indexNotifier.value > 0
? widget.indexNotifier.value - 1
: 0,
);
},
),
),
);
}
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => widget.onIndexChanged(index),
child: ThumbnailItem(
images: widget.images,
index: index,
selectedIndex: widget.indexNotifier.value,
userProfile: _userProfile,
onDeleted: () async {
final transaction = widget.editorState.transaction;
final images = widget.images.toList();
images.removeAt(index);
transaction.updateNode(
widget.node,
{
MultiImageBlockKeys.images:
images.map((e) => e.toJson()).toList(),
MultiImageBlockKeys.layout: widget.node
.attributes[MultiImageBlockKeys.layout],
},
);
await widget.editorState.apply(transaction);
widget.onIndexChanged(
widget.indexNotifier.value > 0
? widget.indexNotifier.value - 1
: 0,
);
},
),
),
);
}).toList(),
}).toList(),
),
);
},
),
@@ -324,7 +312,8 @@ class _ImageBrowserLayoutState extends State<ImageBrowserLayout> {
transaction.updateNode(widget.node, {
MultiImageBlockKeys.images: imagesJson,
MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(),
MultiImageBlockKeys.layout:
widget.node.attributes[MultiImageBlockKeys.layout],
});
await widget.editorState.apply(transaction);
@@ -417,35 +406,3 @@ class _ThumbnailItemState extends State<ThumbnailItem> {
);
}
}
@visibleForTesting
class ImageRender extends StatelessWidget {
const ImageRender({
super.key,
required this.image,
this.userProfile,
this.fit = BoxFit.cover,
});
final ImageBlockData image;
final UserProfilePB? userProfile;
final BoxFit fit;
@override
Widget build(BuildContext context) {
final child = switch (image.type) {
CustomImageType.internal || CustomImageType.external => FlowyNetworkImage(
url: image.url,
userProfilePB: userProfile,
fit: fit,
),
CustomImageType.local => Image.file(File(image.url), fit: fit),
};
return Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(borderRadius: Corners.s6Border),
child: child,
);
}
}
@@ -0,0 +1,323 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart';
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:collection/collection.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:provider/provider.dart';
class ImageGridLayout extends ImageBlockMultiLayout {
const ImageGridLayout({
super.key,
required super.node,
required super.editorState,
required super.images,
required super.indexNotifier,
required super.isLocalMode,
});
@override
State<ImageGridLayout> createState() => _ImageGridLayoutState();
}
class _ImageGridLayoutState extends State<ImageGridLayout> {
@override
Widget build(BuildContext context) {
return StaggeredGridBuilder(
images: widget.images,
onImageDoubleTapped: (index) {
_openInteractiveViewer(context, index);
},
);
}
void _openInteractiveViewer(BuildContext context, int index) => showDialog(
context: context,
builder: (_) => InteractiveImageViewer(
userProfile: context.read<DocumentBloc>().state.userProfilePB,
imageProvider: AFBlockImageProvider(
images: widget.images,
initialIndex: index,
onDeleteImage: (index) async {
final transaction = widget.editorState.transaction;
final newImages = widget.images.toList();
newImages.removeAt(index);
if (newImages.isNotEmpty) {
transaction.updateNode(
widget.node,
{
MultiImageBlockKeys.images:
newImages.map((e) => e.toJson()).toList(),
MultiImageBlockKeys.layout:
widget.node.attributes[MultiImageBlockKeys.layout],
},
);
} else {
transaction.deleteNode(widget.node);
}
await widget.editorState.apply(transaction);
},
),
),
);
}
/// Draws a staggered grid of images, where the pattern is based
/// on the amount of images to fill the grid at all times.
///
/// They will be alternating depending on the current index of the images, such that
/// the layout is reversed in odd segments.
///
/// If there are 4 images in the last segment, this layout will be used:
/// ┌─────┐┌─┐┌─┐
/// │ │└─┘└─┘
/// │ │┌────┐
/// └─────┘└────┘
///
/// If there are 3 images in the last segment, this layout will be used:
/// ┌─────┐┌────┐
/// │ │└────┘
/// │ │┌────┐
/// └─────┘└────┘
///
/// If there are 2 images in the last segment, this layout will be used:
/// ┌─────┐┌─────┐
/// │ ││ │
/// └─────┘└─────┘
///
/// If there is 1 image in the last segment, this layout will be used:
/// ┌──────────┐
/// │ │
/// └──────────┘
class StaggeredGridBuilder extends StatefulWidget {
const StaggeredGridBuilder({
super.key,
required this.images,
required this.onImageDoubleTapped,
});
final List<ImageBlockData> images;
final void Function(int) onImageDoubleTapped;
@override
State<StaggeredGridBuilder> createState() => _StaggeredGridBuilderState();
}
class _StaggeredGridBuilderState extends State<StaggeredGridBuilder> {
late final UserProfilePB? _userProfile;
final List<List<ImageBlockData>> _splitImages = [];
@override
void initState() {
super.initState();
_userProfile = context.read<DocumentBloc>().state.userProfilePB;
for (int i = 0; i < widget.images.length; i += 4) {
final end = (i + 4 < widget.images.length) ? i + 4 : widget.images.length;
_splitImages.add(widget.images.sublist(i, end));
}
}
@override
void didUpdateWidget(covariant StaggeredGridBuilder oldWidget) {
if (widget.images.length != oldWidget.images.length) {
_splitImages.clear();
for (int i = 0; i < widget.images.length; i += 4) {
final end =
(i + 4 < widget.images.length) ? i + 4 : widget.images.length;
_splitImages.add(widget.images.sublist(i, end));
}
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return StaggeredGrid.count(
crossAxisCount: 4,
mainAxisSpacing: 6,
crossAxisSpacing: 6,
children:
_splitImages.indexed.map(_buildTilesForImages).flattened.toList(),
);
}
List<Widget> _buildTilesForImages((int, List<ImageBlockData>) data) {
final index = data.$1;
final images = data.$2;
final isReversed = index.isOdd;
if (images.length == 4) {
return [
StaggeredGridTile.count(
crossAxisCellCount: isReversed ? 1 : 2,
mainAxisCellCount: isReversed ? 1 : 2,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[0],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
StaggeredGridTile.count(
crossAxisCellCount: 1,
mainAxisCellCount: 1,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4 + 1;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[1],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
StaggeredGridTile.count(
crossAxisCellCount: isReversed ? 2 : 1,
mainAxisCellCount: isReversed ? 2 : 1,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4 + 2;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[2],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
StaggeredGridTile.count(
crossAxisCellCount: 2,
mainAxisCellCount: 1,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4 + 3;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[3],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
];
} else if (images.length == 3) {
return [
StaggeredGridTile.count(
crossAxisCellCount: 2,
mainAxisCellCount: isReversed ? 1 : 2,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[0],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
StaggeredGridTile.count(
crossAxisCellCount: 2,
mainAxisCellCount: isReversed ? 2 : 1,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4 + 1;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[1],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
StaggeredGridTile.count(
crossAxisCellCount: 2,
mainAxisCellCount: 1,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4 + 2;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[2],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
];
} else if (images.length == 2) {
return [
StaggeredGridTile.count(
crossAxisCellCount: 2,
mainAxisCellCount: 2,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[0],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
StaggeredGridTile.count(
crossAxisCellCount: 2,
mainAxisCellCount: 2,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4 + 1;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[1],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
];
} else {
return [
StaggeredGridTile.count(
crossAxisCellCount: 4,
mainAxisCellCount: 2,
child: GestureDetector(
onDoubleTap: () {
final imageIndex = index * 4;
widget.onImageDoubleTapped(imageIndex);
},
child: ImageRender(
image: images[0],
userProfile: _userProfile,
borderRadius: BorderRadius.zero,
),
),
),
];
}
}
}
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_grid_layout.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage;
abstract class ImageBlockMultiLayout extends StatefulWidget {
const ImageBlockMultiLayout({
super.key,
required this.node,
required this.editorState,
required this.images,
required this.indexNotifier,
required this.isLocalMode,
});
final Node node;
final EditorState editorState;
final List<ImageBlockData> images;
final ValueNotifier<int> indexNotifier;
final bool isLocalMode;
}
class ImageLayoutRender extends StatelessWidget {
const ImageLayoutRender({
super.key,
required this.node,
required this.editorState,
required this.images,
required this.indexNotifier,
required this.isLocalMode,
required this.onIndexChanged,
});
final Node node;
final EditorState editorState;
final List<ImageBlockData> images;
final ValueNotifier<int> indexNotifier;
final bool isLocalMode;
final void Function(int) onIndexChanged;
@override
Widget build(BuildContext context) {
final layout = _getLayout();
return _buildLayout(layout);
}
MultiImageLayout _getLayout() {
return MultiImageLayout.fromIntValue(
node.attributes[MultiImageBlockKeys.layout] ?? 0,
);
}
Widget _buildLayout(MultiImageLayout layout) {
switch (layout) {
case MultiImageLayout.grid:
return ImageGridLayout(
node: node,
editorState: editorState,
images: images,
indexNotifier: indexNotifier,
isLocalMode: isLocalMode,
);
case MultiImageLayout.browser:
default:
return ImageBrowserLayout(
node: node,
editorState: editorState,
images: images,
indexNotifier: indexNotifier,
isLocalMode: isLocalMode,
onIndexChanged: onIndexChanged,
);
}
}
}
@@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_layouts.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:provider/provider.dart';
const kMultiImagePlaceholderKey = 'multiImagePlaceholderKey';
@@ -109,6 +112,38 @@ class MultiImageBlockComponentState extends State<MultiImageBlockComponent>
bool alwaysShowMenu = false;
static const _interceptorKey = 'multi-image-block-interceptor';
late final interceptor = SelectionGestureInterceptor(
key: _interceptorKey,
canTap: (details) => _isTapInBounds(details.globalPosition),
canPanStart: (details) => _isTapInBounds(details.globalPosition),
);
@override
void initState() {
super.initState();
editorState.selectionService.registerGestureInterceptor(interceptor);
}
@override
void dispose() {
editorState.selectionService.unregisterGestureInterceptor(_interceptorKey);
super.dispose();
}
bool _isTapInBounds(Offset offset) {
if (_renderBox == null) {
// We shouldn't block any actions if the render box is not available.
// This has the potential to break taps on the editor completely if we
// accidentally return false here.
return true;
}
final localPosition = _renderBox!.globalToLocal(offset);
return !_renderBox!.paintBounds.contains(localPosition);
}
@override
Widget build(BuildContext context) {
final data = MultiImageData.fromJson(
@@ -127,7 +162,7 @@ class MultiImageBlockComponentState extends State<MultiImageBlockComponent>
node: node,
);
} else {
child = ImageBrowserLayout(
child = ImageLayoutRender(
node: node,
images: data.images,
editorState: editorState,
@@ -302,17 +337,14 @@ class MultiImageData {
enum MultiImageLayout {
browser,
masonry,
grid;
int toIntValue() {
switch (this) {
case MultiImageLayout.browser:
return 0;
case MultiImageLayout.masonry:
return 1;
case MultiImageLayout.grid:
return 2;
return 1;
}
}
@@ -321,11 +353,19 @@ enum MultiImageLayout {
case 0:
return MultiImageLayout.browser;
case 1:
return MultiImageLayout.masonry;
case 2:
return MultiImageLayout.grid;
default:
throw UnimplementedError();
}
}
String get label => switch (this) {
browser => LocaleKeys.document_plugins_photoGallery_browserLayout.tr(),
grid => LocaleKeys.document_plugins_photoGallery_gridLayout.tr(),
};
FlowySvgData get icon => switch (this) {
browser => FlowySvgs.photo_layout_browser_s,
grid => FlowySvgs.photo_layout_grid_s,
};
}
@@ -21,6 +21,8 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu, Log;
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:http/http.dart';
@@ -57,6 +59,7 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
);
final PopoverController controller = PopoverController();
final PopoverController layoutController = PopoverController();
late List<ImageBlockData> images;
late final EditorState editorState;
@@ -73,6 +76,7 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
void dispose() {
allowMenuClose();
controller.close();
layoutController.close();
super.dispose();
}
@@ -88,6 +92,9 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final layout = MultiImageLayout.fromIntValue(
widget.node.attributes[MultiImageBlockKeys.layout] ?? 0,
);
return Container(
height: 32,
decoration: BoxDecoration(
@@ -104,11 +111,6 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
child: Row(
children: [
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(),
iconData: FlowySvgs.full_view_s,
onTap: openFullScreen,
),
AppFlowyPopover(
controller: controller,
direction: PopoverDirection.bottomWithRightAligned,
@@ -141,9 +143,57 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
onTap: () {},
),
),
const HSpace(4),
AppFlowyPopover(
controller: layoutController,
onClose: allowMenuClose,
direction: PopoverDirection.bottomWithRightAligned,
offset: const Offset(0, 10),
constraints: const BoxConstraints(
maxHeight: 300,
maxWidth: 300,
),
popupBuilder: (context) {
preventMenuClose();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_LayoutSelector(
selectedLayout: layout,
onSelected: (layout) {
allowMenuClose();
layoutController.close();
final transaction = editorState.transaction;
transaction.updateNode(widget.node, {
MultiImageBlockKeys.images:
widget.node.attributes[MultiImageBlockKeys.images],
MultiImageBlockKeys.layout: layout.toIntValue(),
});
editorState.apply(transaction);
},
),
],
);
},
child: MenuBlockButton(
tooltip: LocaleKeys
.document_plugins_photoGallery_changeLayoutTooltip
.tr(),
iconData: FlowySvgs.edit_layout_s,
onTap: () {},
),
),
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(),
iconData: FlowySvgs.full_view_s,
onTap: openFullScreen,
),
// disable the copy link button if the image is hosted on appflowy cloud
// because the url needs the verification token to be accessible
if (!images[widget.indexNotifier.value].url.isAppFlowyCloudUrl) ...[
if (layout == MultiImageLayout.browser &&
!images[widget.indexNotifier.value].url.isAppFlowyCloudUrl) ...[
const HSpace(4),
MenuBlockButton(
tooltip: LocaleKeys.editor_copyLink.tr(),
@@ -153,7 +203,8 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
],
const _Divider(),
MenuBlockButton(
tooltip: LocaleKeys.button_delete.tr(),
tooltip: LocaleKeys.document_plugins_photoGallery_deleteBlockTooltip
.tr(),
iconData: FlowySvgs.delete_s,
onTap: deleteImage,
),
@@ -202,8 +253,8 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
newImages.map((image) => image.toJson()).toList();
transaction.updateNode(widget.node, {
MultiImageBlockKeys.images: imagesJson,
// Default to Browser layout
MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(),
MultiImageBlockKeys.layout:
widget.node.attributes[MultiImageBlockKeys.layout],
});
await editorState.apply(transaction);
@@ -242,12 +293,12 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
return;
}
newImages.addAll(images);
final imagesJson = newImages.map((image) => image.toJson()).toList();
final imagesJson =
[...images, ...newImages].map((i) => i.toJson()).toList();
transaction.updateNode(widget.node, {
MultiImageBlockKeys.images: imagesJson,
// Default to Browser layout
MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(),
MultiImageBlockKeys.layout:
widget.node.attributes[MultiImageBlockKeys.layout],
});
await editorState.apply(transaction);
@@ -310,8 +361,8 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
final imagesJson = newImages.map((image) => image.toJson()).toList();
transaction.updateNode(widget.node, {
MultiImageBlockKeys.images: imagesJson,
// Default to Browser layout
MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(),
MultiImageBlockKeys.layout:
widget.node.attributes[MultiImageBlockKeys.layout],
});
await editorState.apply(transaction);
@@ -330,3 +381,58 @@ class _Divider extends StatelessWidget {
);
}
}
class _LayoutSelector extends StatelessWidget {
const _LayoutSelector({
required this.selectedLayout,
required this.onSelected,
});
final MultiImageLayout selectedLayout;
final Function(MultiImageLayout) onSelected;
@override
Widget build(BuildContext context) {
return SeparatedRow(
separatorBuilder: () => const HSpace(6),
mainAxisSize: MainAxisSize.min,
children: MultiImageLayout.values
.map(
(layout) => MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () => onSelected(layout),
child: Container(
height: 80,
width: 80,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(
width: 2,
color: selectedLayout == layout
? Theme.of(context).colorScheme.primary
: Theme.of(context).dividerColor,
),
borderRadius: Corners.s8Border,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlowySvg(
layout.icon,
color: AFThemeExtension.of(context).strongText,
size: const Size.square(24),
),
const VSpace(6),
FlowyText(layout.label),
],
),
),
),
),
)
.toList(),
);
}
}
@@ -222,8 +222,9 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
transaction.updateNode(widget.node, {
MultiImageBlockKeys.images: imagesJson,
// Default to Browser layout
MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(),
MultiImageBlockKeys.layout:
widget.node.attributes[MultiImageBlockKeys.layout] ??
MultiImageLayout.browser.toIntValue(),
});
await editorState.apply(transaction);
@@ -282,8 +283,9 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
transaction.updateNode(widget.node, {
MultiImageBlockKeys.images:
images.map((image) => image.toJson()).toList(),
// Default to Browser layout
MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(),
MultiImageBlockKeys.layout:
widget.node.attributes[MultiImageBlockKeys.layout] ??
MultiImageLayout.browser.toIntValue(),
});
await editorState.apply(transaction);
}
@@ -75,7 +75,7 @@ class FontFamilyItem extends StatelessWidget {
}
});
},
text: (fontFamily ?? systemFonFamily).parseFontFamilyName(),
text: (fontFamily ?? systemFonFamily).fontFamilyDisplayName,
fontFamily: fontFamily ?? systemFonFamily,
backgroundColor: theme.toolbarMenuItemBackgroundColor,
isSelected: false,
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
@@ -13,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'ai_limit_dialog.dart';
@@ -178,6 +177,7 @@ class _AutoCompletionBlockComponentState
await editorState.apply(
transaction,
options: const ApplyOptions(recordUndo: false),
withUpdateSelection: false,
);
}
@@ -49,7 +49,7 @@ extension _StartWithsSort on List<InlineActionsResult> {
);
}
const _invalidSearchesAmount = 20;
const _invalidSearchesAmount = 10;
class InlineActionsHandler extends StatefulWidget {
const InlineActionsHandler({
@@ -81,8 +81,6 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
final _focusNode = FocusNode(debugLabel: 'inline_actions_menu_handler');
final _scrollController = ScrollController();
Timer? _debounce;
late List<InlineActionsResult> results = widget.results;
int invalidCounter = 0;
late int startOffset;
@@ -90,8 +88,7 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
String _search = '';
set search(String search) {
_search = search;
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 200), _doSearch);
_doSearch();
}
Future<void> _doSearch() async {
@@ -109,10 +106,13 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
: 0;
if (invalidCounter >= _invalidSearchesAmount) {
widget.onDismiss();
// Workaround to bring focus back to editor
await widget.editorState
.updateSelectionWithReason(widget.editorState.selection);
return widget.onDismiss();
return;
}
_resetSelection();
@@ -143,7 +143,6 @@ class _InlineActionsHandlerState extends State<InlineActionsHandler> {
void dispose() {
_scrollController.dispose();
_focusNode.dispose();
_debounce?.cancel();
super.dispose();
}
@@ -4,7 +4,6 @@ import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
typedef FeatureFlagMap = Map<FeatureFlag, bool>;
@@ -208,11 +208,6 @@ void _resolveFolderDeps(GetIt getIt) {
),
);
// Settings
getIt.registerFactoryParam<SettingsDialogBloc, UserProfilePB, void>(
(user, _) => SettingsDialogBloc(user),
);
// User
getIt.registerFactoryParam<SettingsUserViewBloc, UserProfilePB, void>(
(user, _) => SettingsUserViewBloc(user),
@@ -1,8 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.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:flowy_infra/theme.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:go_router/go_router.dart';
import 'package:toastification/toastification.dart';
import 'prelude.dart';
@@ -197,31 +197,33 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
child: BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>(
builder: (context, state) {
_setSystemOverlayStyle(state);
return MaterialApp.router(
builder: (context, child) => MediaQuery(
// use the 1.0 as the textScaleFactor to avoid the text size
// affected by the system setting.
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(state.textScaleFactor),
),
child: overlayManagerBuilder(
context,
!PlatformExtension.isMobile && FeatureFlag.search.isOn
? CommandPalette(
notifier: _commandPaletteNotifier,
child: child,
)
: child,
return ToastificationWrapper(
child: MaterialApp.router(
builder: (context, child) => MediaQuery(
// use the 1.0 as the textScaleFactor to avoid the text size
// affected by the system setting.
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(state.textScaleFactor),
),
child: overlayManagerBuilder(
context,
!PlatformExtension.isMobile && FeatureFlag.search.isOn
? CommandPalette(
notifier: _commandPaletteNotifier,
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,
);
},
),
@@ -0,0 +1,41 @@
import 'dart:async';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:url_launcher/url_launcher.dart' show launchUrl;
part 'download_offline_ai_app_bloc.freezed.dart';
class DownloadOfflineAIBloc
extends Bloc<DownloadOfflineAIEvent, DownloadOfflineAIState> {
DownloadOfflineAIBloc() : super(const DownloadOfflineAIState()) {
on<DownloadOfflineAIEvent>(_handleEvent);
}
Future<void> _handleEvent(
DownloadOfflineAIEvent event,
Emitter<DownloadOfflineAIState> emit,
) async {
await event.when(
started: () async {
final result = await ChatEventGetOfflineAIAppLink().send();
await result.fold(
(app) async {
await launchUrl(Uri.parse(app.link));
},
(err) {},
);
},
);
}
}
@freezed
class DownloadOfflineAIEvent with _$DownloadOfflineAIEvent {
const factory DownloadOfflineAIEvent.started() = _Started;
}
@freezed
class DownloadOfflineAIState with _$DownloadOfflineAIState {
const factory DownloadOfflineAIState() = _DownloadOfflineAIState;
}
@@ -20,7 +20,7 @@ class LocalAIChatSettingBloc
listener.start(
stateCallback: (newState) {
if (!isClosed) {
add(LocalAIChatSettingEvent.updateLLMRunningState(newState.state));
add(LocalAIChatSettingEvent.updatePluginState(newState));
}
},
);
@@ -46,14 +46,14 @@ class LocalAIChatSettingBloc
modelInfo: modelInfo,
models: modelInfo.models,
selectedLLMModel: modelInfo.selectedModel,
fetchModelInfoState: const LoadingState.finish(),
aiModelProgress: const AIModelProgress.finish(),
),
);
},
(err) {
emit(
state.copyWith(
fetchModelInfoState: LoadingState.finish(error: err),
aiModelProgress: AIModelProgress.finish(error: err),
),
);
},
@@ -68,7 +68,7 @@ class LocalAIChatSettingBloc
emit(
state.copyWith(
selectedLLMModel: llmModel,
localAIInfo: LocalAIProgress.showDownload(
progressIndicator: LocalAIProgress.showDownload(
llmResource,
llmModel,
),
@@ -80,7 +80,7 @@ class LocalAIChatSettingBloc
state.copyWith(
selectedLLMModel: llmModel,
selectLLMState: const LoadingState.finish(),
localAIInfo: const LocalAIProgress.checkPluginState(),
progressIndicator: const LocalAIProgress.checkPluginState(),
),
);
}
@@ -106,7 +106,7 @@ class LocalAIChatSettingBloc
if (llmResource.pendingResources.isEmpty) {
emit(
state.copyWith(
localAIInfo: const LocalAIProgress.checkPluginState(),
progressIndicator: const LocalAIProgress.checkPluginState(),
),
);
} else {
@@ -115,7 +115,7 @@ class LocalAIChatSettingBloc
if (llmResource.isDownloading) {
emit(
state.copyWith(
localAIInfo:
progressIndicator:
LocalAIProgress.startDownloading(state.selectedLLMModel!),
selectLLMState: const LoadingState.finish(),
),
@@ -124,7 +124,7 @@ class LocalAIChatSettingBloc
} else {
emit(
state.copyWith(
localAIInfo: LocalAIProgress.showDownload(
progressIndicator: LocalAIProgress.showDownload(
llmResource,
state.selectedLLMModel!,
),
@@ -138,7 +138,7 @@ class LocalAIChatSettingBloc
startDownloadModel: (LLMModelPB llmModel) {
emit(
state.copyWith(
localAIInfo: LocalAIProgress.startDownloading(llmModel),
progressIndicator: LocalAIProgress.startDownloading(llmModel),
selectLLMState: const LoadingState.finish(),
),
);
@@ -149,19 +149,39 @@ class LocalAIChatSettingBloc
},
finishDownload: () async {
emit(
state.copyWith(localAIInfo: const LocalAIProgress.finishDownload()),
state.copyWith(
progressIndicator: const LocalAIProgress.finishDownload(),
),
);
},
updateLLMRunningState: (RunningStatePB newRunningState) {
if (newRunningState == RunningStatePB.Stopped) {
updatePluginState: (LocalAIPluginStatePB pluginState) {
if (pluginState.offlineAiReady) {
ChatEventRefreshLocalAIModelInfo().send().then((result) {
if (!isClosed) {
add(LocalAIChatSettingEvent.didLoadModelInfo(result));
}
});
if (pluginState.state == RunningStatePB.Stopped) {
emit(
state.copyWith(
runningState: pluginState.state,
progressIndicator: const LocalAIProgress.checkPluginState(),
),
);
} else {
emit(
state.copyWith(
runningState: pluginState.state,
),
);
}
} else {
emit(
state.copyWith(
runningState: newRunningState,
localAIInfo: const LocalAIProgress.checkPluginState(),
progressIndicator: const LocalAIProgress.startOfflineAIApp(),
),
);
} else {
emit(state.copyWith(runningState: newRunningState));
}
},
);
@@ -183,10 +203,21 @@ class LocalAIChatSettingBloc
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await ChatEventRefreshLocalAIModelInfo().send();
if (!isClosed) {
add(LocalAIChatSettingEvent.didLoadModelInfo(result));
}
final result = await ChatEventGetLocalAIPluginState().send();
result.fold(
(pluginState) async {
if (!isClosed) {
add(LocalAIChatSettingEvent.updatePluginState(pluginState));
if (pluginState.offlineAiReady) {
final result = await ChatEventRefreshLocalAIModelInfo().send();
if (!isClosed) {
add(LocalAIChatSettingEvent.didLoadModelInfo(result));
}
}
}
},
(err) => Log.error(err.toString()),
);
}
@override
@@ -214,9 +245,9 @@ class LocalAIChatSettingEvent with _$LocalAIChatSettingEvent {
const factory LocalAIChatSettingEvent.cancelDownload() = _CancelDownload;
const factory LocalAIChatSettingEvent.finishDownload() = _FinishDownload;
const factory LocalAIChatSettingEvent.updateLLMRunningState(
RunningStatePB newRunningState,
) = _RunningState;
const factory LocalAIChatSettingEvent.updatePluginState(
LocalAIPluginStatePB pluginState,
) = _PluginState;
}
@freezed
@@ -224,29 +255,16 @@ class LocalAIChatSettingState with _$LocalAIChatSettingState {
const factory LocalAIChatSettingState({
LLMModelInfoPB? modelInfo,
LLMModelPB? selectedLLMModel,
LocalAIProgress? localAIInfo,
@Default(LoadingState.loading()) LoadingState fetchModelInfoState,
LocalAIProgress? progressIndicator,
@Default(AIModelProgress.init()) AIModelProgress aiModelProgress,
@Default(LoadingState.loading()) LoadingState selectLLMState,
@Default([]) List<LLMModelPB> models,
@Default(RunningStatePB.Connecting) RunningStatePB runningState,
}) = _LocalAIChatSettingState;
}
// @freezed
// class LocalChatAIStateIndicator with _$LocalChatAIStateIndicator {
// // when start downloading the model
// const factory LocalChatAIStateIndicator.error(FlowyError error) = _OnError;
// const factory LocalChatAIStateIndicator.ready(bool isEnabled) = _Ready;
// }
@freezed
class LocalAIProgress with _$LocalAIProgress {
// when user select a new model, it will call requestDownload
const factory LocalAIProgress.requestDownloadInfo(
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) = _RequestDownload;
// when user comes back to the setting page, it will auto detect current llm state
const factory LocalAIProgress.showDownload(
LocalModelResourcePB llmResource,
@@ -257,5 +275,13 @@ class LocalAIProgress with _$LocalAIProgress {
const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) =
_Downloading;
const factory LocalAIProgress.finishDownload() = _Finish;
const factory LocalAIProgress.checkPluginState() = _PluginState;
const factory LocalAIProgress.checkPluginState() = _CheckPluginState;
const factory LocalAIProgress.startOfflineAIApp() = _StartOfflineAIApp;
}
@freezed
class AIModelProgress with _$AIModelProgress {
const factory AIModelProgress.init() = _AIModelProgressInit;
const factory AIModelProgress.loading() = _AIModelDownloading;
const factory AIModelProgress.finish({FlowyError? error}) = _AIModelFinish;
}
@@ -7,6 +7,7 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:url_launcher/url_launcher.dart' show launchUrl;
part 'plugin_state_bloc.freezed.dart';
class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
@@ -53,19 +54,31 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
);
},
updateState: (LocalAIPluginStatePB pluginState) {
switch (pluginState.state) {
case RunningStatePB.Connecting:
emit(
const PluginStateState(action: PluginStateAction.loadingPlugin()),
);
case RunningStatePB.Running:
emit(const PluginStateState(action: PluginStateAction.ready()));
break;
default:
emit(
state.copyWith(action: const PluginStateAction.restart()),
);
break;
// if the offline ai is not started, ask user to start it
if (pluginState.offlineAiReady) {
// Chech state of the plugin
switch (pluginState.state) {
case RunningStatePB.Connecting:
emit(
const PluginStateState(
action: PluginStateAction.loadingPlugin(),
),
);
case RunningStatePB.Running:
emit(const PluginStateState(action: PluginStateAction.ready()));
break;
default:
emit(
state.copyWith(action: const PluginStateAction.restartPlugin()),
);
break;
}
} else {
emit(
const PluginStateState(
action: PluginStateAction.startAIOfflineApp(),
),
);
}
},
restartLocalAI: () async {
@@ -83,6 +96,15 @@ class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
(err) => Log.error(err.toString()),
);
},
downloadOfflineAIApp: () async {
final result = await ChatEventGetOfflineAIAppLink().send();
await result.fold(
(app) async {
await launchUrl(Uri.parse(app.link));
},
(err) {},
);
},
);
}
}
@@ -95,6 +117,7 @@ class PluginStateEvent with _$PluginStateEvent {
const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI;
const factory PluginStateEvent.openModelDirectory() =
_OpenModelStorageDirectory;
const factory PluginStateEvent.downloadOfflineAIApp() = _DownloadOfflineAIApp;
}
@freezed
@@ -109,5 +132,6 @@ class PluginStateAction with _$PluginStateAction {
const factory PluginStateAction.init() = _Init;
const factory PluginStateAction.loadingPlugin() = _LoadingPlugin;
const factory PluginStateAction.ready() = _Ready;
const factory PluginStateAction.restart() = _Restart;
const factory PluginStateAction.restartPlugin() = _RestartPlugin;
const factory PluginStateAction.startAIOfflineApp() = _StartAIOfflineApp;
}
@@ -43,6 +43,9 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
emit(state.copyWith(userProfile: userProfile));
},
toggleAISearch: () {
emit(
state.copyWith(enableSearchIndexing: !state.enableSearchIndexing),
);
_updateUserWorkspaceSetting(
disableSearchIndexing:
!(state.aiSettings?.disableSearchIndexing ?? false),
@@ -1,7 +1,12 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -27,7 +32,8 @@ enum SettingsPage {
class SettingsDialogBloc
extends Bloc<SettingsDialogEvent, SettingsDialogState> {
SettingsDialogBloc(
this.userProfile, {
this.userProfile,
this.workspaceMember, {
SettingsPage? initPage,
}) : _userListener = UserListener(userProfile: userProfile),
super(SettingsDialogState.initial(userProfile, initPage)) {
@@ -35,6 +41,7 @@ class SettingsDialogBloc
}
final UserProfilePB userProfile;
final WorkspaceMemberPB? workspaceMember;
final UserListener _userListener;
@override
@@ -49,6 +56,12 @@ class SettingsDialogBloc
await event.when(
initial: () async {
_userListener.start(onProfileUpdated: _profileUpdated);
final isBillingEnabled =
await _isBillingEnabled(userProfile, workspaceMember);
if (isBillingEnabled) {
emit(state.copyWith(isBillingEnabled: true));
}
},
didReceiveUserProfile: (UserProfilePB newUserProfile) {
emit(state.copyWith(userProfile: newUserProfile));
@@ -70,6 +83,45 @@ class SettingsDialogBloc
(err) => Log.error(err),
);
}
Future<bool> _isBillingEnabled(
UserProfilePB userProfile, [
WorkspaceMemberPB? member,
]) async {
if ([
AuthenticatorPB.Local,
AuthenticatorPB.Supabase,
].contains(userProfile.authenticator)) {
return false;
}
if (member == null || member.role != AFRolePB.Owner) {
return false;
}
if (kDebugMode) {
return true;
}
final result = await UserEventGetCloudConfig().send();
return result.fold(
(cloudSetting) {
final whiteList = [
"https://beta.appflowy.cloud",
"https://test.appflowy.cloud",
];
if (kDebugMode) {
whiteList.add("http://localhost:8000");
}
return whiteList.contains(cloudSetting.serverUrl);
},
(err) {
Log.error("Failed to get cloud config: $err");
return false;
},
);
}
}
@freezed
@@ -87,6 +139,7 @@ class SettingsDialogState with _$SettingsDialogState {
const factory SettingsDialogState({
required UserProfilePB userProfile,
required SettingsPage page,
required bool isBillingEnabled,
}) = _SettingsDialogState;
factory SettingsDialogState.initial(
@@ -96,5 +149,6 @@ class SettingsDialogState with _$SettingsDialogState {
SettingsDialogState(
userProfile: userProfile,
page: page ?? SettingsPage.account,
isBillingEnabled: false,
);
}
@@ -48,7 +48,7 @@ class ViewExtKeys {
}
extension ViewExtension on ViewPB {
Widget defaultIcon() => FlowySvg(
Widget defaultIcon({Size? size}) => FlowySvg(
switch (layout) {
ViewLayoutPB.Board => FlowySvgs.icon_board_s,
ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s,
@@ -57,6 +57,7 @@ extension ViewExtension on ViewPB {
ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s,
_ => FlowySvgs.document_s,
},
size: size,
);
PluginType get pluginType => switch (layout) {
@@ -150,12 +150,16 @@ class _FavoriteGroups extends StatelessWidget {
children: [
if (today.isNotEmpty) ...[
...today,
const FlowyDivider(),
const VSpace(16),
],
if (thisWeek.isNotEmpty) ...[
if (today.isNotEmpty) ...[
const FlowyDivider(),
const VSpace(16),
],
...thisWeek,
const VSpace(8),
],
if ((thisWeek.isNotEmpty || today.isNotEmpty) &&
others.isNotEmpty) ...[
const FlowyDivider(),
const VSpace(16),
],
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
@@ -14,6 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class FavoriteMoreActions extends StatelessWidget {
@@ -28,6 +27,7 @@ class FavoriteMoreActions extends StatelessWidget {
child: ViewMoreActionButton(
view: view,
spaceType: FolderSpaceType.favorite,
isExpanded: false,
onEditing: (value) =>
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
onAction: (action, _) {
@@ -275,7 +275,6 @@ class _SidebarState extends State<_Sidebar> {
@override
Widget build(BuildContext context) {
const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 8);
final userState = context.read<UserWorkspaceBloc>().state;
return MouseRegion(
onEnter: (_) => _isHovered.value = true,
onExit: (_) => _isHovered.value = false,
@@ -297,15 +296,19 @@ class _SidebarState extends State<_Sidebar> {
),
),
// user or workspace, setting
Container(
height: HomeSizes.workspaceSectionHeight,
padding: menuHorizontalInset - const EdgeInsets.only(right: 6),
child:
// if the workspaces are empty, show the user profile instead
userState.isCollabWorkspaceOn &&
userState.workspaces.isNotEmpty
? SidebarWorkspace(userProfile: widget.userProfile)
: SidebarUser(userProfile: widget.userProfile),
BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
builder: (context, state) {
return Container(
height: HomeSizes.workspaceSectionHeight,
padding:
menuHorizontalInset - const EdgeInsets.only(right: 6),
child:
// if the workspaces are empty, show the user profile instead
state.isCollabWorkspaceOn && state.workspaces.isNotEmpty
? SidebarWorkspace(userProfile: widget.userProfile)
: SidebarUser(userProfile: widget.userProfile),
);
},
),
if (FeatureFlag.search.isOn) ...[
const VSpace(6),
@@ -292,6 +292,7 @@ class ConfirmPopup extends StatefulWidget {
required this.title,
required this.description,
required this.onConfirm,
this.onCancel,
this.confirmLabel,
this.confirmButtonColor,
});
@@ -299,6 +300,7 @@ class ConfirmPopup extends StatefulWidget {
final String title;
final String description;
final VoidCallback onConfirm;
final VoidCallback? onCancel;
final Color? confirmButtonColor;
final ConfirmPopupStyle style;
@@ -392,7 +394,10 @@ class _ConfirmPopupState extends State<ConfirmPopup> {
);
case ConfirmPopupStyle.cancelAndOk:
return SpaceCancelOrConfirmButton(
onCancel: () => Navigator.of(context).pop(),
onCancel: () {
widget.onCancel?.call();
Navigator.of(context).pop();
},
onConfirm: () {
widget.onConfirm();
Navigator.of(context).pop();
@@ -1,3 +1,4 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
@@ -198,9 +199,9 @@ class _SidebarSwitchWorkspaceButtonState
),
);
},
child: FlowyButton(
child: FlowyIconTextButton(
margin: EdgeInsets.zero,
text: SizedBox(
textBuilder: (onHover) => SizedBox(
height: 30,
child: Row(
children: [
@@ -229,6 +230,10 @@ class _SidebarSwitchWorkspaceButtonState
),
),
const HSpace(4),
if (onHover)
const FlowySvg(
FlowySvgs.workspace_drop_down_menu_show_s,
),
],
),
),
@@ -693,6 +693,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(),
child: ViewMoreActionButton(
view: widget.view,
isExpanded: widget.isExpanded,
spaceType: widget.spaceType,
onEditing: (value) =>
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
@@ -19,12 +19,14 @@ class ViewMoreActionButton extends StatelessWidget {
required this.onEditing,
required this.onAction,
required this.spaceType,
required this.isExpanded,
});
final ViewPB view;
final void Function(bool value) onEditing;
final void Function(ViewMoreActionType type, dynamic data) onAction;
final FolderSpaceType spaceType;
final bool isExpanded;
@override
Widget build(BuildContext context) {
@@ -102,7 +104,9 @@ class ViewMoreActionButton extends StatelessWidget {
// Chat doesn't change collapse
// Only show collapse all pages if the view has child views
if (view.layout != ViewLayoutPB.Chat && view.childViews.isNotEmpty) {
if (view.layout != ViewLayoutPB.Chat &&
view.childViews.isNotEmpty &&
isExpanded) {
actionTypes.add(ViewMoreActionType.collapseAllPages);
actionTypes.add(ViewMoreActionType.divider);
}
@@ -38,20 +38,23 @@ class InitLocalAIIndicator extends StatelessWidget {
],
);
case RunningStatePB.Running:
return Row(
children: [
const HSpace(8),
const FlowySvg(
FlowySvgs.download_success_s,
color: Color(0xFF2E7D32),
),
const HSpace(6),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoaded.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
return SizedBox(
height: 30,
child: Row(
children: [
const HSpace(8),
const FlowySvg(
FlowySvgs.download_success_s,
color: Color(0xFF2E7D32),
),
const HSpace(6),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoaded.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
),
);
case RunningStatePB.Stopped:
return Row(
@@ -1,6 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_chat_toggle_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading_model.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart';
@@ -67,53 +68,53 @@ class LocalAIChatSetting extends StatelessWidget {
tapBodyToExpand: false,
tapHeaderToExpand: false,
),
header: const LocalAIChatSettingHeader(),
header: const SizedBox.shrink(),
collapsed: const SizedBox.shrink(),
expanded: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
// child: _LocalLLMInfoWidget(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModel.tr(),
fontSize: 14,
),
),
const Spacer(),
BlocBuilder<LocalAIChatSettingBloc,
LocalAIChatSettingState>(
builder: (context, state) {
return state.fetchModelInfoState.when(
loading: () => Expanded(
child: Row(
children: [
Flexible(
child: FlowyText(
LocaleKeys
.settings_aiPage_keys_fetchLocalModel
.tr(),
),
),
const Spacer(),
const CircularProgressIndicator.adaptive(),
],
BlocBuilder<LocalAIChatSettingBloc, LocalAIChatSettingState>(
builder: (context, state) {
// If the progress indicator is startOfflineAIApp, then don't show the LLM model.
if (state.progressIndicator ==
const LocalAIProgress.startOfflineAIApp()) {
return const SizedBox.shrink();
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModel.tr(),
fontSize: 14,
),
),
finish: (err) {
return (err == null)
const Spacer(),
state.aiModelProgress.when(
init: () => const SizedBox.shrink(),
loading: () {
return const Expanded(
child: Row(
children: [
Spacer(),
CircularProgressIndicator.adaptive(),
],
),
);
},
finish: (err) => (err == null)
? const _SelectLocalModelDropdownMenu()
: const SizedBox.shrink();
},
);
},
),
],
: const SizedBox.shrink(),
),
],
);
}
},
),
const IntrinsicHeight(child: _LocalLLMInfoWidget()),
const IntrinsicHeight(child: _LocalAIStateWidget()),
],
),
),
@@ -200,8 +201,8 @@ class _SelectLocalModelDropdownMenu extends StatelessWidget {
}
}
class _LocalLLMInfoWidget extends StatelessWidget {
const _LocalLLMInfoWidget();
class _LocalAIStateWidget extends StatelessWidget {
const _LocalAIStateWidget();
@override
Widget build(BuildContext context) {
@@ -210,15 +211,8 @@ class _LocalLLMInfoWidget extends StatelessWidget {
final error = errorFromState(state);
if (error == null) {
// If the error is null, handle selected llm model.
if (state.localAIInfo != null) {
final child = state.localAIInfo!.when(
requestDownloadInfo: (
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
_showDownloadDialog(context, llmResource, llmModel);
return const SizedBox.shrink();
},
if (state.progressIndicator != null) {
final child = state.progressIndicator!.when(
showDownload: (
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
@@ -241,6 +235,13 @@ class _LocalLLMInfoWidget extends StatelessWidget {
},
finishDownload: () => const InitLocalAIIndicator(),
checkPluginState: () => const PluginStateIndicator(),
startOfflineAIApp: () => OpenOrDownloadOfflineAIApp(
onRetry: () {
context
.read<LocalAIChatSettingBloc>()
.add(const LocalAIChatSettingEvent.refreshAISetting());
},
),
);
return Padding(
@@ -266,44 +267,11 @@ class _LocalLLMInfoWidget extends StatelessWidget {
);
}
void _showDownloadDialog(
BuildContext context,
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) {
return _LLMModelDownloadDialog(
llmResource: llmResource,
onOkPressed: () {
context.read<LocalAIChatSettingBloc>().add(
LocalAIChatSettingEvent.startDownloadModel(
llmModel,
),
);
},
onCancelPressed: () {
context.read<LocalAIChatSettingBloc>().add(
const LocalAIChatSettingEvent.cancelDownload(),
);
},
);
},
);
},
debugLabel: 'localModel.download',
);
}
FlowyError? errorFromState(LocalAIChatSettingState state) {
final err = state.fetchModelInfoState.when(
final err = state.aiModelProgress.when(
loading: () => null,
finish: (err) => err,
init: () {},
);
if (err == null) {
@@ -317,39 +285,48 @@ class _LocalLLMInfoWidget extends StatelessWidget {
}
}
class _LLMModelDownloadDialog extends StatelessWidget {
const _LLMModelDownloadDialog({
required this.llmResource,
required this.onOkPressed,
required this.onCancelPressed,
});
final LocalModelResourcePB llmResource;
final VoidCallback onOkPressed;
final VoidCallback onCancelPressed;
void _showDownloadDialog(
BuildContext context,
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
if (llmResource.pendingResources.isEmpty) {
return;
}
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr(
final res = llmResource.pendingResources.first;
String desc = "";
switch (res.resType) {
case PendingResourceTypePB.AIModel:
desc = LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr(
args: [
llmResource.pendingResources[0].name,
llmResource.pendingResources[0].fileSize,
],
),
message: llmResource.pendingResources[0].fileSize == 0
? ""
: LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr(
args: [
llmResource.pendingResources[0].name,
llmResource.pendingResources[0].fileSize.toString(),
],
),
okTitle: LocaleKeys.button_confirm.tr(),
cancelTitle: LocaleKeys.button_cancel.tr(),
onOkPressed: onOkPressed,
onCancelPressed: onCancelPressed,
titleUpperCase: false,
);
);
break;
case PendingResourceTypePB.OfflineApp:
desc = LocaleKeys.settings_aiPage_keys_downloadAppFlowyOfflineAI.tr();
break;
}
showConfirmDialog(
context: context,
style: ConfirmPopupStyle.cancelAndOk,
title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr(
args: [res.name],
),
description: desc,
confirmLabel: LocaleKeys.button_confirm.tr(),
onConfirm: () => context.read<LocalAIChatSettingBloc>().add(
LocalAIChatSettingEvent.startDownloadModel(
llmModel,
),
),
onCancel: () => context.read<LocalAIChatSettingBloc>().add(
const LocalAIChatSettingEvent.cancelDownload(),
),
);
}
class _ShowDownloadIndicator extends StatelessWidget {
@@ -381,29 +358,7 @@ class _ShowDownloadIndicator extends StatelessWidget {
color: Color(0xFF005483),
),
onTap: () {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) {
return _LLMModelDownloadDialog(
llmResource: llmResource,
onOkPressed: () {
context.read<LocalAIChatSettingBloc>().add(
LocalAIChatSettingEvent.startDownloadModel(
llmModel,
),
);
},
onCancelPressed: () {
context.read<LocalAIChatSettingBloc>().add(
const LocalAIChatSettingEvent
.cancelDownload(),
);
},
);
},
);
_showDownloadDialog(context, llmResource, llmModel);
},
),
),
@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_chat_setting.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
@@ -7,9 +10,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:expandable/expandable.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class LocalAISetting extends StatefulWidget {
@@ -108,20 +108,18 @@ class LocalAISettingHeader extends StatelessWidget {
value: isEnabled,
onChanged: (value) {
if (isEnabled) {
showDialog(
showConfirmDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) {
return _ToggleLocalAIDialog(
onOkPressed: () {
context
.read<LocalAIToggleBloc>()
.add(const LocalAIToggleEvent.toggle());
},
onCancelPressed: () {},
);
},
title: LocaleKeys
.settings_aiPage_keys_disableLocalAITitle
.tr(),
description: LocaleKeys
.settings_aiPage_keys_disableLocalAIDescription
.tr(),
confirmLabel: LocaleKeys.button_confirm.tr(),
onConfirm: () => context
.read<LocalAIToggleBloc>()
.add(const LocalAIToggleEvent.toggle()),
);
} else {
context
@@ -138,24 +136,3 @@ class LocalAISettingHeader extends StatelessWidget {
);
}
}
class _ToggleLocalAIDialog extends StatelessWidget {
const _ToggleLocalAIDialog({
required this.onOkPressed,
required this.onCancelPressed,
});
final VoidCallback onOkPressed;
final VoidCallback onCancelPressed;
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
message: LocaleKeys.settings_aiPage_keys_disableLocalAIDialog.tr(),
okTitle: LocaleKeys.button_confirm.tr(),
cancelTitle: LocaleKeys.button_cancel.tr(),
onOkPressed: onOkPressed,
onCancelPressed: onCancelPressed,
titleUpperCase: false,
);
}
}
@@ -1,10 +1,14 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/download_offline_ai_app_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -21,8 +25,15 @@ class PluginStateIndicator extends StatelessWidget {
return state.action.when(
init: () => const _InitPlugin(),
ready: () => const _LocalAIReadyToUse(),
restart: () => const _ReloadButton(),
restartPlugin: () => const _ReloadButton(),
loadingPlugin: () => const _InitPlugin(),
startAIOfflineApp: () => OpenOrDownloadOfflineAIApp(
onRetry: () {
context
.read<PluginStateBloc>()
.add(const PluginStateEvent.started());
},
),
);
},
),
@@ -35,9 +46,15 @@ class _InitPlugin extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 20,
child: CircularProgressIndicator.adaptive(),
return Row(
children: [
FlowyText(LocaleKeys.settings_aiPage_keys_localAIStart.tr()),
const Spacer(),
const SizedBox(
height: 20,
child: CircularProgressIndicator.adaptive(),
),
],
);
}
}
@@ -124,3 +141,106 @@ class _LocalAIReadyToUse extends StatelessWidget {
);
}
}
class OpenOrDownloadOfflineAIApp extends StatelessWidget {
const OpenOrDownloadOfflineAIApp({required this.onRetry, super.key});
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DownloadOfflineAIBloc(),
child: BlocBuilder<DownloadOfflineAIBloc, DownloadOfflineAIState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
maxLines: 3,
textAlign: TextAlign.left,
text: TextSpan(
children: <TextSpan>[
TextSpan(
text:
"${LocaleKeys.settings_aiPage_keys_offlineAIInstruction1.tr()} ",
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(height: 1.5),
),
TextSpan(
text:
" ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction2.tr()} ",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: FontSizes.s14,
color: Theme.of(context).colorScheme.primary,
height: 1.5,
),
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString(
"https://docs.appflowy.io/docs/appflowy/product/appflowy-ai-offline",
),
),
TextSpan(
text:
" ${LocaleKeys.settings_aiPage_keys_offlineAIInstruction3.tr()} ",
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(height: 1.5),
),
TextSpan(
text:
"${LocaleKeys.settings_aiPage_keys_offlineAIDownload1.tr()} ",
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(height: 1.5),
),
TextSpan(
text:
" ${LocaleKeys.settings_aiPage_keys_offlineAIDownload2.tr()} ",
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: FontSizes.s14,
color: Theme.of(context).colorScheme.primary,
height: 1.5,
),
recognizer: TapGestureRecognizer()
..onTap =
() => context.read<DownloadOfflineAIBloc>().add(
const DownloadOfflineAIEvent.started(),
),
),
TextSpan(
text:
" ${LocaleKeys.settings_aiPage_keys_offlineAIDownload3.tr()} ",
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(height: 1.5),
),
],
),
),
const SizedBox(
height: 6,
), // Replaced VSpace with SizedBox for simplicity
SizedBox(
height: 30,
child: FlowyButton(
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(horizontal: 12),
text: FlowyText(
LocaleKeys.settings_aiPage_keys_activeOfflineAI.tr(),
),
onTap: onRetry,
),
),
],
);
},
),
);
}
}
@@ -7,7 +7,6 @@ import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/m
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -53,11 +52,12 @@ class SettingsAIView extends StatelessWidget {
];
children.add(const _AISearchToggle(value: false));
children.add(
_LocalAIOnBoarding(
workspaceId: userProfile.workspaceId,
),
);
// TODO(nathan): enable local ai
// children.add(
// _LocalAIOnBoarding(
// workspaceId: userProfile.workspaceId,
// ),
// );
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
@@ -114,6 +114,7 @@ class _AISearchToggle extends StatelessWidget {
}
}
// ignore: unused_element
class _LocalAIOnBoarding extends StatelessWidget {
const _LocalAIOnBoarding({required this.workspaceId});
final String workspaceId;
@@ -129,7 +130,7 @@ class _LocalAIOnBoarding extends StatelessWidget {
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
builder: (context, state) {
// Show the local AI settings if the user has purchased the AI Local plan
if (kDebugMode || state.isPurchaseAILocal) {
if (state.isPurchaseAILocal) {
return const LocalAISetting();
} else {
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan
@@ -1,6 +1,3 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -22,6 +19,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsAccountView extends StatefulWidget {
@@ -244,80 +243,82 @@ class SignInOutButton extends StatelessWidget {
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
children: [
const FlowySvg(
FlowySvgs.arrow_back_m,
size: Size.square(24),
),
const HSpace(8),
FlowyText.semibold(
LocaleKeys.button_back.tr(),
fontSize: 16,
),
],
),
),
),
const Spacer(),
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: FlowySvg(
FlowySvgs.m_close_m,
size: const Size.square(20),
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_accountPage_login_loginLabel
.tr(),
fontSize: 22,
color: Theme.of(context).colorScheme.tertiary,
maxLines: null,
),
),
],
),
const VSpace(16),
const SignInWithMagicLinkButtons(),
if (isAuthEnabled) ...[
const VSpace(20),
child: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Flexible(child: Divider(thickness: 1)),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Row(
children: [
const FlowySvg(
FlowySvgs.arrow_back_m,
size: Size.square(24),
),
const HSpace(8),
FlowyText.semibold(
LocaleKeys.button_back.tr(),
fontSize: 16,
),
],
),
),
child: FlowyText.regular(
LocaleKeys.signIn_or.tr(),
),
const Spacer(),
GestureDetector(
onTap: Navigator.of(context).pop,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: FlowySvg(
FlowySvgs.m_close_m,
size: const Size.square(20),
color: Theme.of(context).colorScheme.outline,
),
),
),
const Flexible(child: Divider(thickness: 1)),
],
),
const VSpace(10),
SettingThirdPartyLogin(didLogin: onAction),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_accountPage_login_loginLabel
.tr(),
fontSize: 22,
color: Theme.of(context).colorScheme.tertiary,
maxLines: null,
),
),
],
),
const VSpace(16),
const SignInWithMagicLinkButtons(),
if (isAuthEnabled) ...[
const VSpace(20),
Row(
children: [
const Flexible(child: Divider(thickness: 1)),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: FlowyText.regular(
LocaleKeys.signIn_or.tr(),
),
),
const Flexible(child: Divider(thickness: 1)),
],
),
const VSpace(10),
SettingThirdPartyLogin(didLogin: onAction),
],
],
],
),
),
),
),
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/util/int64_extension.dart';
@@ -214,23 +212,24 @@ class _SettingsBillingViewState extends State<SettingsBillingView> {
// Currently, the AI Local tile is only available on macOS
// TODO(nathan): enable windows and linux
if (Platform.isMacOS)
_AITile(
plan: SubscriptionPlanPB.AiLocal,
label: LocaleKeys
.settings_billingPage_addons_aiOnDevice_label
.tr(),
description: LocaleKeys
.settings_billingPage_addons_aiOnDevice_description,
activeDescription: LocaleKeys
.settings_billingPage_addons_aiOnDevice_activeDescription,
canceledDescription: LocaleKeys
.settings_billingPage_addons_aiOnDevice_canceledDescription,
subscriptionInfo:
state.subscriptionInfo.addOns.firstWhereOrNull(
(a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
),
),
// TODO(nathan): enable local ai
// if (Platform.isMacOS)
// _AITile(
// plan: SubscriptionPlanPB.AiLocal,
// label: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_label
// .tr(),
// description: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_description,
// activeDescription: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_activeDescription,
// canceledDescription: LocaleKeys
// .settings_billingPage_addons_aiOnDevice_canceledDescription,
// subscriptionInfo:
// state.subscriptionInfo.addOns.firstWhereOrNull(
// (a) => a.type == WorkspaceAddOnPBType.AddOnAiLocal,
// ),
// ),
],
),
],
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -141,43 +139,44 @@ class _SettingsPlanViewState extends State<SettingsPlanView> {
// Currently, the AI Local tile is only available on macOS
// TODO(nathan): enable windows and linux
if (Platform.isMacOS)
Flexible(
child: _AddOnBox(
title: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_title
.tr(),
description: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_description
.tr(),
price: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_price
.tr(
args: [
SubscriptionPlanPB.AiLocal.priceAnnualBilling,
],
),
priceInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_priceInfo
.tr(),
billingInfo: LocaleKeys
.settings_planPage_planUsage_addons_aiOnDevice_billingInfo
.tr(
args: [
SubscriptionPlanPB.AiLocal.priceMonthBilling,
],
),
buttonText: state.subscriptionInfo.hasAIOnDevice
? LocaleKeys
.settings_planPage_planUsage_addons_activeLabel
.tr()
: LocaleKeys
.settings_planPage_planUsage_addons_addLabel
.tr(),
isActive: state.subscriptionInfo.hasAIOnDevice,
plan: SubscriptionPlanPB.AiLocal,
),
),
// TODO(nathan): enable local ai
// if (Platform.isMacOS)
// Flexible(
// child: _AddOnBox(
// title: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_title
// .tr(),
// description: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_description
// .tr(),
// price: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_price
// .tr(
// args: [
// SubscriptionPlanPB.AiLocal.priceAnnualBilling,
// ],
// ),
// priceInfo: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_priceInfo
// .tr(),
// billingInfo: LocaleKeys
// .settings_planPage_planUsage_addons_aiOnDevice_billingInfo
// .tr(
// args: [
// SubscriptionPlanPB.AiLocal.priceMonthBilling,
// ],
// ),
// buttonText: state.subscriptionInfo.hasAIOnDevice
// ? LocaleKeys
// .settings_planPage_planUsage_addons_activeLabel
// .tr()
// : LocaleKeys
// .settings_planPage_planUsage_addons_addLabel
// .tr(),
// isActive: state.subscriptionInfo.hasAIOnDevice,
// plan: SubscriptionPlanPB.AiLocal,
// ),
// ),
],
),
],
@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart';
@@ -25,7 +25,7 @@ class SettingsDialog extends StatelessWidget {
required this.dismissDialog,
required this.didLogout,
required this.restartApp,
this.initPage,
this.initPage,
}) : super(key: ValueKey(user.id));
final VoidCallback dismissDialog;
@@ -39,6 +39,7 @@ class SettingsDialog extends StatelessWidget {
return BlocProvider<SettingsDialogBloc>(
create: (context) => SettingsDialogBloc(
user,
context.read<UserWorkspaceBloc>().state.currentWorkspaceMember,
initPage: initPage,
)..add(const SettingsDialogEvent.initial()),
child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
@@ -60,6 +61,7 @@ class SettingsDialog extends StatelessWidget {
.add(SettingsDialogEvent.setSelectedPage(index)),
currentPage:
context.read<SettingsDialogBloc>().state.page,
isBillingEnabled: state.isBillingEnabled,
member: context
.read<UserWorkspaceBloc>()
.state
@@ -1,4 +1,3 @@
import 'package:appflowy_backend/log.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -13,7 +12,9 @@ import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_bu
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -355,25 +356,25 @@ class BillingGateGuard extends StatelessWidget {
Future<bool> isBillingEnabled() async {
final result = await UserEventGetCloudConfig().send();
return result.fold((cloudSetting) {
final whiteList = [
"https://beta.appflowy.cloud",
"https://test.appflowy.cloud",
];
if (kDebugMode) {
whiteList.add("http://localhost:8000");
}
return result.fold(
(cloudSetting) {
final whiteList = [
"https://beta.appflowy.cloud",
"https://test.appflowy.cloud",
];
if (kDebugMode) {
whiteList.add("http://localhost:8000");
}
if (whiteList.contains(cloudSetting.serverUrl)) {
return true;
} else {
Log.warn(
"Billing is not enabled for this server:${cloudSetting.serverUrl}",
);
final isWhiteListed = whiteList.contains(cloudSetting.serverUrl);
if (!isWhiteListed) {
Log.warn("Billing is not enabled for server ${cloudSetting.serverUrl}");
}
return isWhiteListed;
},
(err) {
Log.error("Failed to get cloud config: $err");
return false;
}
}, (err) {
Log.error("Failed to get cloud config: $err");
return false;
});
},
);
}
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
@@ -17,12 +16,14 @@ class SettingsMenu extends StatelessWidget {
required this.changeSelectedPage,
required this.currentPage,
required this.userProfile,
required this.isBillingEnabled,
this.member,
});
final Function changeSelectedPage;
final SettingsPage currentPage;
final UserProfilePB userProfile;
final bool isBillingEnabled;
final WorkspaceMemberPB? member;
@override
@@ -112,11 +113,7 @@ class SettingsMenu extends StatelessWidget {
),
changeSelectedPage: changeSelectedPage,
),
if (FeatureFlag.planBilling.isOn &&
userProfile.authenticator ==
AuthenticatorPB.AppFlowyCloud &&
member != null &&
member!.role.isOwner) ...[
if (FeatureFlag.planBilling.isOn && isBillingEnabled) ...[
SettingsMenuElement(
page: SettingsPage.plan,
selectedPage: currentPage,
@@ -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/startup/tasks/app_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:flowy_infra/size.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/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:toastification/toastification.dart';
export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
@@ -303,6 +304,18 @@ void showToastNotification(
String? description,
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(
context: context,
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({
required BuildContext context,
required String name,
@@ -361,6 +418,7 @@ Future<void> showConfirmDialog({
required String title,
required String description,
VoidCallback? onConfirm,
VoidCallback? onCancel,
String? confirmLabel,
ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk,
}) {
@@ -377,6 +435,7 @@ Future<void> showConfirmDialog({
title: title,
description: description,
onConfirm: () => onConfirm?.call(),
onCancel: () => onCancel?.call(),
confirmLabel: confirmLabel,
style: style,
),
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -14,7 +16,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path/path.dart';
@@ -190,7 +191,7 @@ class InteractiveImageToolbar extends StatelessWidget {
tooltip: LocaleKeys
.document_imageBlock_interactiveViewer_toolbar_closeViewer
.tr(),
icon: FlowySvgs.close_s,
icon: FlowySvgs.close_viewer_s,
onTap: () => Navigator.of(context).pop(),
),
],
@@ -291,11 +292,12 @@ class _ToolbarItem extends StatelessWidget {
isDisabled ? Colors.transparent : Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 32,
height: 32,
padding: const EdgeInsets.all(8),
child: FlowySvg(
icon,
size: const Size.square(16),
color: isDisabled ? Colors.grey : Colors.white,
),
),
@@ -110,10 +110,14 @@ class _InteractiveImageViewerState extends State<InteractiveImageViewer> {
child: SizedBox(
height: size.height,
width: size.width,
child: widget.imageProvider.renderImage(
context,
currentIndex,
userProfile,
child: GestureDetector(
// We can consider adding zoom behavior instead in a later iteration
onDoubleTap: () => Navigator.of(context).pop(),
child: widget.imageProvider.renderImage(
context,
currentIndex,
userProfile,
),
),
),
),
@@ -1,7 +1,6 @@
import 'dart:io';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
@@ -311,7 +310,7 @@ class FlowyTextButton extends StatelessWidget {
constraints: const BoxConstraints(minHeight: 32),
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: const Color(0xFF005483),
fontColor: AFThemeExtension.of(context).strongText,
fontColor: Theme.of(context).colorScheme.onPrimary,
fontHoverColor: Colors.white,
onPressed: onPressed,
);
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -201,7 +202,7 @@ class FlowyText extends StatelessWidget {
}
if (withTooltip) {
child = Tooltip(
child = FlowyTooltip(
message: text,
child: child,
);
+13 -5
View File
@@ -53,8 +53,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "268aae9"
resolved-ref: "268aae905b18efc8a3a9c88dc75ebd19b314bd43"
ref: aac7729
resolved-ref: aac77292a1a175fd7450eef30167032d3cec7fea
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "3.1.0"
@@ -806,6 +806,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
flutter_staggered_grid_view:
dependency: "direct main"
description:
name: flutter_staggered_grid_view
sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_sticky_header:
dependency: transitive
description:
@@ -1670,10 +1678,10 @@ packages:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
realtime_client:
dependency: transitive
description:
@@ -2443,5 +2451,5 @@ packages:
source: hosted
version: "2.0.0"
sdks:
dart: ">=3.3.0 <4.0.0"
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.0"
+3 -1
View File
@@ -149,6 +149,8 @@ dependencies:
desktop_drop: ^0.4.4
cross_file: ^0.3.4+1
flutter_staggered_grid_view: ^0.7.0
# Window Manager for MacOS and Linux
window_manager: ^0.3.9
@@ -194,7 +196,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "268aae9"
ref: "aac7729"
appflowy_editor_plugins:
git:
+26 -22
View File
@@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bincode",
@@ -192,7 +192,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bytes",
@@ -206,7 +206,7 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f3b678e36f22012b241f8e2f3cb811be2da245c0#f3b678e36f22012b241f8e2f3cb811be2da245c0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec#8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec"
dependencies = [
"anyhow",
"appflowy-plugin",
@@ -225,7 +225,7 @@ dependencies = [
[[package]]
name = "appflowy-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f3b678e36f22012b241f8e2f3cb811be2da245c0#f3b678e36f22012b241f8e2f3cb811be2da245c0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec#8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec"
dependencies = [
"anyhow",
"cfg-if",
@@ -826,7 +826,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"again",
"anyhow",
@@ -876,7 +876,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"collab-entity",
"collab-rt-entity",
@@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"futures-channel",
"futures-util",
@@ -962,7 +962,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-trait",
@@ -986,7 +986,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-trait",
@@ -1016,7 +1016,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"collab",
@@ -1036,22 +1036,26 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"bytes",
"collab",
"getrandom 0.2.10",
"prost",
"prost-build",
"protoc-bin-vendored",
"serde",
"serde_json",
"serde_repr",
"uuid",
"walkdir",
]
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"chrono",
@@ -1089,7 +1093,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-stream",
@@ -1128,7 +1132,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bincode",
@@ -1153,7 +1157,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"async-trait",
@@ -1170,7 +1174,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"collab",
@@ -1417,7 +1421,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.6",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@@ -1528,7 +1532,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
@@ -3034,7 +3038,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"futures-util",
@@ -3051,7 +3055,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
@@ -3483,7 +3487,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bytes",
@@ -6027,7 +6031,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
+10 -10
View File
@@ -53,7 +53,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0062c950677f7f633f5b7edabc827a35d3bc92c3" }
[dependencies]
serde_json.workspace = true
@@ -116,17 +116,17 @@ custom-protocol = ["tauri/custom-protocol"]
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f3b678e36f22012b241f8e2f3cb811be2da245c0" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f3b678e36f22012b241f8e2f3cb811be2da245c0" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec" }
+26 -22
View File
@@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bincode",
@@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bytes",
@@ -197,7 +197,7 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f3b678e36f22012b241f8e2f3cb811be2da245c0#f3b678e36f22012b241f8e2f3cb811be2da245c0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec#8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec"
dependencies = [
"anyhow",
"appflowy-plugin",
@@ -216,7 +216,7 @@ dependencies = [
[[package]]
name = "appflowy-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f3b678e36f22012b241f8e2f3cb811be2da245c0#f3b678e36f22012b241f8e2f3cb811be2da245c0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec#8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec"
dependencies = [
"anyhow",
"cfg-if",
@@ -800,7 +800,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"again",
"anyhow",
@@ -850,7 +850,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"collab-entity",
"collab-rt-entity",
@@ -862,7 +862,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"futures-channel",
"futures-util",
@@ -945,7 +945,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-trait",
@@ -969,7 +969,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-trait",
@@ -999,7 +999,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"collab",
@@ -1019,22 +1019,26 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"bytes",
"collab",
"getrandom 0.2.12",
"prost",
"prost-build",
"protoc-bin-vendored",
"serde",
"serde_json",
"serde_repr",
"uuid",
"walkdir",
]
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"chrono",
@@ -1072,7 +1076,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-stream",
@@ -1111,7 +1115,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bincode",
@@ -1136,7 +1140,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"async-trait",
@@ -1153,7 +1157,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"collab",
@@ -1407,7 +1411,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.10",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@@ -1518,7 +1522,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
@@ -3101,7 +3105,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"futures-util",
@@ -3118,7 +3122,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
@@ -3555,7 +3559,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bytes",
@@ -6091,7 +6095,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
+10 -10
View File
@@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0062c950677f7f633f5b7edabc827a35d3bc92c3" }
[dependencies]
serde_json.workspace = true
@@ -116,18 +116,18 @@ custom-protocol = ["tauri/custom-protocol"]
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f3b678e36f22012b241f8e2f3cb811be2da245c0" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f3b678e36f22012b241f8e2f3cb811be2da245c0" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec" }
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="11.6765" y="3.27295" width="1.48562" height="11.8849" rx="0.742808" transform="rotate(45 11.6765 3.27295)" fill="#333333"/>
<rect x="12.7271" y="11.6766" width="1.48562" height="11.8849" rx="0.742808" transform="rotate(135 12.7271 11.6766)" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 370 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M215.74-528Q186-528 165-549.5 144-571 144-600v-144q0-29.7 21.18-50.85Q186.35-816 216.09-816h144.17Q390-816 411-794.85q21 21.15 21 50.85v144q0 29-21.18 50.5-21.17 21.5-50.91 21.5H215.74Zm0 384Q186-144 165-165.18q-21-21.17-21-50.91v-144.17Q144-390 165.18-411q21.17-21 50.91-21h144.17Q390-432 411-410.82q21 21.17 21 50.91v144.17Q432-186 410.82-165q-21.17 21-50.91 21H215.74ZM600-528q-29 0-50.5-21.5T528-600v-144q0-29.7 21.5-50.85Q571-816 600-816h144q29.7 0 50.85 21.15Q816-773.7 816-744v144q0 29-21.15 50.5T744-528H600Zm0 384q-29 0-50.5-21.18-21.5-21.17-21.5-50.91v-144.17Q528-390 549.5-411q21.5-21 50.5-21h144q29.7 0 50.85 21.18Q816-389.65 816-359.91v144.17Q816-186 794.85-165 773.7-144 744-144H600ZM216-600h144v-144H216v144Zm384 0h144v-144H600v144Zm0 384h144v-144H600v144Zm-384 0h144v-144H216v144Zm384-384Zm0 240Zm-240 0Zm0-240Z"/></svg>

After

Width:  |  Height:  |  Size: 952 B

@@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.64912 1.71521C2.64912 1.43942 2.84766 1.2002 3.16 1.2002H8.855C9.125 1.2002 9.36707 1.40199 9.36707 1.71021C9.36707 1.89847 9.20994 2.20021 8.855 2.20021L8.01019 2.20277L8.00984 4.46598C8.00984 4.53315 8.05692 4.59785 8.10288 4.64382L10.1752 7.02856C10.223 7.07629 10.25 7.14117 10.25 7.20834L10.2495 7.61027C10.25 7.89027 10.025 8.11628 9.75 8.11628L6.51011 8.11753L6.51 11.2002C6.50999 11.4763 6.28614 11.7002 6.01 11.7002H5.99949C5.72355 11.7002 5.49977 11.4761 5.49949 11.2002L5.49613 8.11681L2.255 8.11631C1.96831 8.11631 1.75 7.89027 1.75 7.61027L1.75046 7.20898C1.75048 7.14179 1.77659 7.07678 1.82409 7.02928L3.90196 4.64452C3.94951 4.59697 4.00575 4.53192 4.00575 4.46474L4.00593 2.20171L3.16 2.20021C2.89258 2.20171 2.64912 1.9824 2.64912 1.71521Z" fill="#4DA594"/>
<path d="M2.64912 1.71521C2.64912 1.43942 2.84766 1.2002 3.16 1.2002H8.855C9.125 1.2002 9.36707 1.40199 9.36707 1.71021C9.36707 1.89847 9.20994 2.20021 8.855 2.20021L8.01019 2.20277L8.00984 4.46598C8.00984 4.53315 8.05692 4.59785 8.10288 4.64382L10.1752 7.02856C10.223 7.07629 10.25 7.14117 10.25 7.20834L10.2495 7.61027C10.25 7.89027 10.025 8.11628 9.75 8.11628L6.51011 8.11753L6.51 11.2002C6.50999 11.4763 6.28614 11.7002 6.01 11.7002H5.99949C5.72355 11.7002 5.49977 11.4761 5.49949 11.2002L5.49613 8.11681L2.255 8.11631C1.96831 8.11631 1.75 7.89027 1.75 7.61027L1.75046 7.20898C1.75048 7.14179 1.77659 7.07678 1.82409 7.02928L3.90196 4.64452C3.94951 4.59697 4.00575 4.53192 4.00575 4.46474L4.00593 2.20171L3.16 2.20021C2.89258 2.20171 2.64912 1.9824 2.64912 1.71521Z" fill="#F65B6E"/>
</svg>

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 891 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M96-384v-192q0-29.7 21.21-50.85 21.21-21.15 51-21.15T219-626.85q21 21.15 21 50.85v192q0 29.7-21.21 50.85-21.21 21.15-51 21.15T117-333.15Q96-354.3 96-384Zm264 144q-33 0-52.5-19.5T288-312v-336q0-33 19.5-52.5T360-720h240q33 0 52.5 19.5T672-648v336q0 33-19.5 52.5T600-240H360Zm360-144v-192q0-29.7 21.21-50.85 21.21-21.15 51-21.15T843-626.85q21 21.15 21 50.85v192q0 29.7-21.21 50.85-21.21 21.15-51 21.15T741-333.15Q720-354.3 720-384Zm-360 72h240v-336H360v336Zm120-168Z"/></svg>

After

Width:  |  Height:  |  Size: 588 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M216-144q-29 0-50.5-21.5T144-216v-528q0-29.7 21.5-50.85Q187-816 216-816h528q29.7 0 50.85 21.15Q816-773.7 816-744v528q0 29-21.15 50.5T744-144H216Zm0-72h228v-528H216v528Zm300 0h228v-264H516v264Zm0-336h228v-192H516v192Z"/></svg>

After

Width:  |  Height:  |  Size: 341 B

+20 -4
View File
@@ -159,6 +159,7 @@
"chat": {
"newChat": "AI Chat",
"inputMessageHint": "Message @:appName AI",
"inputLocalAIMessageHint": "Message @:appName Local AI",
"unsupportedCloudPrompt": "This feature is only available when using @:appName Cloud",
"relatedQuestion": "Related",
"serverUnavailable": "Service Temporarily Unavailable. Please try again later.",
@@ -276,7 +277,7 @@
"justNow": "just now",
"minutesAgo": "{count} minutes ago",
"lastViewed": "Last viewed",
"favoriteAt": "Favorited at",
"favoriteAt": "Favorited",
"emptyRecent": "No Recent Documents",
"emptyRecentDescription": "As you view documents, they will appear here for easy retrieval",
"emptyFavorite": "No Favorite Documents",
@@ -337,6 +338,8 @@
"removeFromFavorites": "Remove from favorites",
"removeFromRecent": "Remove from recent",
"addToFavorites": "Add to favorites",
"favoriteSuccessfully": "Favorited success",
"unfavoriteSuccessfully": "Unfavorited success",
"rename": "Rename",
"helpCenter": "Help Center",
"add": "Add",
@@ -652,6 +655,7 @@
"llmModel": "Language Model",
"llmModelType": "Language Model Type",
"downloadLLMPrompt": "Download {}",
"downloadAppFlowyOfflineAI": "Downloading AI offline package will enable AI to run on your device. Do you want to continue?",
"downloadLLMPromptDetail": "Downloading {} local model will take up to {} of storage. Do you want to continue?",
"downloadAIModelButton": "Download AI model",
"downloadingModel": "Downloading",
@@ -661,9 +665,17 @@
"localAIStopped": "Local AI stopped",
"failToLoadLocalAI": "Failed to start local AI",
"restartLocalAI": "Restart Local AI",
"disableLocalAIDialog": "Do you want to disable local AI?",
"disableLocalAITitle": "Disable local AI",
"disableLocalAIDescription": "Do you want to disable local AI?",
"localAIToggleTitle": "Toggle to enable or disable local AI",
"fetchLocalModel": "Fetch local model configuration",
"offlineAIInstruction1": "Follow the",
"offlineAIInstruction2": "instruction",
"offlineAIInstruction3": "to enable offline AI.",
"offlineAIDownload1": "If you have not downloaded the AppFlowy AI, please",
"offlineAIDownload2": "download",
"offlineAIDownload3": "it first",
"activeOfflineAI": "Active",
"downloadOfflineAI": "Download",
"openModelDirectory": "Open folder"
}
},
@@ -1502,7 +1514,11 @@
"photoKeyword": "photo",
"photoBrowserKeyword": "photo browser",
"galleryKeyword": "gallery",
"addImageTooltip": "Add image"
"addImageTooltip": "Add image",
"changeLayoutTooltip": "Change layout",
"browserLayout": "Browser",
"gridLayout": "Grid",
"deleteBlockTooltip": "Delete whole gallery"
},
"math": {
"copiedToPasteBoard": "The math equation has been copied to the clipboard"
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -152,7 +152,7 @@
"moveTo": "移动",
"addToFavorites": "添加到收藏夹",
"copyLink": "复制链接",
"changeIcon": "更改图表",
"changeIcon": "更改图标",
"collapseAllPages": "收起全部子页面"
},
"blankPageTitle": "空白页",
+116 -21
View File
@@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bincode",
@@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bytes",
@@ -197,7 +197,7 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f3b678e36f22012b241f8e2f3cb811be2da245c0#f3b678e36f22012b241f8e2f3cb811be2da245c0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec#8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec"
dependencies = [
"anyhow",
"appflowy-plugin",
@@ -216,7 +216,7 @@ dependencies = [
[[package]]
name = "appflowy-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=f3b678e36f22012b241f8e2f3cb811be2da245c0#f3b678e36f22012b241f8e2f3cb811be2da245c0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec#8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec"
dependencies = [
"anyhow",
"cfg-if",
@@ -718,7 +718,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"again",
"anyhow",
@@ -768,7 +768,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"collab-entity",
"collab-rt-entity",
@@ -780,7 +780,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"futures-channel",
"futures-util",
@@ -823,7 +823,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-trait",
@@ -847,7 +847,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-trait",
@@ -877,7 +877,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"collab",
@@ -897,22 +897,26 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"bytes",
"collab",
"getrandom 0.2.10",
"prost",
"prost-build",
"protoc-bin-vendored",
"serde",
"serde_json",
"serde_repr",
"uuid",
"walkdir",
]
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"chrono",
@@ -950,7 +954,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"async-stream",
@@ -989,7 +993,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bincode",
@@ -1014,7 +1018,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"async-trait",
@@ -1031,7 +1035,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=43b1c98435d63c225229c9def79f2f5213d6eaf1#43b1c98435d63c225229c9def79f2f5213d6eaf1"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=93ec9a38722d5ed0628e6d03a161e2de172967f3#93ec9a38722d5ed0628e6d03a161e2de172967f3"
dependencies = [
"anyhow",
"collab",
@@ -1352,7 +1356,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
@@ -1735,6 +1739,18 @@ dependencies = [
"getrandom 0.2.10",
]
[[package]]
name = "filetime"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.4.1",
"windows-sys 0.52.0",
]
[[package]]
name = "finl_unicode"
version = "1.2.0"
@@ -1790,6 +1806,7 @@ dependencies = [
"lib-infra",
"log",
"md5",
"notify",
"parking_lot 0.12.1",
"protobuf",
"reqwest",
@@ -2453,6 +2470,15 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@@ -2701,7 +2727,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"futures-util",
@@ -2718,7 +2744,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
@@ -3083,7 +3109,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"bytes",
@@ -3094,6 +3120,26 @@ dependencies = [
"tracing",
]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "inout"
version = "0.1.3"
@@ -3177,6 +3223,26 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "kqueue"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -3579,6 +3645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
@@ -3638,6 +3705,25 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.4.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"walkdir",
"windows-sys 0.48.0",
]
[[package]]
name = "ntapi"
version = "0.4.1"
@@ -4674,6 +4760,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "regex"
version = "1.9.5"
@@ -5229,7 +5324,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=c2a839ba8bf9ead44679eb08f3a9680467b767ca#c2a839ba8bf9ead44679eb08f3a9680467b767ca"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0062c950677f7f633f5b7edabc827a35d3bc92c3#0062c950677f7f633f5b7edabc827a35d3bc92c3"
dependencies = [
"anyhow",
"app-error",
+11 -11
View File
@@ -99,8 +99,8 @@ zip = "2.1.3"
# Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "c2a839ba8bf9ead44679eb08f3a9680467b767ca" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0062c950677f7f633f5b7edabc827a35d3bc92c3" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0062c950677f7f633f5b7edabc827a35d3bc92c3" }
[profile.dev]
opt-level = 0
@@ -135,17 +135,17 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "43b1c98435d63c225229c9def79f2f5213d6eaf1" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "93ec9a38722d5ed0628e6d03a161e2de172967f3" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f3b678e36f22012b241f8e2f3cb811be2da245c0" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "f3b678e36f22012b241f8e2f3cb811be2da245c0" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "8ef7d3e4c38fbf92ff9b3630fe79017e95a496ec" }
@@ -1,6 +1,6 @@
use bytes::Bytes;
pub use client_api::entity::ai_dto::{
AppFlowyAIPlugin, CompletionType, LLMModel, LocalAIConfig, ModelInfo, RelatedQuestion,
AppFlowyOfflineAI, CompletionType, LLMModel, LocalAIConfig, ModelInfo, RelatedQuestion,
RepeatedRelatedQuestion, StringOrMessage,
};
pub use client_api::entity::{
+3
View File
@@ -44,6 +44,9 @@ md5 = "0.7.0"
zip = { workspace = true, features = ["deflate"] }
zip-extensions = "0.8.0"
[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
notify = "6.1.1"
[dev-dependencies]
dotenv = "0.15.0"
uuid.workspace = true
@@ -21,7 +21,7 @@ pub trait ChatUserService: Send + Sync + 'static {
fn device_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<String, FlowyError>;
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>;
fn user_data_dir(&self) -> Result<PathBuf, FlowyError>;
fn data_root_dir(&self) -> Result<PathBuf, FlowyError>;
}
pub struct ChatManager {
+30 -1
View File
@@ -1,6 +1,7 @@
use crate::local_ai::local_llm_chat::LLMModelInfo;
use appflowy_plugin::core::plugin::RunningState;
use crate::local_ai::local_llm_resource::PendingResource;
use flowy_chat_pub::cloud::{
ChatMessage, LLMModel, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion,
};
@@ -360,16 +361,38 @@ pub struct PendingResourcePB {
pub name: String,
#[pb(index = 2)]
pub file_size: i64,
pub file_size: String,
#[pb(index = 3)]
pub requirements: String,
#[pb(index = 4)]
pub res_type: PendingResourceTypePB,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum PendingResourceTypePB {
#[default]
OfflineApp = 0,
AIModel = 1,
}
impl From<PendingResource> for PendingResourceTypePB {
fn from(value: PendingResource) -> Self {
match value {
PendingResource::OfflineApp { .. } => PendingResourceTypePB::OfflineApp,
PendingResource::ModelInfoRes { .. } => PendingResourceTypePB::AIModel,
}
}
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct LocalAIPluginStatePB {
#[pb(index = 1)]
pub state: RunningStatePB,
#[pb(index = 2)]
pub offline_ai_ready: bool,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
@@ -416,3 +439,9 @@ pub struct LocalModelStoragePB {
#[pb(index = 1)]
pub file_path: String,
}
#[derive(Default, ProtoBuf, Clone, Debug)]
pub struct OfflineAIPB {
#[pb(index = 1)]
pub link: String,
}
@@ -374,3 +374,22 @@ pub(crate) async fn get_model_storage_directory_handler(
.get_model_storage_directory()?;
data_result_ok(LocalModelStoragePB { file_path })
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn get_offline_app_handler(
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> DataResult<OfflineAIPB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let (tx, rx) = oneshot::channel::<Result<String, FlowyError>>();
tokio::spawn(async move {
let link = chat_manager
.local_ai_controller
.get_offline_ai_app_download_link()
.await?;
let _ = tx.send(Ok(link));
Ok::<_, FlowyError>(())
});
let link = rx.await??;
data_result_ok(OfflineAIPB { link })
}
@@ -57,6 +57,7 @@ pub fn init(chat_manager: Weak<ChatManager>) -> AFPlugin {
ChatEvent::GetModelStorageDirectory,
get_model_storage_directory_handler,
)
.event(ChatEvent::GetOfflineAIAppLink, get_offline_app_handler)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@@ -133,4 +134,7 @@ pub enum ChatEvent {
#[event(output = "LocalModelStoragePB")]
GetModelStorageDirectory = 21,
#[event(output = "OfflineAIPB")]
GetOfflineAIAppLink = 22,
}
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more