Merge branch 'main' into ai_tag_feature

This commit is contained in:
nathan 2024-06-13 15:23:56 +08:00
commit fba6106787
116 changed files with 4678 additions and 651 deletions

View File

@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi" LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.5.9" APPFLOWY_VERSION = "0.6.0"
FLUTTER_DESKTOP_FEATURES = "dart" FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy" PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0" MACOSX_DEPLOYMENT_TARGET = "11.0"

View File

@ -32,69 +32,69 @@ import '../../shared/util.dart';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
final email = '${uuid()}@appflowy.io'; // final email = '${uuid()}@appflowy.io';
group('collaborative workspace', () { group('collaborative workspace', () {
// combine the create and delete workspace test to reduce the time // combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it', testWidgets('create a new workspace, open it and then delete it',
(tester) async { (tester) async {
// only run the test when the feature flag is on // only run the test when the feature flag is on
if (!FeatureFlag.collaborativeWorkspace.isOn) { // if (!FeatureFlag.collaborativeWorkspace.isOn) {
return; // return;
} // }
await tester.initializeAppFlowy( // await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost, // cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email, // email: email,
); // );
await tester.tapGoogleLoginInButton(); // await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage(); // await tester.expectToSeeHomePageWithGetStartedPage();
const name = 'AppFlowy.IO'; // const name = 'AppFlowy.IO';
// the workspace will be opened after created // // the workspace will be opened after created
await tester.createCollaborativeWorkspace(name); // await tester.createCollaborativeWorkspace(name);
final loading = find.byType(Loading); // final loading = find.byType(Loading);
await tester.pumpUntilNotFound(loading); // await tester.pumpUntilNotFound(loading);
Finder success; // Finder success;
final Finder items = find.byType(WorkspaceMenuItem); // final Finder items = find.byType(WorkspaceMenuItem);
// delete the newly created workspace // // delete the newly created workspace
await tester.openCollaborativeWorkspaceMenu(); // await tester.openCollaborativeWorkspaceMenu();
await tester.pumpUntilFound(items); // await tester.pumpUntilFound(items);
expect(items, findsNWidgets(2)); // expect(items, findsNWidgets(2));
expect( // expect(
tester.widget<WorkspaceMenuItem>(items.last).workspace.name, // tester.widget<WorkspaceMenuItem>(items.last).workspace.name,
name, // name,
); // );
final secondWorkspace = find.byType(WorkspaceMenuItem).last; // final secondWorkspace = find.byType(WorkspaceMenuItem).last;
await tester.hoverOnWidget( // await tester.hoverOnWidget(
secondWorkspace, // secondWorkspace,
onHover: () async { // onHover: () async {
// click the more button // // click the more button
final moreButton = find.byType(WorkspaceMoreActionList); // final moreButton = find.byType(WorkspaceMoreActionList);
expect(moreButton, findsOneWidget); // expect(moreButton, findsOneWidget);
await tester.tapButton(moreButton); // await tester.tapButton(moreButton);
// click the delete button // // click the delete button
final deleteButton = find.text(LocaleKeys.button_delete.tr()); // final deleteButton = find.text(LocaleKeys.button_delete.tr());
expect(deleteButton, findsOneWidget); // expect(deleteButton, findsOneWidget);
await tester.tapButton(deleteButton); // await tester.tapButton(deleteButton);
// see the delete confirm dialog // // see the delete confirm dialog
final confirm = // final confirm =
find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr()); // find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr());
expect(confirm, findsOneWidget); // expect(confirm, findsOneWidget);
await tester.tapButton(find.text(LocaleKeys.button_ok.tr())); // await tester.tapButton(find.text(LocaleKeys.button_ok.tr()));
// delete success // // delete success
success = find.text(LocaleKeys.workspace_createSuccess.tr()); // success = find.text(LocaleKeys.workspace_createSuccess.tr());
await tester.pumpUntilFound(success); // await tester.pumpUntilFound(success);
expect(success, findsOneWidget); // expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success); // await tester.pumpUntilNotFound(success);
}, // },
); // );
}); });
}); });
} }

View File

@ -200,7 +200,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -227,4 +227,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.11.3 COCOAPODS: 1.15.2

View File

@ -81,14 +81,24 @@ class KVKeys {
/// The value is a double string. /// The value is a double string.
static const String scaleFactor = 'scaleFactor'; static const String scaleFactor = 'scaleFactor';
/// The key for saving the last opened space /// The key for saving the last opened tab (favorite, recent, space etc.)
/// ///
/// The value is a int string. /// The value is a int string.
static const String lastOpenedSpace = 'lastOpenedSpace'; static const String lastOpenedSpace = 'lastOpenedSpace';
/// The key for saving the space order /// The key for saving the space tab order
/// ///
/// The value is a json string with the following format: /// The value is a json string with the following format:
/// [0, 1, 2] /// [0, 1, 2]
static const String spaceOrder = 'spaceOrder'; static const String spaceOrder = 'spaceOrder';
/// The key for saving the last opened space id (space A, space B)
///
/// The value is a string.
static const String lastOpenedSpaceId = 'lastOpenedSpaceId';
/// The key for saving the upgrade space tag
///
/// The value is a boolean string
static const String hasUpgradedSpace = 'hasUpgradedSpace060';
} }

View File

@ -182,7 +182,6 @@ ActionPane buildEndActionPane(
MobileViewCardType? cardType, MobileViewCardType? cardType,
FolderSpaceType? spaceType, FolderSpaceType? spaceType,
}) { }) {
debugPrint('actions: $actions');
return ActionPane( return ActionPane(
motion: const ScrollMotion(), motion: const ScrollMotion(),
extentRatio: actions.length / 5, extentRatio: actions.length / 5,

View File

@ -28,6 +28,7 @@ const mobileSupportedFieldTypes = [
FieldType.CreatedTime, FieldType.CreatedTime,
FieldType.Checkbox, FieldType.Checkbox,
FieldType.Checklist, FieldType.Checklist,
FieldType.Time,
]; ];
Future<FieldType?> showFieldTypeGridBottomSheet( Future<FieldType?> showFieldTypeGridBottomSheet(

View File

@ -119,6 +119,7 @@ class FieldOptionValues {
case FieldType.RichText: case FieldType.RichText:
case FieldType.URL: case FieldType.URL:
case FieldType.Checkbox: case FieldType.Checkbox:
case FieldType.Time:
return null; return null;
case FieldType.Number: case FieldType.Number:
return NumberTypeOptionPB( return NumberTypeOptionPB(

View File

@ -3,9 +3,11 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/home.dart'; import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -34,16 +36,15 @@ class MobileFolders extends StatelessWidget {
providers: [ providers: [
BlocProvider( BlocProvider(
create: (_) => SidebarSectionsBloc() create: (_) => SidebarSectionsBloc()
..add( ..add(SidebarSectionsEvent.initial(user, workspaceId)),
SidebarSectionsEvent.initial(
user,
workspaceId,
),
),
), ),
BlocProvider( BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
), ),
BlocProvider(
create: (_) =>
SpaceBloc()..add(SpaceEvent.initial(user, workspaceId)),
),
], ],
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>( child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) { listener: (context, state) {
@ -53,24 +54,66 @@ class MobileFolders extends StatelessWidget {
state.currentWorkspace?.workspaceId ?? workspaceId, state.currentWorkspace?.workspaceId ?? workspaceId,
), ),
); );
context.read<SpaceBloc>().add(
SpaceEvent.reset(
user,
state.currentWorkspace?.workspaceId ?? workspaceId,
),
);
}, },
child: BlocConsumer<SidebarSectionsBloc, SidebarSectionsState>( child: MultiBlocListener(
listeners: [
BlocListener<SpaceBloc, SpaceState>(
listenWhen: (p, c) =>
p.lastCreatedPage?.id != c.lastCreatedPage?.id,
listener: (context, state) {
final lastCreatedPage = state.lastCreatedPage;
if (lastCreatedPage != null) {
context.pushView(lastCreatedPage);
}
},
),
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) => listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) { listener: (context, state) {
final lastCreatedRootView = state.lastCreatedRootView; final lastCreatedPage = state.lastCreatedRootView;
if (lastCreatedRootView != null) { if (lastCreatedPage != null) {
context.pushView(lastCreatedRootView); context.pushView(lastCreatedPage);
} }
}, },
),
],
child: BlocBuilder<SidebarSectionsBloc, SidebarSectionsState>(
builder: (context, state) { builder: (context, state) {
final isCollaborativeWorkspace =
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
return SlidableAutoCloseBehavior( return SlidableAutoCloseBehavior(
child: Column( child: Column(
children: [ children: [
...isCollaborativeWorkspace ..._buildSpaceOrSection(context, state),
? [ const VSpace(4.0),
const _TrashButton(),
],
),
);
},
),
),
),
);
}
List<Widget> _buildSpaceOrSection(
BuildContext context,
SidebarSectionsState state,
) {
if (context.watch<SpaceBloc>().state.spaces.isNotEmpty) {
return [
const MobileSpace(),
];
}
if (context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn) {
return [
MobileSectionFolder( MobileSectionFolder(
title: LocaleKeys.sideBar_workspace.tr(), title: LocaleKeys.sideBar_workspace.tr(),
spaceType: FolderSpaceType.public, spaceType: FolderSpaceType.public,
@ -82,23 +125,16 @@ class MobileFolders extends StatelessWidget {
spaceType: FolderSpaceType.private, spaceType: FolderSpaceType.private,
views: state.section.privateViews, views: state.section.privateViews,
), ),
] ];
: [ }
return [
MobileSectionFolder( MobileSectionFolder(
title: LocaleKeys.sideBar_personal.tr(), title: LocaleKeys.sideBar_personal.tr(),
spaceType: FolderSpaceType.public, spaceType: FolderSpaceType.public,
views: state.section.publicViews, views: state.section.publicViews,
), ),
], ];
const VSpace(4.0),
const _TrashButton(),
],
),
);
},
),
),
);
} }
} }

View File

@ -10,15 +10,12 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/util/string_extension.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/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -78,7 +75,6 @@ class MobileViewCard extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTapUp: (_) => context.pushView(view), onTapUp: (_) => context.pushView(view),
onLongPressUp: () => _showActionSheet(context),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -249,59 +245,6 @@ class MobileViewCard extends StatelessWidget {
return date; return date;
} }
Future<void> _showActionSheet(BuildContext context) async {
final viewBloc = context.read<ViewBloc>();
final favoriteBloc = context.read<FavoriteBloc>();
final recentViewsBloc = context.read<RecentViewsBloc?>();
await showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
backgroundColor: AFThemeExtension.of(context).background,
useRootNavigator: true,
builder: (context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: viewBloc),
BlocProvider.value(value: favoriteBloc),
if (recentViewsBloc != null)
BlocProvider.value(value: recentViewsBloc),
],
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return MobileViewItemBottomSheet(
view: viewBloc.state.view,
actions: _buildActions(state.view),
);
},
),
);
},
);
}
List<MobileViewItemBottomSheetBodyAction> _buildActions(ViewPB view) {
switch (type) {
case MobileViewCardType.recent:
return [
view.isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
if (view.layout != ViewLayoutPB.Chat)
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent,
];
case MobileViewCardType.favorite:
return [
MobileViewItemBottomSheetBodyAction.removeFromFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
];
}
}
} }
class _ViewCover extends StatelessWidget { class _ViewCover extends StatelessWidget {

View File

@ -0,0 +1,143 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.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/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileSpace extends StatefulWidget {
const MobileSpace({super.key});
@override
State<MobileSpace> createState() => _MobileSpaceState();
}
class _MobileSpaceState extends State<MobileSpace> {
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
if (state.spaces.isEmpty) {
return const SizedBox.shrink();
}
final currentSpace = state.currentSpace ?? state.spaces.first;
return Column(
children: [
MobileSpaceHeader(
isExpanded: state.isExpanded,
space: currentSpace,
onAdded: () {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: currentSpace.spacePermission ==
SpacePermission.publicToAll
? ViewSectionPB.Public
: ViewSectionPB.Private,
),
);
context.read<SpaceBloc>().add(
SpaceEvent.expand(currentSpace, true),
);
},
onPressed: () => _showSpaceMenu(context),
),
_Pages(
key: ValueKey(currentSpace.id),
space: currentSpace,
),
],
);
},
);
}
void _showSpaceMenu(BuildContext context) {
showMobileBottomSheet(
context,
showDivider: false,
showHeader: true,
showDragHandle: true,
showCloseButton: true,
showDoneButton: true,
title: LocaleKeys.space_title.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
return BlocProvider.value(
value: context.read<SpaceBloc>(),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: MobileSpaceMenu(),
),
);
},
);
}
}
class _Pages extends StatelessWidget {
const _Pages({
super.key,
required this.space,
});
final ViewPB space;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
ViewBloc(view: space)..add(const ViewEvent.initial()),
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final spaceType = space.spacePermission == SpacePermission.publicToAll
? FolderSpaceType.public
: FolderSpaceType.private;
return Column(
children: state.view.childViews
.map(
(view) => MobileViewItem(
key: ValueKey(
'${space.id} ${view.id}',
),
spaceType: spaceType,
isFirstChild: view.id == state.view.childViews.first.id,
view: view,
level: 0,
leftPadding: HomeSpaceViewSizes.leftPadding,
isFeedback: false,
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read<ViewBloc>().state.view;
return buildEndActionPane(
context,
[
MobilePaneActionType.more,
if (view.layout == ViewLayoutPB.Document)
MobilePaneActionType.add,
],
spaceType: spaceType,
needSpace: false,
);
},
),
)
.toList(),
);
},
),
);
}
}

View File

@ -0,0 +1,132 @@
import 'package:appflowy/generated/flowy_svgs.g.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';
import 'package:flutter/material.dart';
@visibleForTesting
const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
class MobileSpaceHeader extends StatelessWidget {
const MobileSpaceHeader({
super.key,
required this.space,
required this.onPressed,
required this.onAdded,
required this.isExpanded,
});
final ViewPB space;
final VoidCallback onPressed;
final VoidCallback onAdded;
final bool isExpanded;
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onPressed,
child: SizedBox(
height: 48,
child: Row(
children: [
SpaceIcon(
dimension: 24,
space: space,
cornerRadius: 6.0,
),
const HSpace(8),
FlowyText.medium(
space.name,
lineHeight: 1.15,
fontSize: 16.0,
),
const HSpace(4.0),
const FlowySvg(
FlowySvgs.workspace_drop_down_menu_show_s,
),
const Spacer(),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onAdded,
child: const FlowySvg(
FlowySvgs.m_space_add_s,
),
),
],
),
),
);
}
// Future<void> _onAction(SpaceMoreActionType type, dynamic data) async {
// switch (type) {
// case SpaceMoreActionType.rename:
// await _showRenameDialog();
// break;
// case SpaceMoreActionType.changeIcon:
// final (String icon, String iconColor) = data;
// context.read<SpaceBloc>().add(SpaceEvent.changeIcon(icon, iconColor));
// break;
// case SpaceMoreActionType.manage:
// _showManageSpaceDialog(context);
// break;
// case SpaceMoreActionType.addNewSpace:
// break;
// case SpaceMoreActionType.collapseAllPages:
// break;
// case SpaceMoreActionType.delete:
// _showDeleteSpaceDialog(context);
// break;
// case SpaceMoreActionType.divider:
// break;
// }
// }
// Future<void> _showRenameDialog() async {
// await NavigatorTextFieldDialog(
// title: LocaleKeys.space_rename.tr(),
// value: space.name,
// autoSelectAllText: true,
// onConfirm: (name, _) {
// context.read<SpaceBloc>().add(SpaceEvent.rename(space, name));
// },
// ).show(context);
// }
// void _showManageSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const ManageSpacePopup(),
// ),
// );
// },
// );
// }
// void _showDeleteSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const SizedBox(width: 440, child: DeleteSpacePopup()),
// ),
// );
// },
// );
// }
}

View File

@ -0,0 +1,128 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileSpaceMenu extends StatelessWidget {
const MobileSpaceMenu({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(4.0),
for (final space in state.spaces)
SizedBox(
height: 52,
child: _SidebarSpaceMenuItem(
space: space,
isSelected: state.currentSpace?.id == space.id,
),
),
// const Padding(
// padding: EdgeInsets.symmetric(vertical: 8.0),
// child: Divider(
// height: 0.5,
// ),
// ),
// const SizedBox(
// height: 52,
// child: _CreateSpaceButton(),
// ),
],
);
},
);
}
}
class _SidebarSpaceMenuItem extends StatelessWidget {
const _SidebarSpaceMenuItem({
required this.space,
required this.isSelected,
});
final ViewPB space;
final bool isSelected;
@override
Widget build(BuildContext context) {
return FlowyButton(
text: Row(
children: [
FlowyText.medium(
space.name,
),
const HSpace(6.0),
if (space.spacePermission == SpacePermission.private)
const FlowySvg(
FlowySvgs.space_lock_s,
size: Size.square(12),
),
],
),
iconPadding: 10,
leftIcon: SpaceIcon(
dimension: 24,
space: space,
cornerRadius: 6.0,
),
leftIconSize: const Size.square(20),
rightIcon: isSelected
? const FlowySvg(
FlowySvgs.workspace_selected_s,
blendMode: null,
)
: null,
onTap: () {
context.read<SpaceBloc>().add(SpaceEvent.open(space));
Navigator.of(context).pop();
},
);
}
}
// class _CreateSpaceButton extends StatelessWidget {
// const _CreateSpaceButton();
// @override
// Widget build(BuildContext context) {
// return FlowyButton(
// text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()),
// iconPadding: 10,
// leftIcon: const FlowySvg(
// FlowySvgs.space_add_s,
// ),
// leftIconSize: const Size.square(20),
// onTap: () {
// PopoverContainer.of(context).close();
// _showCreateSpaceDialog(context);
// },
// );
// }
// void _showCreateSpaceDialog(BuildContext context) {
// final spaceBloc = context.read<SpaceBloc>();
// showDialog(
// context: context,
// builder: (_) {
// return Dialog(
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12.0),
// ),
// child: BlocProvider.value(
// value: spaceBloc,
// child: const CreateSpacePopup(),
// ),
// );
// },
// );
// }
// }

View File

@ -296,30 +296,29 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
final icon = widget.view.icon.value.isNotEmpty final icon = widget.view.icon.value.isNotEmpty
? FlowyText.emoji( ? FlowyText.emoji(
widget.view.icon.value, widget.view.icon.value,
fontSize: 20.0, fontSize: 18.0,
) )
: Opacity( : Opacity(
opacity: 0.7, opacity: 0.7,
child: SizedBox.square(
dimension: 18.0,
child: widget.view.defaultIcon(), child: widget.view.defaultIcon(),
),
); );
return icon; return SizedBox(width: 18.0, child: icon);
} }
// > button or · button // > button or · button
// show > if the view is expandable. // show > if the view is expandable.
// show · if the view can't contain child views. // show · if the view can't contain child views.
Widget _buildLeftIcon() { Widget _buildLeftIcon() {
const rightPadding = 6.0;
if (context.read<ViewBloc>().state.view.childViews.isEmpty) { if (context.read<ViewBloc>().state.view.childViews.isEmpty) {
return HSpace(widget.leftPadding); return HSpace(widget.leftPadding + rightPadding);
} }
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 6.0, top: 6.0, bottom: 6.0), padding:
const EdgeInsets.only(right: rightPadding, top: 6.0, bottom: 6.0),
child: FlowySvg( child: FlowySvg(
widget.isExpanded ? FlowySvgs.m_expand_s : FlowySvgs.m_collapse_s, widget.isExpanded ? FlowySvgs.m_expand_s : FlowySvgs.m_collapse_s,
blendMode: null, blendMode: null,

View File

@ -0,0 +1,117 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/util/time.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
part 'time_cell_bloc.freezed.dart';
class TimeCellBloc extends Bloc<TimeCellEvent, TimeCellState> {
TimeCellBloc({
required this.cellController,
}) : super(TimeCellState.initial(cellController)) {
_dispatch();
_startListening();
}
final TimeCellController cellController;
void Function()? _onCellChangedFn;
@override
Future<void> close() async {
if (_onCellChangedFn != null) {
cellController.removeListener(
onCellChanged: _onCellChangedFn!,
onFieldChanged: _onFieldChangedListener,
);
}
await cellController.dispose();
return super.close();
}
void _dispatch() {
on<TimeCellEvent>(
(event, emit) async {
await event.when(
didReceiveCellUpdate: (content) {
emit(
state.copyWith(
content:
content != null ? formatTime(content.time.toInt()) : "",
),
);
},
didUpdateField: (fieldInfo) {
final wrap = fieldInfo.wrapCellContent;
if (wrap != null) {
emit(state.copyWith(wrap: wrap));
}
},
updateCell: (text) async {
text = parseTime(text)?.toString() ?? text;
if (state.content != text) {
emit(state.copyWith(content: text));
await cellController.saveCellData(text);
// If the input content is "abc" that can't parsered as number
// then the data stored in the backend will be an empty string.
// So for every cell data that will be formatted in the backend.
// It needs to get the formatted data after saving.
add(
TimeCellEvent.didReceiveCellUpdate(
cellController.getCellData(),
),
);
}
},
);
},
);
}
void _startListening() {
_onCellChangedFn = cellController.addListener(
onCellChanged: (cellContent) {
if (!isClosed) {
add(TimeCellEvent.didReceiveCellUpdate(cellContent));
}
},
onFieldChanged: _onFieldChangedListener,
);
}
void _onFieldChangedListener(FieldInfo fieldInfo) {
if (!isClosed) {
add(TimeCellEvent.didUpdateField(fieldInfo));
}
}
}
@freezed
class TimeCellEvent with _$TimeCellEvent {
const factory TimeCellEvent.didReceiveCellUpdate(TimeCellDataPB? cell) =
_DidReceiveCellUpdate;
const factory TimeCellEvent.didUpdateField(FieldInfo fieldInfo) =
_DidUpdateField;
const factory TimeCellEvent.updateCell(String text) = _UpdateCell;
}
@freezed
class TimeCellState with _$TimeCellState {
const factory TimeCellState({
required String content,
required bool wrap,
}) = _TimeCellState;
factory TimeCellState.initial(TimeCellController cellController) {
final wrap = cellController.fieldInfo.wrapCellContent;
final cellData = cellController.getCellData();
return TimeCellState(
content: cellData != null ? formatTime(cellData.time.toInt()) : "",
wrap: wrap ?? true,
);
}
}

View File

@ -16,6 +16,7 @@ typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
typedef URLCellController = CellController<URLCellDataPB, String>; typedef URLCellController = CellController<URLCellDataPB, String>;
typedef RelationCellController = CellController<RelationCellDataPB, String>; typedef RelationCellController = CellController<RelationCellDataPB, String>;
typedef SummaryCellController = CellController<String, String>; typedef SummaryCellController = CellController<String, String>;
typedef TimeCellController = CellController<TimeCellDataPB, String>;
typedef TranslateCellController = CellController<String, String>; typedef TranslateCellController = CellController<String, String>;
CellController makeCellController( CellController makeCellController(
@ -121,7 +122,6 @@ CellController makeCellController(
), ),
cellDataPersistence: TextCellDataPersistence(), cellDataPersistence: TextCellDataPersistence(),
); );
case FieldType.Relation: case FieldType.Relation:
return RelationCellController( return RelationCellController(
viewId: viewId, viewId: viewId,
@ -146,6 +146,18 @@ CellController makeCellController(
), ),
cellDataPersistence: TextCellDataPersistence(), cellDataPersistence: TextCellDataPersistence(),
); );
case FieldType.Time:
return TimeCellController(
viewId: viewId,
fieldController: fieldController,
cellContext: cellContext,
rowCache: rowCache,
cellDataLoader: CellDataLoader(
parser: TimeCellDataParser(),
reloadOnFieldChange: true,
),
cellDataPersistence: TextCellDataPersistence(),
);
case FieldType.Translate: case FieldType.Translate:
return TranslateCellController( return TranslateCellController(
viewId: viewId, viewId: viewId,

View File

@ -181,3 +181,18 @@ class RelationCellDataParser implements CellDataParser<RelationCellDataPB> {
} }
} }
} }
class TimeCellDataParser implements CellDataParser<TimeCellDataPB> {
@override
TimeCellDataPB? parserData(List<int> data) {
if (data.isEmpty) {
return null;
}
try {
return TimeCellDataPB.fromBuffer(data);
} catch (e) {
Log.error("Failed to parse timer data: $e");
return null;
}
}
}

View File

@ -64,6 +64,7 @@ class FieldInfo with _$FieldInfo {
case FieldType.SingleSelect: case FieldType.SingleSelect:
case FieldType.Checklist: case FieldType.Checklist:
case FieldType.URL: case FieldType.URL:
case FieldType.Time:
return true; return true;
default: default:
return false; return false;
@ -85,6 +86,7 @@ class FieldInfo with _$FieldInfo {
case FieldType.LastEditedTime: case FieldType.LastEditedTime:
case FieldType.CreatedTime: case FieldType.CreatedTime:
case FieldType.Checklist: case FieldType.Checklist:
case FieldType.Time:
return true; return true;
default: default:
return false; return false;

View File

@ -202,6 +202,30 @@ class FilterBackendService {
); );
} }
Future<FlowyResult<void, FlowyError>> insertTimeFilter({
required String fieldId,
String? filterId,
required NumberFilterConditionPB condition,
String content = "",
}) {
final filter = TimeFilterPB()
..condition = condition
..content = content;
return filterId == null
? insertFilter(
fieldId: fieldId,
fieldType: FieldType.Time,
data: filter.writeToBuffer(),
)
: updateFilter(
filterId: filterId,
fieldId: fieldId,
fieldType: FieldType.Time,
data: filter.writeToBuffer(),
);
}
Future<FlowyResult<void, FlowyError>> insertFilter({ Future<FlowyResult<void, FlowyError>> insertFilter({
required String fieldId, required String fieldId,
required FieldType fieldType, required FieldType fieldType,

View File

@ -127,6 +127,11 @@ class GridCreateFilterBloc
fieldId: fieldId, fieldId: fieldId,
condition: NumberFilterConditionPB.Equal, condition: NumberFilterConditionPB.Equal,
); );
case FieldType.Time:
return _filterBackendSvc.insertTimeFilter(
fieldId: fieldId,
condition: NumberFilterConditionPB.Equal,
);
case FieldType.RichText: case FieldType.RichText:
return _filterBackendSvc.insertTextFilter( return _filterBackendSvc.insertTextFilter(
fieldId: fieldId, fieldId: fieldId,

View File

@ -0,0 +1,111 @@
import 'dart:async';
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'time_filter_editor_bloc.freezed.dart';
class TimeFilterEditorBloc
extends Bloc<TimeFilterEditorEvent, TimeFilterEditorState> {
TimeFilterEditorBloc({required this.filterInfo})
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
_listener = FilterListener(
viewId: filterInfo.viewId,
filterId: filterInfo.filter.id,
),
super(TimeFilterEditorState.initial(filterInfo)) {
_dispatch();
_startListening();
}
final FilterInfo filterInfo;
final FilterBackendService _filterBackendSvc;
final FilterListener _listener;
void _dispatch() {
on<TimeFilterEditorEvent>(
(event, emit) async {
event.when(
didReceiveFilter: (filter) {
final filterInfo = state.filterInfo.copyWith(filter: filter);
emit(
state.copyWith(
filterInfo: filterInfo,
filter: filterInfo.timeFilter()!,
),
);
},
updateCondition: (NumberFilterConditionPB condition) {
_filterBackendSvc.insertTimeFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: condition,
content: state.filter.content,
);
},
updateContent: (content) {
_filterBackendSvc.insertTimeFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: state.filter.condition,
content: content,
);
},
delete: () {
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
);
},
);
},
);
}
void _startListening() {
_listener.start(
onUpdated: (filter) {
if (!isClosed) {
add(TimeFilterEditorEvent.didReceiveFilter(filter));
}
},
);
}
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
}
@freezed
class TimeFilterEditorEvent with _$TimeFilterEditorEvent {
const factory TimeFilterEditorEvent.didReceiveFilter(FilterPB filter) =
_DidReceiveFilter;
const factory TimeFilterEditorEvent.updateCondition(
NumberFilterConditionPB condition,
) = _UpdateCondition;
const factory TimeFilterEditorEvent.updateContent(String content) =
_UpdateContent;
const factory TimeFilterEditorEvent.delete() = _Delete;
}
@freezed
class TimeFilterEditorState with _$TimeFilterEditorState {
const factory TimeFilterEditorState({
required FilterInfo filterInfo,
required TimeFilterPB filter,
}) = _TimeFilterEditorState;
factory TimeFilterEditorState.initial(FilterInfo filterInfo) {
return TimeFilterEditorState(
filterInfo: filterInfo,
filter: filterInfo.timeFilter()!,
);
}
}

View File

@ -0,0 +1,227 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/time_filter_editor_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
class TimeFilterChoiceChip extends StatefulWidget {
const TimeFilterChoiceChip({
super.key,
required this.filterInfo,
});
final FilterInfo filterInfo;
@override
State<TimeFilterChoiceChip> createState() => _TimeFilterChoiceChipState();
}
class _TimeFilterChoiceChipState extends State<TimeFilterChoiceChip> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TimeFilterEditorBloc(
filterInfo: widget.filterInfo,
),
child: BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
builder: (context, state) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(200, 100)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (_) {
return BlocProvider.value(
value: context.read<TimeFilterEditorBloc>(),
child: const TimeFilterEditor(),
);
},
child: ChoiceChipButton(
filterInfo: state.filterInfo,
),
);
},
),
);
}
}
class TimeFilterEditor extends StatefulWidget {
const TimeFilterEditor({super.key});
@override
State<TimeFilterEditor> createState() => _TimeFilterEditorState();
}
class _TimeFilterEditorState extends State<TimeFilterEditor> {
final popoverMutex = PopoverMutex();
@override
Widget build(BuildContext context) {
return BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPanel(context, state),
if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty &&
state.filter.condition !=
NumberFilterConditionPB.NumberIsNotEmpty) ...[
const VSpace(4),
_buildFilterTimeField(context, state),
],
];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: IntrinsicHeight(child: Column(children: children)),
);
},
);
}
Widget _buildFilterPanel(
BuildContext context,
TimeFilterEditorState state,
) {
return SizedBox(
height: 20,
child: Row(
children: [
Expanded(
child: FlowyText(
state.filterInfo.fieldInfo.name,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(4),
Expanded(
child: TimeFilterConditionPBList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
context
.read<TimeFilterEditorBloc>()
.add(TimeFilterEditorEvent.updateCondition(condition));
},
),
),
const HSpace(4),
DisclosureButton(
popoverMutex: popoverMutex,
onAction: (action) {
switch (action) {
case FilterDisclosureAction.delete:
context
.read<TimeFilterEditorBloc>()
.add(const TimeFilterEditorEvent.delete());
break;
}
},
),
],
),
);
}
Widget _buildFilterTimeField(
BuildContext context,
TimeFilterEditorState state,
) {
return FlowyTextField(
text: state.filter.content,
hintText: LocaleKeys.grid_settings_typeAValue.tr(),
debounceDuration: const Duration(milliseconds: 300),
autoFocus: false,
onChanged: (text) {
context
.read<TimeFilterEditorBloc>()
.add(TimeFilterEditorEvent.updateContent(text));
},
);
}
}
class TimeFilterConditionPBList extends StatelessWidget {
const TimeFilterConditionPBList({
super.key,
required this.filterInfo,
required this.popoverMutex,
required this.onCondition,
});
final FilterInfo filterInfo;
final PopoverMutex popoverMutex;
final Function(NumberFilterConditionPB) onCondition;
@override
Widget build(BuildContext context) {
final timeFilter = filterInfo.timeFilter()!;
return PopoverActionList<ConditionWrapper>(
asBarrier: true,
mutex: popoverMutex,
direction: PopoverDirection.bottomWithCenterAligned,
actions: NumberFilterConditionPB.values
.map(
(action) => ConditionWrapper(
action,
timeFilter.condition == action,
),
)
.toList(),
buildChild: (controller) {
return ConditionButton(
conditionName: timeFilter.condition.filterName,
onTap: () => controller.show(),
);
},
onSelected: (action, controller) {
onCondition(action.inner);
controller.close();
},
);
}
}
class ConditionWrapper extends ActionCell {
ConditionWrapper(this.inner, this.isSelected);
final NumberFilterConditionPB inner;
final bool isSelected;
@override
Widget? rightIcon(Color iconColor) =>
isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
@override
String get name => inner.filterName;
}
extension TimeFilterConditionPBExtension on NumberFilterConditionPB {
String get filterName {
return switch (this) {
NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(),
NumberFilterConditionPB.NotEqual =>
LocaleKeys.grid_numberFilter_notEqual.tr(),
NumberFilterConditionPB.LessThan =>
LocaleKeys.grid_numberFilter_lessThan.tr(),
NumberFilterConditionPB.LessThanOrEqualTo =>
LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(),
NumberFilterConditionPB.GreaterThan =>
LocaleKeys.grid_numberFilter_greaterThan.tr(),
NumberFilterConditionPB.GreaterThanOrEqualTo =>
LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(),
NumberFilterConditionPB.NumberIsEmpty =>
LocaleKeys.grid_numberFilter_isEmpty.tr(),
NumberFilterConditionPB.NumberIsNotEmpty =>
LocaleKeys.grid_numberFilter_isNotEmpty.tr(),
_ => "",
};
}
}

View File

@ -60,4 +60,10 @@ class FilterInfo {
? NumberFilterPB.fromBuffer(filter.data.data) ? NumberFilterPB.fromBuffer(filter.data.data)
: null; : null;
} }
TimeFilterPB? timeFilter() {
return filter.data.fieldType == FieldType.Time
? TimeFilterPB.fromBuffer(filter.data.data)
: null;
}
} }

View File

@ -8,6 +8,7 @@ import 'choicechip/number.dart';
import 'choicechip/select_option/select_option.dart'; import 'choicechip/select_option/select_option.dart';
import 'choicechip/text.dart'; import 'choicechip/text.dart';
import 'choicechip/url.dart'; import 'choicechip/url.dart';
import 'choicechip/time.dart';
import 'filter_info.dart'; import 'filter_info.dart';
class FilterMenuItem extends StatelessWidget { class FilterMenuItem extends StatelessWidget {
@ -22,12 +23,15 @@ class FilterMenuItem extends StatelessWidget {
FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo), FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo),
FieldType.MultiSelect => FieldType.MultiSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo), SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo), FieldType.Number =>
NumberFilterChoiceChip(filterInfo: filterInfo),
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo), FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
FieldType.SingleSelect => FieldType.SingleSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo), SelectOptionFilterChoicechip(filterInfo: filterInfo),
FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo), FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo), FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
FieldType.Time =>
TimeFilterChoiceChip(filterInfo: filterInfo),
_ => const SizedBox(), _ => const SizedBox(),
}; };
} }

View File

@ -1,19 +1,21 @@
import 'package:flutter/widgets.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:flutter/widgets.dart';
import 'card_cell_skeleton/card_cell.dart'; import 'card_cell_skeleton/card_cell.dart';
import 'card_cell_skeleton/checkbox_card_cell.dart'; import 'card_cell_skeleton/checkbox_card_cell.dart';
import 'card_cell_skeleton/checklist_card_cell.dart'; import 'card_cell_skeleton/checklist_card_cell.dart';
import 'card_cell_skeleton/date_card_cell.dart'; import 'card_cell_skeleton/date_card_cell.dart';
import 'card_cell_skeleton/number_card_cell.dart'; import 'card_cell_skeleton/number_card_cell.dart';
import 'card_cell_skeleton/relation_card_cell.dart';
import 'card_cell_skeleton/select_option_card_cell.dart'; import 'card_cell_skeleton/select_option_card_cell.dart';
import 'card_cell_skeleton/summary_card_cell.dart'; import 'card_cell_skeleton/summary_card_cell.dart';
import 'card_cell_skeleton/text_card_cell.dart'; import 'card_cell_skeleton/text_card_cell.dart';
import 'card_cell_skeleton/time_card_cell.dart';
import 'card_cell_skeleton/timestamp_card_cell.dart';
import 'card_cell_skeleton/translate_card_cell.dart';
import 'card_cell_skeleton/url_card_cell.dart'; import 'card_cell_skeleton/url_card_cell.dart';
typedef CardCellStyleMap = Map<FieldType, CardCellStyle>; typedef CardCellStyleMap = Map<FieldType, CardCellStyle>;
@ -99,6 +101,12 @@ class CardCellBuilder {
databaseController: databaseController, databaseController: databaseController,
cellContext: cellContext, cellContext: cellContext,
), ),
FieldType.Time => TimeCardCell(
key: key,
style: isStyleOrNull(style),
databaseController: databaseController,
cellContext: cellContext,
),
FieldType.Translate => TranslateCardCell( FieldType.Translate => TranslateCardCell(
key: key, key: key,
style: isStyleOrNull(style), style: isStyleOrNull(style),

View File

@ -0,0 +1,62 @@
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
import 'card_cell.dart';
class TimeCardCellStyle extends CardCellStyle {
const TimeCardCellStyle({
required super.padding,
required this.textStyle,
});
final TextStyle textStyle;
}
class TimeCardCell extends CardCell<TimeCardCellStyle> {
const TimeCardCell({
super.key,
required super.style,
required this.databaseController,
required this.cellContext,
});
final DatabaseController databaseController;
final CellContext cellContext;
@override
State<TimeCardCell> createState() => _TimeCellState();
}
class _TimeCellState extends State<TimeCardCell> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) {
return TimeCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
},
child: BlocBuilder<TimeCellBloc, TimeCellState>(
buildWhen: (previous, current) => previous.content != current.content,
builder: (context, state) {
if (state.content.isEmpty) {
return const SizedBox.shrink();
}
return Container(
alignment: AlignmentDirectional.centerStart,
padding: widget.style.padding,
child: Text(state.content, style: widget.style.textStyle),
);
},
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import '../card_cell_builder.dart'; import '../card_cell_builder.dart';
import '../card_cell_skeleton/checkbox_card_cell.dart'; import '../card_cell_skeleton/checkbox_card_cell.dart';
@ -10,6 +11,7 @@ import '../card_cell_skeleton/number_card_cell.dart';
import '../card_cell_skeleton/relation_card_cell.dart'; import '../card_cell_skeleton/relation_card_cell.dart';
import '../card_cell_skeleton/select_option_card_cell.dart'; import '../card_cell_skeleton/select_option_card_cell.dart';
import '../card_cell_skeleton/text_card_cell.dart'; import '../card_cell_skeleton/text_card_cell.dart';
import '../card_cell_skeleton/time_card_cell.dart';
import '../card_cell_skeleton/timestamp_card_cell.dart'; import '../card_cell_skeleton/timestamp_card_cell.dart';
import '../card_cell_skeleton/url_card_cell.dart'; import '../card_cell_skeleton/url_card_cell.dart';
@ -84,6 +86,10 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) {
padding: padding, padding: padding,
textStyle: textStyle, textStyle: textStyle,
), ),
FieldType.Time: TimeCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle( FieldType.Translate: SummaryCardCellStyle(
padding: padding, padding: padding,
textStyle: textStyle, textStyle: textStyle,

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import '../card_cell_builder.dart'; import '../card_cell_builder.dart';
import '../card_cell_skeleton/checkbox_card_cell.dart'; import '../card_cell_skeleton/checkbox_card_cell.dart';
@ -10,6 +11,7 @@ import '../card_cell_skeleton/number_card_cell.dart';
import '../card_cell_skeleton/relation_card_cell.dart'; import '../card_cell_skeleton/relation_card_cell.dart';
import '../card_cell_skeleton/select_option_card_cell.dart'; import '../card_cell_skeleton/select_option_card_cell.dart';
import '../card_cell_skeleton/text_card_cell.dart'; import '../card_cell_skeleton/text_card_cell.dart';
import '../card_cell_skeleton/time_card_cell.dart';
import '../card_cell_skeleton/timestamp_card_cell.dart'; import '../card_cell_skeleton/timestamp_card_cell.dart';
import '../card_cell_skeleton/url_card_cell.dart'; import '../card_cell_skeleton/url_card_cell.dart';
@ -83,6 +85,10 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) {
padding: padding, padding: padding,
textStyle: textStyle, textStyle: textStyle,
), ),
FieldType.Time: TimeCardCellStyle(
padding: padding,
textStyle: textStyle,
),
FieldType.Translate: SummaryCardCellStyle( FieldType.Translate: SummaryCardCellStyle(
padding: padding, padding: padding,
textStyle: textStyle, textStyle: textStyle,

View File

@ -0,0 +1,37 @@
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../editable_cell_skeleton/time.dart';
class DesktopGridTimeCellSkin extends IEditableTimeCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimeCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
maxLines: context.watch<TimeCellBloc>().state.wrap ? null : 1,
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: GridSize.cellContentInsets,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
isDense: true,
),
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/time.dart';
class DesktopRowDetailTimeCellSkin extends IEditableTimeCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimeCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: () => focusNode.unfocus(),
onSubmitted: (_) => focusNode.unfocus(),
style: Theme.of(context).textTheme.bodyMedium,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9),
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).hintColor,
),
isDense: true,
),
);
}
}

View File

@ -1,9 +1,9 @@
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import '../row/accessory/cell_accessory.dart'; import '../row/accessory/cell_accessory.dart';
@ -18,6 +18,7 @@ import 'editable_cell_skeleton/relation.dart';
import 'editable_cell_skeleton/select_option.dart'; import 'editable_cell_skeleton/select_option.dart';
import 'editable_cell_skeleton/summary.dart'; import 'editable_cell_skeleton/summary.dart';
import 'editable_cell_skeleton/text.dart'; import 'editable_cell_skeleton/text.dart';
import 'editable_cell_skeleton/time.dart';
import 'editable_cell_skeleton/timestamp.dart'; import 'editable_cell_skeleton/timestamp.dart';
import 'editable_cell_skeleton/url.dart'; import 'editable_cell_skeleton/url.dart';
@ -121,6 +122,12 @@ class EditableCellBuilder {
skin: IEditableSummaryCellSkin.fromStyle(style), skin: IEditableSummaryCellSkin.fromStyle(style),
key: key, key: key,
), ),
FieldType.Time => EditableTimeCell(
databaseController: databaseController,
cellContext: cellContext,
skin: IEditableTimeCellSkin.fromStyle(style),
key: key,
),
FieldType.Translate => EditableTranslateCell( FieldType.Translate => EditableTranslateCell(
databaseController: databaseController, databaseController: databaseController,
cellContext: cellContext, cellContext: cellContext,
@ -213,6 +220,12 @@ class EditableCellBuilder {
skin: skinMap.relationSkin!, skin: skinMap.relationSkin!,
key: key, key: key,
), ),
FieldType.Time => EditableTimeCell(
databaseController: databaseController,
cellContext: cellContext,
skin: skinMap.timeSkin!,
key: key,
),
_ => throw UnimplementedError(), _ => throw UnimplementedError(),
}; };
} }
@ -368,6 +381,7 @@ class EditableCellSkinMap {
this.textSkin, this.textSkin,
this.urlSkin, this.urlSkin,
this.relationSkin, this.relationSkin,
this.timeSkin,
}); });
final IEditableCheckboxCellSkin? checkboxSkin; final IEditableCheckboxCellSkin? checkboxSkin;
@ -379,6 +393,7 @@ class EditableCellSkinMap {
final IEditableTextCellSkin? textSkin; final IEditableTextCellSkin? textSkin;
final IEditableURLCellSkin? urlSkin; final IEditableURLCellSkin? urlSkin;
final IEditableRelationCellSkin? relationSkin; final IEditableRelationCellSkin? relationSkin;
final IEditableTimeCellSkin? timeSkin;
bool has(FieldType fieldType) { bool has(FieldType fieldType) {
return switch (fieldType) { return switch (fieldType) {
@ -394,6 +409,7 @@ class EditableCellSkinMap {
FieldType.Number => numberSkin != null, FieldType.Number => numberSkin != null,
FieldType.RichText => textSkin != null, FieldType.RichText => textSkin != null,
FieldType.URL => urlSkin != null, FieldType.URL => urlSkin != null,
FieldType.Time => timeSkin != null,
_ => throw UnimplementedError(), _ => throw UnimplementedError(),
}; };
} }

View File

@ -0,0 +1,120 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../desktop_grid/desktop_grid_time_cell.dart';
import '../desktop_row_detail/desktop_row_detail_time_cell.dart';
import '../mobile_grid/mobile_grid_time_cell.dart';
import '../mobile_row_detail/mobile_row_detail_time_cell.dart';
abstract class IEditableTimeCellSkin {
const IEditableTimeCellSkin();
factory IEditableTimeCellSkin.fromStyle(EditableCellStyle style) {
return switch (style) {
EditableCellStyle.desktopGrid => DesktopGridTimeCellSkin(),
EditableCellStyle.desktopRowDetail => DesktopRowDetailTimeCellSkin(),
EditableCellStyle.mobileGrid => MobileGridTimeCellSkin(),
EditableCellStyle.mobileRowDetail => MobileRowDetailTimeCellSkin(),
};
}
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimeCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
);
}
class EditableTimeCell extends EditableCellWidget {
EditableTimeCell({
super.key,
required this.databaseController,
required this.cellContext,
required this.skin,
});
final DatabaseController databaseController;
final CellContext cellContext;
final IEditableTimeCellSkin skin;
@override
GridEditableTextCell<EditableTimeCell> createState() => _TimeCellState();
}
class _TimeCellState extends GridEditableTextCell<EditableTimeCell> {
late final TextEditingController _textEditingController;
late final cellBloc = TimeCellBloc(
cellController: makeCellController(
widget.databaseController,
widget.cellContext,
).as(),
);
@override
void initState() {
super.initState();
_textEditingController =
TextEditingController(text: cellBloc.state.content);
}
@override
void dispose() {
_textEditingController.dispose();
cellBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cellBloc,
child: BlocListener<TimeCellBloc, TimeCellState>(
listener: (context, state) =>
_textEditingController.text = state.content,
child: Builder(
builder: (context) {
return widget.skin.build(
context,
widget.cellContainerNotifier,
cellBloc,
focusNode,
_textEditingController,
);
},
),
),
);
}
@override
SingleListenerFocusNode focusNode = SingleListenerFocusNode();
@override
void onRequestFocus() {
focusNode.requestFocus();
}
@override
String? onCopy() => cellBloc.state.content;
@override
Future<void> focusChanged() async {
if (mounted &&
!cellBloc.isClosed &&
cellBloc.state.content != _textEditingController.text.trim()) {
cellBloc
.add(TimeCellEvent.updateCell(_textEditingController.text.trim()));
}
return super.focusChanged();
}
}

View File

@ -0,0 +1,29 @@
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/time.dart';
class MobileGridTimeCellSkin extends IEditableTimeCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimeCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15),
decoration: const InputDecoration(
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12),
isCollapsed: true,
),
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import '../editable_cell_skeleton/time.dart';
class MobileRowDetailTimeCellSkin extends IEditableTimeCellSkin {
@override
Widget build(
BuildContext context,
CellContainerNotifier cellContainerNotifier,
TimeCellBloc bloc,
FocusNode focusNode,
TextEditingController textEditingController,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16),
decoration: InputDecoration(
enabledBorder:
_getInputBorder(color: Theme.of(context).colorScheme.outline),
focusedBorder:
_getInputBorder(color: Theme.of(context).colorScheme.primary),
hintText: LocaleKeys.grid_row_textPlaceholder.tr(),
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 13),
isCollapsed: true,
isDense: true,
constraints: const BoxConstraints(),
),
// close keyboard when tapping outside of the text field
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
);
}
InputBorder _getInputBorder({Color? color}) {
return OutlineInputBorder(
borderSide: BorderSide(color: color!),
borderRadius: const BorderRadius.all(Radius.circular(14)),
gapPadding: 0,
);
}
}

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
typedef SelectFieldCallback = void Function(FieldType); typedef SelectFieldCallback = void Function(FieldType);
@ -21,6 +22,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.CreatedTime, FieldType.CreatedTime,
FieldType.Relation, FieldType.Relation,
FieldType.Summary, FieldType.Summary,
FieldType.Time,
FieldType.Translate, FieldType.Translate,
]; ];

View File

@ -1,10 +1,10 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/tag.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/tag.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'checkbox.dart'; import 'checkbox.dart';
import 'checklist.dart'; import 'checklist.dart';
@ -15,8 +15,10 @@ import 'relation.dart';
import 'rich_text.dart'; import 'rich_text.dart';
import 'single_select.dart'; import 'single_select.dart';
import 'summary.dart'; import 'summary.dart';
import 'time.dart';
import 'timestamp.dart'; import 'timestamp.dart';
import 'url.dart'; import 'url.dart';
import 'tag.dart';
typedef TypeOptionDataCallback = void Function(Uint8List typeOptionData); typedef TypeOptionDataCallback = void Function(Uint8List typeOptionData);
@ -35,6 +37,7 @@ abstract class TypeOptionEditorFactory {
FieldType.Checklist => const ChecklistTypeOptionEditorFactory(), FieldType.Checklist => const ChecklistTypeOptionEditorFactory(),
FieldType.Relation => const RelationTypeOptionEditorFactory(), FieldType.Relation => const RelationTypeOptionEditorFactory(),
FieldType.Summary => const SummaryTypeOptionEditorFactory(), FieldType.Summary => const SummaryTypeOptionEditorFactory(),
FieldType.Time => const TimeTypeOptionEditorFactory(),
FieldType.Translate => const TranslateTypeOptionEditorFactory(), FieldType.Translate => const TranslateTypeOptionEditorFactory(),
FieldType.Tag => const TagTypeOptionEditorFactory(), FieldType.Tag => const TagTypeOptionEditorFactory(),
_ => throw UnimplementedError(), _ => throw UnimplementedError(),

View File

@ -0,0 +1,19 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
import 'builder.dart';
class TimeTypeOptionEditorFactory implements TypeOptionEditorFactory {
const TimeTypeOptionEditorFactory();
@override
Widget? build({
required BuildContext context,
required String viewId,
required FieldPB field,
required PopoverMutex popoverMutex,
required TypeOptionDataCallback onTypeOptionUpdated,
}) =>
null;
}

View File

@ -22,6 +22,7 @@ extension FieldTypeExtension on FieldType {
FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(), FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(),
FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(), FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(),
FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(), FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(),
FieldType.Time => LocaleKeys.grid_field_timeFieldName.tr(),
FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(), FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(),
FieldType.Tag => LocaleKeys.grid_field_tagFieldName.tr(), FieldType.Tag => LocaleKeys.grid_field_tagFieldName.tr(),
_ => throw UnimplementedError(), _ => throw UnimplementedError(),
@ -40,6 +41,7 @@ extension FieldTypeExtension on FieldType {
FieldType.CreatedTime => FlowySvgs.created_at_s, FieldType.CreatedTime => FlowySvgs.created_at_s,
FieldType.Relation => FlowySvgs.relation_s, FieldType.Relation => FlowySvgs.relation_s,
FieldType.Summary => FlowySvgs.ai_summary_s, FieldType.Summary => FlowySvgs.ai_summary_s,
FieldType.Time => FlowySvgs.timer_start_s,
FieldType.Translate => FlowySvgs.ai_translate_s, FieldType.Translate => FlowySvgs.ai_translate_s,
FieldType.Tag => FlowySvgs.ai_tag_s, FieldType.Tag => FlowySvgs.ai_tag_s,
_ => throw UnimplementedError(), _ => throw UnimplementedError(),
@ -65,6 +67,7 @@ extension FieldTypeExtension on FieldType {
FieldType.Checklist => const Color(0xFF98F4CD), FieldType.Checklist => const Color(0xFF98F4CD),
FieldType.Relation => const Color(0xFFFDEDA7), FieldType.Relation => const Color(0xFFFDEDA7),
FieldType.Summary => const Color(0xFFBECCFF), FieldType.Summary => const Color(0xFFBECCFF),
FieldType.Time => const Color(0xFFFDEDA7),
FieldType.Translate => const Color(0xFFBECCFF), FieldType.Translate => const Color(0xFFBECCFF),
FieldType.Tag => const Color(0xFFBECCFF), FieldType.Tag => const Color(0xFFBECCFF),
_ => throw UnimplementedError(), _ => throw UnimplementedError(),
@ -84,6 +87,7 @@ extension FieldTypeExtension on FieldType {
FieldType.Checklist => const Color(0xFF42AD93), FieldType.Checklist => const Color(0xFF42AD93),
FieldType.Relation => const Color(0xFFFDEDA7), FieldType.Relation => const Color(0xFFFDEDA7),
FieldType.Summary => const Color(0xFF6859A7), FieldType.Summary => const Color(0xFF6859A7),
FieldType.Time => const Color(0xFFFDEDA7),
FieldType.Translate => const Color(0xFF6859A7), FieldType.Translate => const Color(0xFF6859A7),
FieldType.Tag => const Color(0xFF6859A7), FieldType.Tag => const Color(0xFF6859A7),
_ => throw UnimplementedError(), _ => throw UnimplementedError(),

View File

@ -0,0 +1,43 @@
final RegExp timerRegExp =
RegExp(r'(?:(?<hours>\d*)h)? ?(?:(?<minutes>\d*)m)?');
int? parseTime(String timerStr) {
int? res = int.tryParse(timerStr);
if (res != null) {
return res;
}
final matches = timerRegExp.firstMatch(timerStr);
if (matches == null) {
return null;
}
final hours = int.tryParse(matches.namedGroup('hours') ?? "");
final minutes = int.tryParse(matches.namedGroup('minutes') ?? "");
if (hours == null && minutes == null) {
return null;
}
final expected =
"${hours != null ? '${hours}h' : ''}${hours != null && minutes != null ? ' ' : ''}${minutes != null ? '${minutes}m' : ''}";
if (timerStr != expected) {
return null;
}
res = 0;
res += hours != null ? hours * 60 : res;
res += minutes ?? 0;
return res;
}
String formatTime(int minutes) {
if (minutes >= 60) {
if (minutes % 60 == 0) {
return "${minutes ~/ 60}h";
}
return "${minutes ~/ 60}h ${minutes % 60}m";
} else if (minutes >= 0) {
return "${minutes}m";
}
return "";
}

View File

@ -0,0 +1,521 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
part 'space_bloc.freezed.dart';
enum SpacePermission {
publicToAll,
private,
}
class SidebarSection {
const SidebarSection({
required this.publicViews,
required this.privateViews,
});
const SidebarSection.empty()
: publicViews = const [],
privateViews = const [];
final List<ViewPB> publicViews;
final List<ViewPB> privateViews;
List<ViewPB> get views => publicViews + privateViews;
SidebarSection copyWith({
List<ViewPB>? publicViews,
List<ViewPB>? privateViews,
}) {
return SidebarSection(
publicViews: publicViews ?? this.publicViews,
privateViews: privateViews ?? this.privateViews,
);
}
}
/// The [SpaceBloc] is responsible for
/// managing the root views in different sections of the workspace.
class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
SpaceBloc() : super(SpaceState.initial()) {
on<SpaceEvent>(
(event, emit) async {
await event.when(
initial: (userProfile, workspaceId) async {
_initial(userProfile, workspaceId);
final (spaces, publicViews, privateViews) = await _getSpaces();
final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog(
spaces: spaces,
publicViews: publicViews,
privateViews: privateViews,
);
final currentSpace = await _getLastOpenedSpace(spaces);
final isExpanded = await _getSpaceExpandStatus(currentSpace);
emit(
state.copyWith(
spaces: spaces,
currentSpace: currentSpace,
isExpanded: isExpanded,
shouldShowUpgradeDialog: shouldShowUpgradeDialog,
),
);
},
create: (name, icon, iconColor, permission) async {
final space = await _createSpace(
name: name,
icon: icon,
iconColor: iconColor,
permission: permission,
);
if (space != null) {
emit(state.copyWith(spaces: [...state.spaces, space]));
add(SpaceEvent.open(space));
}
},
delete: (space) async {
if (state.spaces.length <= 1) {
return;
}
final deletedSpace = space ?? state.currentSpace;
if (deletedSpace == null) {
return;
}
await ViewBackendService.delete(viewId: deletedSpace.id);
},
rename: (space, name) async {
add(SpaceEvent.update(name: name));
},
changeIcon: (icon, iconColor) async {
add(SpaceEvent.update(icon: icon, iconColor: iconColor));
},
update: (name, icon, iconColor, permission) async {
final space = state.currentSpace;
if (space == null) {
return;
}
if (name != null) {
await _rename(space, name);
}
if (icon != null || iconColor != null || permission != null) {
try {
final extra = space.extra;
final current = extra.isNotEmpty == true
? jsonDecode(extra)
: <String, dynamic>{};
final updated = <String, dynamic>{};
if (icon != null) {
updated[ViewExtKeys.spaceIconKey] = icon;
}
if (iconColor != null) {
updated[ViewExtKeys.spaceIconColorKey] = iconColor;
}
if (permission != null) {
updated[ViewExtKeys.spacePermissionKey] = permission.index;
}
final merged = mergeMaps(current, updated);
await ViewBackendService.updateView(
viewId: space.id,
extra: jsonEncode(merged),
);
} catch (e) {
Log.error('Failed to migrating cover: $e');
}
}
if (permission != null) {
await ViewBackendService.updateViewsVisibility(
[space],
permission == SpacePermission.publicToAll,
);
}
},
open: (space) async {
await _openSpace(space);
final isExpanded = await _getSpaceExpandStatus(space);
emit(state.copyWith(currentSpace: space, isExpanded: isExpanded));
},
expand: (space, isExpanded) async {
await _setSpaceExpandStatus(space, isExpanded);
emit(state.copyWith(isExpanded: isExpanded));
},
createPage: (name, section, index) async {
final parentViewId = state.currentSpace?.id;
if (parentViewId == null) {
return;
}
final result = await ViewBackendService.createView(
name: name,
layoutType: ViewLayoutPB.Document,
parentViewId: parentViewId,
index: index,
);
result.fold(
(view) {
emit(
state.copyWith(
lastCreatedPage: view,
createPageResult: FlowyResult.success(null),
),
);
},
(error) {
Log.error('Failed to create root view: $error');
emit(
state.copyWith(
createPageResult: FlowyResult.failure(error),
),
);
},
);
},
didReceiveSpaceUpdate: () async {
final (spaces, _, _) = await _getSpaces();
final currentSpace = await _getLastOpenedSpace(spaces);
emit(
state.copyWith(
spaces: spaces,
currentSpace: currentSpace,
),
);
},
reset: (userProfile, workspaceId) async {
_reset(userProfile, workspaceId);
add(SpaceEvent.initial(userProfile, workspaceId));
},
migrate: () async {
final result = await migrate();
emit(state.copyWith(shouldShowUpgradeDialog: !result));
},
);
},
);
}
late WorkspaceService _workspaceService;
String? _workspaceId;
WorkspaceSectionsListener? _listener;
@override
Future<void> close() async {
await _listener?.stop();
_listener = null;
return super.close();
}
Future<(List<ViewPB>, List<ViewPB>, List<ViewPB>)> _getSpaces() async {
final sectionViews = await _getSectionViews();
if (sectionViews == null || sectionViews.views.isEmpty) {
return (<ViewPB>[], <ViewPB>[], <ViewPB>[]);
}
final publicViews = sectionViews.publicViews;
final privateViews = sectionViews.privateViews;
final publicSpaces = publicViews.where((e) => e.isSpace);
final privateSpaces = privateViews.where((e) => e.isSpace);
return ([...publicSpaces, ...privateSpaces], publicViews, privateViews);
}
Future<ViewPB?> _createSpace({
required String name,
required String icon,
required String iconColor,
required SpacePermission permission,
}) async {
final section = switch (permission) {
SpacePermission.publicToAll => ViewSectionPB.Public,
SpacePermission.private => ViewSectionPB.Private,
};
final result = await _workspaceService.createView(
name: name,
viewSection: section,
setAsCurrent: false,
);
return await result.fold((space) async {
Log.info('Space created: $space');
final extra = {
ViewExtKeys.isSpaceKey: true,
ViewExtKeys.spaceIconKey: icon,
ViewExtKeys.spaceIconColorKey: iconColor,
ViewExtKeys.spacePermissionKey: permission.index,
ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch,
};
await ViewBackendService.updateView(
viewId: space.id,
extra: jsonEncode(extra),
);
return space;
}, (error) {
Log.error('Failed to create space: $error');
return null;
});
}
Future<ViewPB> _rename(ViewPB space, String name) async {
final result =
await ViewBackendService.updateView(viewId: space.id, name: name);
return result.fold((_) {
space.freeze();
return space.rebuild((b) => b.name = name);
}, (error) {
Log.error('Failed to rename space: $error');
return space;
});
}
Future<SidebarSection?> _getSectionViews() async {
try {
final publicViews = await _workspaceService.getPublicViews().getOrThrow();
final privateViews =
await _workspaceService.getPrivateViews().getOrThrow();
return SidebarSection(
publicViews: publicViews,
privateViews: privateViews,
);
} catch (e) {
Log.error('Failed to get section views: $e');
return null;
}
}
void _initial(UserProfilePB userProfile, String workspaceId) {
_workspaceService = WorkspaceService(workspaceId: workspaceId);
_workspaceId = workspaceId;
_listener = WorkspaceSectionsListener(
user: userProfile,
workspaceId: workspaceId,
)..start(
sectionChanged: (result) async {
add(const SpaceEvent.didReceiveSpaceUpdate());
},
);
}
void _reset(UserProfilePB userProfile, String workspaceId) {
_listener?.stop();
_listener = null;
_initial(userProfile, workspaceId);
}
Future<ViewPB?> _getLastOpenedSpace(List<ViewPB> spaces) async {
if (spaces.isEmpty) {
return null;
}
final spaceId =
await getIt<KeyValueStorage>().get(KVKeys.lastOpenedSpaceId);
if (spaceId == null) {
return spaces.first;
}
final space =
spaces.firstWhereOrNull((e) => e.id == spaceId) ?? spaces.first;
return space;
}
Future<void> _openSpace(ViewPB space) async {
await getIt<KeyValueStorage>().set(KVKeys.lastOpenedSpaceId, space.id);
}
Future<void> _setSpaceExpandStatus(ViewPB? space, bool isExpanded) async {
if (space == null) {
return;
}
final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews);
var map = {};
if (result != null) {
map = jsonDecode(result);
}
if (isExpanded) {
// set expand status to true if it's not expanded
map[space.id] = true;
} else {
// remove the expand status if it's expanded
map.remove(space.id);
}
await getIt<KeyValueStorage>().set(KVKeys.expandedViews, jsonEncode(map));
}
Future<bool> _getSpaceExpandStatus(ViewPB? space) async {
if (space == null) {
return false;
}
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
if (result == null) {
return true;
}
final map = jsonDecode(result);
return map[space.id] ?? true;
});
}
Future<bool> migrate() async {
if (_workspaceId == null) {
return false;
}
try {
final user =
await UserBackendService.getCurrentUserProfile().getOrThrow();
final service = UserBackendService(userId: user.id);
final members =
await service.getWorkspaceMembers(_workspaceId!).getOrThrow();
final isOwner = members.items
.any((e) => e.role == AFRolePB.Owner && e.email == user.email);
// only one member in the workspace, migrate it immediately
// only the owner can migrate the public space
if (members.items.length == 1 || isOwner) {
// create a new public space and a new private space
// move all the views in the workspace to the new public/private space
final publicViews =
await _workspaceService.getPublicViews().getOrThrow();
final publicSpace = await _createSpace(
name: 'shared',
icon: builtInSpaceIcons.first,
iconColor: builtInSpaceColors.first,
permission: SpacePermission.publicToAll,
);
if (publicSpace != null) {
for (final view in publicViews.reversed) {
if (view.isSpace) {
continue;
}
await ViewBackendService.moveViewV2(
viewId: view.id,
newParentId: publicSpace.id,
prevViewId: view.parentViewId,
);
}
}
}
// create a new private space
final privateViews =
await _workspaceService.getPrivateViews().getOrThrow();
final privateSpace = await _createSpace(
name: 'private',
icon: builtInSpaceIcons.last,
iconColor: builtInSpaceColors.last,
permission: SpacePermission.private,
);
if (privateSpace != null) {
for (final view in privateViews.reversed) {
if (view.isSpace) {
continue;
}
await ViewBackendService.moveViewV2(
viewId: view.id,
newParentId: privateSpace.id,
prevViewId: view.parentViewId,
);
}
}
return true;
} catch (e) {
Log.error('migrate space error: $e');
return false;
}
}
Future<bool> shouldShowUpgradeDialog({
required List<ViewPB> spaces,
required List<ViewPB> publicViews,
required List<ViewPB> privateViews,
}) async {
final publicSpaces =
spaces.where((e) => e.spacePermission == SpacePermission.publicToAll);
if (publicSpaces.isEmpty && publicViews.isNotEmpty) {
return true;
}
final privateSpaces =
spaces.where((e) => e.spacePermission == SpacePermission.private);
if (privateSpaces.isEmpty && privateViews.isNotEmpty) {
return true;
}
return false;
}
}
@freezed
class SpaceEvent with _$SpaceEvent {
const factory SpaceEvent.initial(
UserProfilePB userProfile,
String workspaceId,
) = _Initial;
const factory SpaceEvent.create({
required String name,
required String icon,
required String iconColor,
required SpacePermission permission,
}) = _Create;
const factory SpaceEvent.rename(ViewPB space, String name) = _Rename;
const factory SpaceEvent.changeIcon(String icon, String iconColor) =
_ChangeIcon;
const factory SpaceEvent.update({
String? name,
String? icon,
String? iconColor,
SpacePermission? permission,
}) = _Update;
const factory SpaceEvent.open(ViewPB space) = _Open;
const factory SpaceEvent.expand(ViewPB space, bool isExpanded) = _Expand;
const factory SpaceEvent.createPage({
required String name,
required ViewSectionPB viewSection,
int? index,
}) = _CreatePage;
const factory SpaceEvent.delete(ViewPB? space) = _Delete;
const factory SpaceEvent.didReceiveSpaceUpdate() = _DidReceiveSpaceUpdate;
const factory SpaceEvent.reset(
UserProfilePB userProfile,
String workspaceId,
) = _Reset;
const factory SpaceEvent.migrate() = _Migrate;
}
@freezed
class SpaceState with _$SpaceState {
const factory SpaceState({
// use root view with space attributes to represent the space
@Default([]) List<ViewPB> spaces,
@Default(null) ViewPB? currentSpace,
@Default(true) bool isExpanded,
@Default(null) ViewPB? lastCreatedPage,
FlowyResult<void, FlowyError>? createPageResult,
@Default(false) bool shouldShowUpgradeDialog,
}) = _SpaceState;
factory SpaceState.initial() => const SpaceState();
}

View File

@ -10,6 +10,7 @@ import 'package:appflowy/plugins/database/grid/presentation/mobile_grid_page.dar
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/plugins/document/document.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -36,6 +37,14 @@ class ViewExtKeys {
// is pinned // is pinned
static String isPinnedKey = 'is_pinned'; static String isPinnedKey = 'is_pinned';
// space
static String isSpaceKey = 'is_space';
static String spaceCreatorKey = 'space_creator';
static String spaceCreatedAtKey = 'space_created_at';
static String spaceIconKey = 'space_icon';
static String spaceIconColorKey = 'space_icon_color';
static String spacePermissionKey = 'space_permission';
} }
extension ViewExtension on ViewPB { extension ViewExtension on ViewPB {
@ -104,6 +113,64 @@ extension ViewExtension on ViewPB {
FlowySvgData get iconData => layout.icon; FlowySvgData get iconData => layout.icon;
bool get isSpace {
try {
final ext = jsonDecode(extra);
final isSpace = ext[ViewExtKeys.isSpaceKey] ?? false;
return isSpace;
} catch (e) {
return false;
}
}
SpacePermission get spacePermission {
try {
final ext = jsonDecode(extra);
final permission = ext[ViewExtKeys.spacePermissionKey] ?? 1;
return SpacePermission.values[permission];
} catch (e) {
return SpacePermission.private;
}
}
FlowySvg get spaceIconSvg {
try {
final ext = jsonDecode(extra);
final icon = ext[ViewExtKeys.spaceIconKey];
final color = ext[ViewExtKeys.spaceIconColorKey];
if (icon == null || color == null) {
return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null);
}
return FlowySvg(
FlowySvgData('assets/flowy_icons/16x/$icon.svg'),
color: Color(int.parse(color)),
blendMode: BlendMode.srcOut,
);
} catch (e) {
return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null);
}
}
String? get spaceIcon {
try {
final ext = jsonDecode(extra);
final icon = ext[ViewExtKeys.spaceIconKey];
return icon;
} catch (e) {
return null;
}
}
String? get spaceIconColor {
try {
final ext = jsonDecode(extra);
final color = ext[ViewExtKeys.spaceIconColorKey];
return color;
} catch (e) {
return null;
}
}
bool get isPinned { bool get isPinned {
try { try {
final ext = jsonDecode(extra); final ext = jsonDecode(extra);

View File

@ -17,6 +17,7 @@ class WorkspaceService {
String? desc, String? desc,
int? index, int? index,
ViewLayoutPB? layout, ViewLayoutPB? layout,
bool? setAsCurrent,
}) { }) {
final payload = CreateViewPayloadPB.create() final payload = CreateViewPayloadPB.create()
..parentViewId = workspaceId ..parentViewId = workspaceId
@ -32,6 +33,10 @@ class WorkspaceService {
payload.index = index; payload.index = index;
} }
if (setAsCurrent != null) {
payload.setAsCurrent = setAsCurrent;
}
return FolderEventCreateView(payload).send(); return FolderEventCreateView(payload).send();
} }

View File

@ -9,8 +9,7 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -95,11 +94,12 @@ class SidebarTopMenu extends StatelessWidget {
onPointerDown: (_) => context onPointerDown: (_) => context
.read<HomeSettingBloc>() .read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu()), .add(const HomeSettingEvent.collapseMenu()),
child: FlowyIconButton( child: FlowyHover(
child: Container(
width: 24, width: 24,
onPressed: () {}, padding: const EdgeInsets.all(4),
iconPadding: const EdgeInsets.all(4), child: const FlowySvg(FlowySvgs.hide_menu_s),
icon: const FlowySvg(FlowySvgs.hide_menu_s), ),
), ),
), ),
), ),

View File

@ -1,7 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/shared/feature_flags.dart';
@ -13,6 +11,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/favorite/prelude.dart'; import 'package:appflowy/workspace/application/favorite/prelude.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
@ -23,6 +22,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar
import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
@ -32,6 +32,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
/// Home Sidebar is the left side bar of the home page. /// Home Sidebar is the left side bar of the home page.
@ -107,6 +108,16 @@ class HomeSideBar extends StatelessWidget {
), ),
), ),
), ),
BlocProvider(
create: (_) => SpaceBloc()
..add(
SpaceEvent.initial(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
),
),
], ],
child: MultiBlocListener( child: MultiBlocListener(
listeners: [ listeners: [
@ -119,6 +130,15 @@ class HomeSideBar extends StatelessWidget {
), ),
), ),
), ),
BlocListener<SpaceBloc, SpaceState>(
listenWhen: (p, c) =>
p.lastCreatedPage?.id != c.lastCreatedPage?.id,
listener: (context, state) => context.read<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: state.lastCreatedPage!.plugin(),
),
),
),
BlocListener<ActionNavigationBloc, ActionNavigationState>( BlocListener<ActionNavigationBloc, ActionNavigationState>(
listenWhen: (_, curr) => curr.action != null, listenWhen: (_, curr) => curr.action != null,
listener: _onNotificationAction, listener: _onNotificationAction,
@ -140,6 +160,13 @@ class HomeSideBar extends StatelessWidget {
context context
.read<FavoriteBloc>() .read<FavoriteBloc>()
.add(const FavoriteEvent.fetchFavorites()); .add(const FavoriteEvent.fetchFavorites());
context.read<SpaceBloc>().add(
SpaceEvent.reset(
userProfile,
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
),
);
} }
}, },
), ),
@ -274,20 +301,10 @@ class _SidebarState extends State<_Sidebar> {
), ),
), ),
), ),
Expanded(
child: Padding( _renderFolderOrSpace(menuHorizontalInset),
padding: menuHorizontalInset - const EdgeInsets.only(right: 6),
child: SingleChildScrollView( _renderUpgradeSpaceButton(menuHorizontalInset),
padding: const EdgeInsets.only(right: 6),
controller: _scrollController,
physics: const ClampingScrollPhysics(),
child: SidebarFolder(
userProfile: widget.userProfile,
isHoverEnabled: !_isScrolling,
),
),
),
),
// trash // trash
Padding( Padding(
@ -308,6 +325,66 @@ class _SidebarState extends State<_Sidebar> {
); );
} }
Widget _renderFolderOrSpace(EdgeInsets menuHorizontalInset) {
// there's no space or the workspace is not collaborative,
// show the folder section (Workspace, Private, Personal)
// otherwise, show the space
return context.watch<SpaceBloc>().state.spaces.isEmpty ||
!context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn
? Expanded(
child: Padding(
padding: menuHorizontalInset - const EdgeInsets.only(right: 6),
child: SingleChildScrollView(
padding: const EdgeInsets.only(right: 6),
controller: _scrollController,
physics: const ClampingScrollPhysics(),
child: SidebarFolder(
userProfile: widget.userProfile,
isHoverEnabled: !_isScrolling,
),
),
),
)
: Expanded(
child: Padding(
padding: menuHorizontalInset - const EdgeInsets.only(right: 6),
child: SingleChildScrollView(
padding: const EdgeInsets.only(right: 6),
controller: _scrollController,
physics: const ClampingScrollPhysics(),
child: SidebarSpace(
userProfile: widget.userProfile,
isHoverEnabled: !_isScrolling,
),
),
),
);
}
Widget _renderUpgradeSpaceButton(EdgeInsets menuHorizontalInset) {
return !context.watch<SpaceBloc>().state.shouldShowUpgradeDialog
? const SizedBox.shrink()
: Container(
height: 40,
padding: menuHorizontalInset,
child: FlowyButton(
onTap: () {
context.read<SpaceBloc>().add(const SpaceEvent.migrate());
},
leftIcon: const Icon(
Icons.upgrade_rounded,
color: Colors.red,
),
leftIconSize: const Size.square(20),
iconPadding: 12.0,
text: FlowyText.regular(
LocaleKeys.space_enableSpacesForYourWorkspace.tr(),
overflow: TextOverflow.ellipsis,
),
),
);
}
void _onScrollChanged() { void _onScrollChanged() {
setState(() => _isScrolling = true); setState(() => _isScrolling = true);

View File

@ -0,0 +1,108 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CreateSpacePopup extends StatefulWidget {
const CreateSpacePopup({super.key});
@override
State<CreateSpacePopup> createState() => _CreateSpacePopupState();
}
class _CreateSpacePopupState extends State<CreateSpacePopup> {
String spaceName = '';
String spaceIcon = '';
String spaceIconColor = '';
SpacePermission spacePermission = SpacePermission.publicToAll;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
width: 500,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
LocaleKeys.space_createNewSpace.tr(),
fontSize: 18.0,
),
const VSpace(6.0),
FlowyText.regular(
LocaleKeys.space_createSpaceDescription.tr(),
fontSize: 14.0,
color: Theme.of(context).hintColor,
maxLines: 2,
),
const VSpace(16.0),
SizedBox.square(
dimension: 56,
child: SpaceIconPopup(
onIconChanged: (icon, iconColor) {
spaceIcon = icon;
spaceIconColor = iconColor;
},
),
),
const VSpace(8.0),
_SpaceNameTextField(onChanged: (value) => spaceName = value),
const VSpace(20.0),
SpacePermissionSwitch(
onPermissionChanged: (value) => spacePermission = value,
),
const VSpace(20.0),
SpaceCancelOrConfirmButton(
confirmButtonName: LocaleKeys.button_create.tr(),
onCancel: () => Navigator.of(context).pop(),
onConfirm: () {
context.read<SpaceBloc>().add(
SpaceEvent.create(
name: spaceName,
icon: spaceIcon,
iconColor: spaceIconColor,
permission: spacePermission,
),
);
Navigator.of(context).pop();
},
),
],
),
);
}
}
class _SpaceNameTextField extends StatelessWidget {
const _SpaceNameTextField({required this.onChanged});
final void Function(String name) onChanged;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.regular(
LocaleKeys.space_spaceName.tr(),
fontSize: 14.0,
color: Theme.of(context).hintColor,
),
const VSpace(6.0),
SizedBox(
height: 40,
child: FlowyTextField(
hintText: 'Untitled space',
onChanged: onChanged,
),
),
],
);
}
}

View File

@ -0,0 +1,123 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ManageSpacePopup extends StatefulWidget {
const ManageSpacePopup({super.key});
@override
State<ManageSpacePopup> createState() => _ManageSpacePopupState();
}
class _ManageSpacePopupState extends State<ManageSpacePopup> {
String? spaceName;
String? spaceIcon;
String? spaceIconColor;
SpacePermission? spacePermission;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
width: 500,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
LocaleKeys.space_manage.tr(),
fontSize: 18.0,
),
const VSpace(16.0),
_SpaceNameTextField(
onNameChanged: (name) => spaceName = name,
onIconChanged: (icon, color) {
spaceIcon = icon;
spaceIconColor = color;
},
),
const VSpace(16.0),
SpacePermissionSwitch(
spacePermission:
context.read<SpaceBloc>().state.currentSpace?.spacePermission,
onPermissionChanged: (value) => spacePermission = value,
),
const VSpace(16.0),
SpaceCancelOrConfirmButton(
confirmButtonName: LocaleKeys.button_save.tr(),
onCancel: () => Navigator.of(context).pop(),
onConfirm: () {
context.read<SpaceBloc>().add(
SpaceEvent.update(
name: spaceName,
icon: spaceIcon,
iconColor: spaceIconColor,
permission: spacePermission,
),
);
Navigator.of(context).pop();
},
),
],
),
);
}
}
class _SpaceNameTextField extends StatelessWidget {
const _SpaceNameTextField({
required this.onNameChanged,
required this.onIconChanged,
});
final void Function(String name) onNameChanged;
final void Function(String icon, String color) onIconChanged;
@override
Widget build(BuildContext context) {
final space = context.read<SpaceBloc>().state.currentSpace;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.regular(
LocaleKeys.space_spaceName.tr(),
fontSize: 14.0,
color: Theme.of(context).hintColor,
),
const VSpace(8.0),
SizedBox(
height: 40,
child: Row(
children: [
SizedBox.square(
dimension: 40,
child: SpaceIconPopup(
icon: space?.spaceIcon,
iconColor: space?.spaceIconColor,
onIconChanged: onIconChanged,
),
),
const HSpace(12),
Expanded(
child: SizedBox(
height: 40,
child: FlowyTextField(
text: space?.name,
onChanged: onNameChanged,
),
),
),
],
),
),
],
);
}
}

View File

@ -0,0 +1,253 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SpacePermissionSwitch extends StatefulWidget {
const SpacePermissionSwitch({
super.key,
required this.onPermissionChanged,
this.spacePermission,
this.showArrow = false,
});
final SpacePermission? spacePermission;
final void Function(SpacePermission permission) onPermissionChanged;
final bool showArrow;
@override
State<SpacePermissionSwitch> createState() => _SpacePermissionSwitchState();
}
class _SpacePermissionSwitchState extends State<SpacePermissionSwitch> {
late SpacePermission spacePermission =
widget.spacePermission ?? SpacePermission.publicToAll;
final popoverController = PopoverController();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.regular(
LocaleKeys.space_permission.tr(),
fontSize: 14.0,
color: Theme.of(context).hintColor,
),
const VSpace(6.0),
AppFlowyPopover(
controller: popoverController,
direction: PopoverDirection.bottomWithCenterAligned,
constraints: const BoxConstraints(maxWidth: 500),
offset: const Offset(0, 4),
margin: EdgeInsets.zero,
decoration: FlowyDecoration.decoration(
Theme.of(context).cardColor,
Theme.of(context).colorScheme.shadow,
borderRadius: 10,
),
popupBuilder: (_) => _buildPermissionButtons(),
child: DecoratedBox(
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: BorderSide(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(10),
),
),
child: SpacePermissionButton(
showArrow: true,
permission: spacePermission,
),
),
),
],
);
}
Widget _buildPermissionButtons() {
return SizedBox(
width: 452,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SpacePermissionButton(
permission: SpacePermission.publicToAll,
onTap: () => _onPermissionChanged(SpacePermission.publicToAll),
),
SpacePermissionButton(
permission: SpacePermission.private,
onTap: () => _onPermissionChanged(SpacePermission.private),
),
],
),
);
}
void _onPermissionChanged(SpacePermission permission) {
widget.onPermissionChanged(permission);
setState(() {
spacePermission = permission;
});
popoverController.close();
}
}
class SpacePermissionButton extends StatelessWidget {
const SpacePermissionButton({
super.key,
required this.permission,
this.onTap,
this.showArrow = false,
});
final SpacePermission permission;
final VoidCallback? onTap;
final bool showArrow;
@override
Widget build(BuildContext context) {
final (title, desc, icon) = switch (permission) {
SpacePermission.publicToAll => (
LocaleKeys.space_publicPermission.tr(),
LocaleKeys.space_publicPermissionDescription.tr(),
FlowySvgs.space_permission_public_s
),
SpacePermission.private => (
LocaleKeys.space_privatePermission.tr(),
LocaleKeys.space_privatePermissionDescription.tr(),
FlowySvgs.space_permission_private_s
),
};
return FlowyButton(
margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0),
radius: BorderRadius.circular(10),
iconPadding: 16.0,
leftIcon: FlowySvg(icon),
rightIcon: showArrow
? const FlowySvg(FlowySvgs.space_permission_dropdown_s)
: null,
text: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.regular(title),
const VSpace(4.0),
FlowyText.regular(
desc,
fontSize: 12.0,
color: Theme.of(context).hintColor,
),
],
),
onTap: onTap,
);
}
}
class SpaceCancelOrConfirmButton extends StatelessWidget {
const SpaceCancelOrConfirmButton({
super.key,
required this.onCancel,
required this.onConfirm,
required this.confirmButtonName,
this.confirmButtonColor,
});
final VoidCallback onCancel;
final VoidCallback onConfirm;
final String confirmButtonName;
final Color? confirmButtonColor;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
DecoratedBox(
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(color: Color(0x1E14171B)),
borderRadius: BorderRadius.circular(8),
),
),
child: FlowyButton(
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),
text: FlowyText.regular(LocaleKeys.button_cancel.tr()),
onTap: onCancel,
),
),
const HSpace(12.0),
DecoratedBox(
decoration: ShapeDecoration(
color: confirmButtonColor ?? Theme.of(context).colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: FlowyButton(
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),
radius: BorderRadius.circular(8),
text: FlowyText.regular(
confirmButtonName,
color: Theme.of(context).colorScheme.onPrimary,
),
onTap: onConfirm,
),
),
],
);
}
}
class DeleteSpacePopup extends StatelessWidget {
const DeleteSpacePopup({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 12.0,
horizontal: 20.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
LocaleKeys.space_deleteConfirmation.tr(),
fontSize: 14.0,
),
const VSpace(16.0),
FlowyText.regular(
LocaleKeys.space_deleteConfirmationDescription.tr(),
fontSize: 12.0,
color: Theme.of(context).hintColor,
maxLines: 3,
lineHeight: 1.4,
),
const VSpace(20.0),
SpaceCancelOrConfirmButton(
onCancel: () => Navigator.of(context).pop(),
onConfirm: () {
context.read<SpaceBloc>().add(const SpaceEvent.delete(null));
Navigator.of(context).pop();
},
confirmButtonName: LocaleKeys.space_delete.tr(),
confirmButtonColor: Theme.of(context).colorScheme.error,
),
const VSpace(8.0),
],
),
);
}
}

View File

@ -0,0 +1,188 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_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/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package: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:provider/provider.dart';
class SidebarSpace extends StatelessWidget {
const SidebarSpace({
super.key,
this.isHoverEnabled = true,
required this.userProfile,
});
final bool isHoverEnabled;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
// const sectionPadding = 16.0;
return ValueListenableBuilder(
valueListenable: getIt<MenuSharedState>().notifier,
builder: (context, value, child) {
return Provider.value(
value: userProfile,
child: Column(
children: [
const VSpace(4.0),
// favorite
BlocBuilder<FavoriteBloc, FavoriteState>(
builder: (context, state) {
if (state.views.isEmpty) {
return const SizedBox.shrink();
}
return FavoriteFolder(
views: state.views.map((e) => e.item).toList(),
);
},
),
const VSpace(16.0),
// spaces
const _Space(),
const VSpace(200),
],
),
);
},
);
}
}
class _Space extends StatefulWidget {
const _Space();
@override
State<_Space> createState() => _SpaceState();
}
class _SpaceState extends State<_Space> {
final ValueNotifier<bool> isHovered = ValueNotifier(false);
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
// final isCollaborativeWorkspace =
// context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
if (state.spaces.isEmpty) {
return const SizedBox.shrink();
}
final currentSpace = state.currentSpace ?? state.spaces.first;
return MouseRegion(
onEnter: (_) => isHovered.value = true,
onExit: (_) => isHovered.value = false,
child: Column(
children: [
SidebarSpaceHeader(
isExpanded: state.isExpanded,
space: currentSpace,
onAdded: () => _showCreatePagePopup(context, currentSpace),
onPressed: () {},
onTapMore: () {},
),
_Pages(
key: ValueKey(currentSpace.id),
space: currentSpace,
isHovered: isHovered,
),
],
),
);
},
);
}
void _showCreatePagePopup(BuildContext context, ViewPB space) {
createViewAndShowRenameDialogIfNeeded(
context,
LocaleKeys.newPageText.tr(),
(viewName, _) {
if (viewName.isNotEmpty) {
context.read<SpaceBloc>().add(
SpaceEvent.createPage(
name: viewName,
index: 0,
viewSection:
space.spacePermission == SpacePermission.publicToAll
? ViewSectionPB.Public
: ViewSectionPB.Private,
),
);
context.read<SpaceBloc>().add(SpaceEvent.expand(space, true));
}
},
);
}
}
class _Pages extends StatelessWidget {
const _Pages({
super.key,
required this.space,
required this.isHovered,
});
final ViewPB space;
final ValueNotifier<bool> isHovered;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
ViewBloc(view: space)..add(const ViewEvent.initial()),
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
return Column(
children: state.view.childViews
.map(
(view) => ViewItem(
key: ValueKey('${space.id} ${view.id}'),
spaceType:
space.spacePermission == SpacePermission.publicToAll
? FolderSpaceType.public
: FolderSpaceType.private,
isFirstChild: view.id == state.view.childViews.first.id,
view: view,
level: 0,
leftPadding: HomeSpaceViewSizes.leftPadding,
isFeedback: false,
isHovered: isHovered,
onSelected: (viewContext, view) {
if (HardwareKeyboard.instance.isControlPressed) {
context.read<TabsBloc>().openTab(view);
}
context.read<TabsBloc>().openPlugin(view);
},
onTertiarySelected: (viewContext, view) =>
context.read<TabsBloc>().openTab(view),
),
)
.toList(),
);
},
),
);
}
}

View File

@ -0,0 +1,210 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
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/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarSpaceHeader extends StatefulWidget {
const SidebarSpaceHeader({
super.key,
required this.space,
required this.onPressed,
required this.onAdded,
required this.onTapMore,
required this.isExpanded,
});
final ViewPB space;
final VoidCallback onPressed;
final VoidCallback onAdded;
final VoidCallback onTapMore;
final bool isExpanded;
@override
State<SidebarSpaceHeader> createState() => _SidebarSpaceHeaderState();
}
class _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {
final isHovered = ValueNotifier(false);
final onEditing = ValueNotifier(false);
@override
void dispose() {
isHovered.dispose();
onEditing.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
constraints: const BoxConstraints(maxWidth: 252),
direction: PopoverDirection.bottomWithLeftAligned,
clickHandler: PopoverClickHandler.gestureDetector,
offset: const Offset(0, 4),
popupBuilder: (_) => BlocProvider.value(
value: context.read<SpaceBloc>(),
child: const SidebarSpaceMenu(),
),
child: SizedBox(
height: HomeSizes.workspaceSectionHeight,
child: MouseRegion(
onEnter: (_) => isHovered.value = true,
onExit: (_) => isHovered.value = false,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
height: HomeSizes.workspaceSectionHeight,
child: FlowyButton(
margin: const EdgeInsets.only(left: 6.0, right: 4.0),
// rightIcon: _buildRightIcon(),
iconPadding: 10.0,
text: _buildChild(),
),
),
Positioned(
right: 4,
child: _buildRightIcon(),
),
],
),
),
),
);
}
Widget _buildChild() {
return Row(
children: [
SpaceIcon(
dimension: 20,
space: widget.space,
cornerRadius: 6.0,
),
const HSpace(10),
FlowyText.medium(
widget.space.name,
lineHeight: 1.15,
fontSize: 14.0,
),
const HSpace(4.0),
FlowySvg(
widget.isExpanded
? FlowySvgs.workspace_drop_down_menu_show_s
: FlowySvgs.workspace_drop_down_menu_hide_s,
),
],
);
}
Widget _buildRightIcon() {
return ValueListenableBuilder(
valueListenable: onEditing,
builder: (context, onEditing, child) => ValueListenableBuilder(
valueListenable: isHovered,
builder: (context, onHover, child) =>
Opacity(opacity: onHover || onEditing ? 1 : 0, child: child),
child: Row(
children: [
SpaceMorePopup(
space: widget.space,
onEditing: (value) => this.onEditing.value = value,
onAction: _onAction,
),
const HSpace(8.0),
FlowyIconButton(
width: 24,
iconPadding: const EdgeInsets.all(4.0),
icon: const FlowySvg(FlowySvgs.view_item_add_s),
onPressed: widget.onAdded,
),
],
),
),
);
}
Future<void> _onAction(SpaceMoreActionType type, dynamic data) async {
switch (type) {
case SpaceMoreActionType.rename:
await _showRenameDialog();
break;
case SpaceMoreActionType.changeIcon:
final (String icon, String iconColor) = data;
context.read<SpaceBloc>().add(SpaceEvent.changeIcon(icon, iconColor));
break;
case SpaceMoreActionType.manage:
_showManageSpaceDialog(context);
break;
case SpaceMoreActionType.addNewSpace:
break;
case SpaceMoreActionType.collapseAllPages:
break;
case SpaceMoreActionType.delete:
_showDeleteSpaceDialog(context);
break;
case SpaceMoreActionType.divider:
break;
}
}
Future<void> _showRenameDialog() async {
await NavigatorTextFieldDialog(
title: LocaleKeys.space_rename.tr(),
value: widget.space.name,
autoSelectAllText: true,
onConfirm: (name, _) {
context.read<SpaceBloc>().add(SpaceEvent.rename(widget.space, name));
},
).show(context);
}
void _showManageSpaceDialog(BuildContext context) {
final spaceBloc = context.read<SpaceBloc>();
showDialog(
context: context,
builder: (_) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: BlocProvider.value(
value: spaceBloc,
child: const ManageSpacePopup(),
),
);
},
);
}
void _showDeleteSpaceDialog(BuildContext context) {
final spaceBloc = context.read<SpaceBloc>();
showDialog(
context: context,
builder: (_) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: BlocProvider.value(
value: spaceBloc,
child: const SizedBox(width: 440, child: DeleteSpacePopup()),
),
);
},
);
}
}

View File

@ -0,0 +1,133 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SidebarSpaceMenu extends StatelessWidget {
const SidebarSpaceMenu({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceBloc, SpaceState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const VSpace(4.0),
for (final space in state.spaces)
SizedBox(
height: HomeSpaceViewSizes.viewHeight,
child: _SidebarSpaceMenuItem(
space: space,
isSelected: state.currentSpace?.id == space.id,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(
height: 0.5,
),
),
const SizedBox(
height: HomeSpaceViewSizes.viewHeight,
child: _CreateSpaceButton(),
),
],
);
},
);
}
}
class _SidebarSpaceMenuItem extends StatelessWidget {
const _SidebarSpaceMenuItem({
required this.space,
required this.isSelected,
});
final ViewPB space;
final bool isSelected;
@override
Widget build(BuildContext context) {
return FlowyButton(
text: Row(
children: [
FlowyText.regular(space.name),
const HSpace(6.0),
if (space.spacePermission == SpacePermission.private)
FlowyTooltip(
message: LocaleKeys.space_privatePermissionDescription.tr(),
child: const FlowySvg(
FlowySvgs.space_lock_s,
),
),
],
),
iconPadding: 10,
leftIcon: SpaceIcon(
dimension: 20,
space: space,
cornerRadius: 6.0,
),
leftIconSize: const Size.square(20),
rightIcon: isSelected
? const FlowySvg(
FlowySvgs.workspace_selected_s,
blendMode: null,
)
: null,
onTap: () {
context.read<SpaceBloc>().add(SpaceEvent.open(space));
PopoverContainer.of(context).close();
},
);
}
}
class _CreateSpaceButton extends StatelessWidget {
const _CreateSpaceButton();
@override
Widget build(BuildContext context) {
return FlowyButton(
text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()),
iconPadding: 10,
leftIcon: const FlowySvg(
FlowySvgs.space_add_s,
),
onTap: () {
PopoverContainer.of(context).close();
_showCreateSpaceDialog(context);
},
);
}
void _showCreateSpaceDialog(BuildContext context) {
final spaceBloc = context.read<SpaceBloc>();
showDialog(
context: context,
builder: (_) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: BlocProvider.value(
value: spaceBloc,
child: const CreateSpacePopup(),
),
);
},
);
}
}

View File

@ -0,0 +1,68 @@
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:flutter/material.dart';
enum SpaceMoreActionType {
delete,
rename,
changeIcon,
collapseAllPages,
divider,
addNewSpace,
manage,
}
extension ViewMoreActionTypeExtension on SpaceMoreActionType {
String get name {
switch (this) {
case SpaceMoreActionType.delete:
return LocaleKeys.space_delete.tr();
case SpaceMoreActionType.rename:
return LocaleKeys.space_rename.tr();
case SpaceMoreActionType.changeIcon:
return LocaleKeys.space_changeIcon.tr();
case SpaceMoreActionType.collapseAllPages:
return LocaleKeys.space_collapseAllSubPages.tr();
case SpaceMoreActionType.addNewSpace:
return LocaleKeys.space_addNewSpace.tr();
case SpaceMoreActionType.manage:
return LocaleKeys.space_manage.tr();
case SpaceMoreActionType.divider:
return '';
}
}
Widget get leftIcon {
switch (this) {
case SpaceMoreActionType.delete:
return const FlowySvg(FlowySvgs.trash_s, blendMode: null);
case SpaceMoreActionType.rename:
return const FlowySvg(FlowySvgs.view_item_rename_s);
case SpaceMoreActionType.changeIcon:
return const FlowySvg(FlowySvgs.change_icon_s);
case SpaceMoreActionType.collapseAllPages:
return const FlowySvg(FlowySvgs.collapse_all_page_s);
case SpaceMoreActionType.addNewSpace:
return const FlowySvg(FlowySvgs.space_add_s);
case SpaceMoreActionType.manage:
return const FlowySvg(FlowySvgs.space_manage_s);
case SpaceMoreActionType.divider:
return const SizedBox.shrink();
}
}
Widget get rightIcon {
switch (this) {
case SpaceMoreActionType.changeIcon:
return const FlowySvg(FlowySvgs.view_item_right_arrow_s);
case SpaceMoreActionType.rename:
case SpaceMoreActionType.collapseAllPages:
case SpaceMoreActionType.divider:
case SpaceMoreActionType.delete:
case SpaceMoreActionType.addNewSpace:
case SpaceMoreActionType.manage:
return const SizedBox.shrink();
}
}
}

View File

@ -0,0 +1,27 @@
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
class SpaceIcon extends StatelessWidget {
const SpaceIcon({
super.key,
required this.dimension,
this.cornerRadius = 0,
required this.space,
});
final double dimension;
final double cornerRadius;
final ViewPB space;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: dimension,
child: ClipRRect(
borderRadius: BorderRadius.circular(cornerRadius),
child: space.spaceIconSvg,
),
);
}
}

View File

@ -0,0 +1,320 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/decoration.dart';
import 'package:flutter/material.dart';
final builtInSpaceColors = [
'0xFFA34AFD',
'0xFFFB006D',
'0xFF00C8FF',
'0xFFFFBA00',
'0xFFF254BC',
'0xFF2AC985',
'0xFFAAD93D',
'0xFF535CE4',
'0xFF808080',
'0xFFD2515F',
'0xFF409BF8',
'0xFFFF8933',
];
final builtInSpaceIcons =
List.generate(15, (index) => 'space_icon_${index + 1}');
class SpaceIconPopup extends StatefulWidget {
const SpaceIconPopup({
super.key,
this.icon,
this.iconColor,
required this.onIconChanged,
});
final String? icon;
final String? iconColor;
final void Function(String icon, String color) onIconChanged;
@override
State<SpaceIconPopup> createState() => _SpaceIconPopupState();
}
class _SpaceIconPopupState extends State<SpaceIconPopup> {
late ValueNotifier<String> selectedColor =
ValueNotifier<String>(widget.iconColor ?? builtInSpaceColors.first);
late ValueNotifier<String> selectedIcon =
ValueNotifier<String>(widget.icon ?? builtInSpaceIcons.first);
@override
void dispose() {
selectedColor.dispose();
selectedIcon.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AppFlowyPopover(
offset: const Offset(0, 4),
decoration: FlowyDecoration.decoration(
Theme.of(context).cardColor,
Theme.of(context).colorScheme.shadow,
borderRadius: 10,
),
constraints: const BoxConstraints(maxWidth: 220),
margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0),
direction: PopoverDirection.bottomWithCenterAligned,
child: _buildPreview(),
popupBuilder: (_) => SpaceIconPicker(
icon: selectedIcon.value,
iconColor: selectedColor.value,
onIconChanged: (icon, iconColor) {
selectedIcon.value = icon;
selectedColor.value = iconColor;
widget.onIconChanged(icon, iconColor);
},
),
);
}
Widget _buildPreview() {
bool onHover = false;
return StatefulBuilder(
builder: (context, setState) {
return MouseRegion(
cursor: SystemMouseCursors.click,
onEnter: (event) => setState(() => onHover = true),
onExit: (event) => setState(() => onHover = false),
child: ValueListenableBuilder(
valueListenable: selectedColor,
builder: (_, color, __) {
return ValueListenableBuilder(
valueListenable: selectedIcon,
builder: (_, icon, __) {
final child = ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: FlowySvg(
FlowySvgData('assets/flowy_icons/16x/$icon.svg'),
color: Color(int.parse(color)),
blendMode: BlendMode.srcOut,
),
);
if (onHover) {
return Stack(
children: [
Positioned.fill(
child: Opacity(opacity: 0.2, child: child),
),
const Center(
child: FlowySvg(
FlowySvgs.view_item_rename_s,
size: Size.square(20),
),
),
],
);
}
return child;
},
);
},
),
);
},
);
}
}
class SpaceIconPicker extends StatefulWidget {
const SpaceIconPicker({
super.key,
required this.onIconChanged,
this.skipFirstNotification = false,
this.icon,
this.iconColor,
});
final bool skipFirstNotification;
final void Function(String icon, String color) onIconChanged;
final String? icon;
final String? iconColor;
@override
State<SpaceIconPicker> createState() => _SpaceIconPickerState();
}
class _SpaceIconPickerState extends State<SpaceIconPicker> {
late ValueNotifier<String> selectedColor =
ValueNotifier<String>(widget.iconColor ?? builtInSpaceColors.first);
late ValueNotifier<String> selectedIcon =
ValueNotifier<String>(widget.icon ?? builtInSpaceIcons.first);
@override
void initState() {
super.initState();
if (!widget.skipFirstNotification) {
widget.onIconChanged(selectedIcon.value, selectedColor.value);
}
selectedColor.addListener(() {
widget.onIconChanged(selectedIcon.value, selectedColor.value);
});
selectedIcon.addListener(() {
widget.onIconChanged(selectedIcon.value, selectedColor.value);
});
}
@override
void dispose() {
selectedColor.dispose();
selectedIcon.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FlowyText.regular(
LocaleKeys.space_spaceIconBackground.tr(),
color: Theme.of(context).hintColor,
),
const VSpace(10.0),
_Colors(
selectedColor: selectedColor.value,
onColorSelected: (color) {
selectedColor.value = color;
},
),
const VSpace(12.0),
FlowyText.regular(
LocaleKeys.space_spaceIcon.tr(),
color: Theme.of(context).hintColor,
),
const VSpace(10.0),
ValueListenableBuilder(
valueListenable: selectedColor,
builder: (_, value, ___) => _Icons(
selectedColor: value,
selectedIcon: selectedIcon.value,
onIconSelected: (icon) {
selectedIcon.value = icon;
},
),
),
],
);
}
}
class _Colors extends StatefulWidget {
const _Colors({
required this.selectedColor,
required this.onColorSelected,
});
final String selectedColor;
final void Function(String color) onColorSelected;
@override
State<_Colors> createState() => _ColorsState();
}
class _ColorsState extends State<_Colors> {
late String selectedColor = widget.selectedColor;
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 6,
mainAxisSpacing: 4.0,
children: builtInSpaceColors.map((color) {
return GestureDetector(
onTap: () {
setState(() {
selectedColor = color;
});
widget.onColorSelected(color);
},
child: Container(
margin: const EdgeInsets.all(2.0),
padding: const EdgeInsets.all(2.0),
decoration: selectedColor == color
? ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.50,
strokeAlign: BorderSide.strokeAlignOutside,
color: Color(0xFF00BCF0),
),
borderRadius: BorderRadius.circular(20),
),
)
: null,
child: DecoratedBox(
decoration: BoxDecoration(
color: Color(int.parse(color)),
borderRadius: BorderRadius.circular(20.0),
),
),
),
);
}).toList(),
);
}
}
class _Icons extends StatefulWidget {
const _Icons({
required this.selectedColor,
required this.selectedIcon,
required this.onIconSelected,
});
final String selectedColor;
final String selectedIcon;
final void Function(String color) onIconSelected;
@override
State<_Icons> createState() => _IconsState();
}
class _IconsState extends State<_Icons> {
late String selectedIcon = widget.selectedIcon;
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 5,
mainAxisSpacing: 8.0,
crossAxisSpacing: 12.0,
children: builtInSpaceIcons.map((icon) {
return GestureDetector(
onTap: () {
setState(() {
selectedIcon = icon;
});
widget.onIconSelected(icon);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: FlowySvg(
FlowySvgData('assets/flowy_icons/16x/$icon.svg'),
color: Color(int.parse(widget.selectedColor)),
blendMode: BlendMode.srcOut,
),
),
);
}).toList(),
);
}
}

View File

@ -0,0 +1,188 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SpaceMorePopup extends StatelessWidget {
const SpaceMorePopup({
super.key,
required this.space,
required this.onAction,
required this.onEditing,
});
final ViewPB space;
final void Function(SpaceMoreActionType type, dynamic data) onAction;
final void Function(bool value) onEditing;
@override
Widget build(BuildContext context) {
final wrappers = _buildActionTypeWrappers();
return PopoverActionList<SpaceMoreActionTypeWrapper>(
direction: PopoverDirection.bottomWithLeftAligned,
offset: const Offset(0, 8),
actions: wrappers,
constraints: const BoxConstraints(
minWidth: 260,
),
buildChild: (popover) {
return FlowyIconButton(
width: 24,
icon: const FlowySvg(FlowySvgs.workspace_three_dots_s),
onPressed: () {
onEditing(true);
popover.show();
},
);
},
onSelected: (_, __) {},
onClosed: () => onEditing(false),
);
}
List<SpaceMoreActionTypeWrapper> _buildActionTypeWrappers() {
final actionTypes = _buildActionTypes();
return actionTypes
.map(
(e) => SpaceMoreActionTypeWrapper(e, (controller, data) {
onAction(e, data);
controller.close();
}),
)
.toList();
}
List<SpaceMoreActionType> _buildActionTypes() {
return [
SpaceMoreActionType.rename,
SpaceMoreActionType.changeIcon,
SpaceMoreActionType.manage,
// SpaceMoreActionType.divider,
// SpaceMoreActionType.addNewSpace,
// SpaceMoreActionType.collapseAllPages,
SpaceMoreActionType.divider,
SpaceMoreActionType.delete,
];
}
}
class SpaceMoreActionTypeWrapper extends CustomActionCell {
SpaceMoreActionTypeWrapper(this.inner, this.onTap);
final SpaceMoreActionType inner;
final void Function(PopoverController controller, dynamic data) onTap;
@override
Widget buildWithContext(BuildContext context, PopoverController controller) {
if (inner == SpaceMoreActionType.divider) {
return _buildDivider();
} else if (inner == SpaceMoreActionType.changeIcon) {
return _buildEmojiActionButton(context, controller);
} else {
return _buildNormalActionButton(context, controller);
}
}
Widget _buildNormalActionButton(
BuildContext context,
PopoverController controller,
) {
return _buildActionButton(context, () => onTap(controller, null));
}
Widget _buildEmojiActionButton(
BuildContext context,
PopoverController controller,
) {
final child = _buildActionButton(context, null);
final spaceBloc = context.read<SpaceBloc>();
final color = spaceBloc.state.currentSpace?.spaceIconColor;
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(216, 256)),
margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0),
clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (_) => SpaceIconPicker(
iconColor: color,
skipFirstNotification: true,
onIconChanged: (icon, color) {
onTap(controller, (icon, color));
},
),
child: child,
);
}
Widget _buildDivider() {
return const Padding(
padding: EdgeInsets.all(8.0),
child: Divider(height: 1.0),
);
}
Widget _buildActionButton(
BuildContext context,
VoidCallback? onTap,
) {
final spaceBloc = context.read<SpaceBloc>();
final spaces = spaceBloc.state.spaces;
final currentSpace = spaceBloc.state.currentSpace;
bool disable = false;
var message = '';
if (inner == SpaceMoreActionType.delete) {
if (spaces.length <= 1) {
disable = true;
message = LocaleKeys.space_unableToDeleteLastSpace.tr();
} else if (currentSpace?.createdBy != context.read<UserProfilePB>().id) {
disable = true;
message = LocaleKeys.space_unableToDeleteSpaceNotCreatedByYou.tr();
}
}
final child = Container(
height: 34,
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Opacity(
opacity: disable ? 0.5 : 1.0,
child: FlowyButton(
disable: disable,
margin: const EdgeInsets.symmetric(horizontal: 6),
leftIcon: inner.leftIcon,
rightIcon: inner.rightIcon,
iconPadding: 10.0,
text: SizedBox(
height: 18.0,
child: FlowyText.regular(
inner.name,
color: inner == SpaceMoreActionType.delete
? Theme.of(context).colorScheme.error
: null,
),
),
onTap: onTap,
),
),
);
if (inner == SpaceMoreActionType.delete) {
return FlowyTooltip(
message: message,
child: child,
);
}
return child;
}
}

View File

@ -145,6 +145,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
return AppFlowyPopover( return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(364, 356)), constraints: BoxConstraints.loose(const Size(364, 356)),
margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0),
clickHandler: PopoverClickHandler.gestureDetector, clickHandler: PopoverClickHandler.gestureDetector,
popupBuilder: (_) => FlowyIconPicker( popupBuilder: (_) => FlowyIconPicker(
onSelected: (result) => onTap(controller, result), onSelected: (result) => onTap(controller, result),

View File

@ -178,7 +178,7 @@ class FlowyText extends StatelessWidget {
textStyle, textStyle,
forceStrutHeight: true, forceStrutHeight: true,
leadingDistribution: TextLeadingDistribution.even, leadingDistribution: TextLeadingDistribution.even,
height: 1.1, height: lineHeight ?? 1.1,
) )
: null, : null,
); );

View File

@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.5.9 version: 0.6.0
environment: environment:
flutter: ">=3.22.0" flutter: ">=3.22.0"

View File

@ -0,0 +1,24 @@
import 'package:appflowy/util/time.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('parseTime should parse time string to minutes', () {
expect(parseTime('10'), 10);
expect(parseTime('70m'), 70);
expect(parseTime('4h 20m'), 260);
expect(parseTime('1h 80m'), 140);
expect(parseTime('asffsa2h3m'), null);
expect(parseTime('2h3m'), null);
expect(parseTime('blah'), null);
expect(parseTime('10a'), null);
expect(parseTime('2h'), 120);
});
test('formatTime should format time minutes to formatted string', () {
expect(formatTime(5), "5m");
expect(formatTime(75), "1h 15m");
expect(formatTime(120), "2h");
expect(formatTime(-50), "");
expect(formatTime(0), "0m");
});
}

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -189,28 +189,10 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"getrandom 0.2.10",
"reqwest",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"tsify",
"url",
"uuid",
"wasm-bindgen",
]
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -221,34 +203,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bytes",
"futures",
"serde",
"serde_json",
"serde_repr",
"thiserror",
]
[[package]]
name = "appflowy-cloud-billing-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client?rev=9f9c2d1ad180987a31d18c6c067a56a5fa1f6da6#9f9c2d1ad180987a31d18c6c067a56a5fa1f6da6"
dependencies = [
"client-api",
"reqwest",
"serde",
"serde_json",
"shared-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"tokio",
"yrs",
]
[[package]] [[package]]
name = "appflowy_tauri" name = "appflowy_tauri"
version = "0.0.0" version = "0.0.0"
@ -818,11 +772,11 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"async-trait", "async-trait",
"bincode", "bincode",
"brotli", "brotli",
@ -833,12 +787,12 @@ dependencies = [
"collab-entity", "collab-entity",
"collab-rt-entity", "collab-rt-entity",
"collab-rt-protocol", "collab-rt-protocol",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"futures-core", "futures-core",
"futures-util", "futures-util",
"getrandom 0.2.10", "getrandom 0.2.10",
"gotrue", "gotrue",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"mime", "mime",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"prost", "prost",
@ -849,7 +803,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serde_urlencoded", "serde_urlencoded",
"shared-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "shared-entity",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-retry", "tokio-retry",
@ -865,7 +819,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -1105,7 +1059,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -1115,7 +1069,7 @@ dependencies = [
"collab", "collab",
"collab-entity", "collab-entity",
"collab-rt-protocol", "collab-rt-protocol",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"prost", "prost",
"prost-build", "prost-build",
"protoc-bin-vendored", "protoc-bin-vendored",
@ -1130,7 +1084,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1487,29 +1441,10 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"bincode",
"chrono",
"collab-entity",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"tracing",
"uuid",
"validator",
]
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"bincode", "bincode",
"chrono", "chrono",
"collab-entity", "collab-entity",
@ -2313,7 +2248,6 @@ name = "flowy-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"appflowy-cloud-billing-client",
"bytes", "bytes",
"chrono", "chrono",
"client-api", "client-api",
@ -2927,12 +2861,12 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
"getrandom 0.2.10", "getrandom 0.2.10",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"infra", "infra",
"reqwest", "reqwest",
"serde", "serde",
@ -2944,24 +2878,10 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"chrono",
"jsonwebtoken",
"lazy_static",
"serde",
"serde_json",
]
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"chrono", "chrono",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
@ -3390,7 +3310,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -5880,41 +5800,17 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"appflowy-ai-client 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "appflowy-ai-client",
"bytes", "bytes",
"chrono", "chrono",
"collab-entity", "collab-entity",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"futures", "futures",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"log",
"pin-project",
"reqwest",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"uuid",
]
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"appflowy-ai-client 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"bytes",
"chrono",
"collab-entity",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"futures",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"log", "log",
"pin-project", "pin-project",
"reqwest", "reqwest",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { version = "0.2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9d3d28ad8937712cc688c20be7c0ee6e4d14a168" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true
@ -113,6 +113,3 @@ collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFl
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" }
shared-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }

View File

@ -216,7 +216,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -233,28 +233,10 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"getrandom 0.2.12",
"reqwest",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"tsify",
"url",
"uuid",
"wasm-bindgen",
]
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -265,34 +247,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bytes",
"futures",
"serde",
"serde_json",
"serde_repr",
"thiserror",
]
[[package]]
name = "appflowy-cloud-billing-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client?rev=9f9c2d1ad180987a31d18c6c067a56a5fa1f6da6#9f9c2d1ad180987a31d18c6c067a56a5fa1f6da6"
dependencies = [
"client-api",
"reqwest",
"serde",
"serde_json",
"shared-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"tokio",
"yrs",
]
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.7.1" version = "1.7.1"
@ -608,11 +562,11 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"async-trait", "async-trait",
"bincode", "bincode",
"brotli", "brotli",
@ -623,12 +577,12 @@ dependencies = [
"collab-entity", "collab-entity",
"collab-rt-entity", "collab-rt-entity",
"collab-rt-protocol", "collab-rt-protocol",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"futures-core", "futures-core",
"futures-util", "futures-util",
"getrandom 0.2.12", "getrandom 0.2.12",
"gotrue", "gotrue",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"mime", "mime",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"prost", "prost",
@ -639,7 +593,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serde_urlencoded", "serde_urlencoded",
"shared-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "shared-entity",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-retry", "tokio-retry",
@ -655,7 +609,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -833,7 +787,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -843,7 +797,7 @@ dependencies = [
"collab", "collab",
"collab-entity", "collab-entity",
"collab-rt-protocol", "collab-rt-protocol",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"prost", "prost",
"prost-build", "prost-build",
"protoc-bin-vendored", "protoc-bin-vendored",
@ -858,7 +812,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1072,29 +1026,10 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"bincode",
"chrono",
"collab-entity",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"tracing",
"uuid",
"validator",
]
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"bincode", "bincode",
"chrono", "chrono",
"collab-entity", "collab-entity",
@ -1597,7 +1532,6 @@ name = "flowy-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"appflowy-cloud-billing-client",
"bytes", "bytes",
"chrono", "chrono",
"client-api", "client-api",
@ -1952,12 +1886,12 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
"getrandom 0.2.12", "getrandom 0.2.12",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"infra", "infra",
"reqwest", "reqwest",
"serde", "serde",
@ -1969,24 +1903,10 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"chrono",
"jsonwebtoken",
"lazy_static",
"serde",
"serde_json",
]
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"chrono", "chrono",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
@ -2284,7 +2204,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -3986,41 +3906,17 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"appflowy-ai-client 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "appflowy-ai-client",
"bytes", "bytes",
"chrono", "chrono",
"collab-entity", "collab-entity",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"futures", "futures",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"log",
"pin-project",
"reqwest",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"uuid",
]
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"appflowy-ai-client 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"bytes",
"chrono",
"collab-entity",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"futures",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"log", "log",
"pin-project", "pin-project",
"reqwest", "reqwest",

View File

@ -55,7 +55,7 @@ yrs = "0.18.8"
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { version = "0.2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9d3d28ad8937712cc688c20be7c0ee6e4d14a168" }
[profile.dev] [profile.dev]
opt-level = 0 opt-level = 0
@ -75,6 +75,3 @@ collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFl
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" }
shared-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { version = "0.2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9d3d28ad8937712cc688c20be7c0ee6e4d14a168" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true
@ -114,6 +114,3 @@ collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFl
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" }
shared-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }

View File

@ -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="2" y="7.39941" width="12" height="1.2" rx="0.6" fill="#171717"/>
<rect x="7.40234" y="14" width="12" height="1.2" rx="0.6" transform="rotate(-90 7.40234 14)" fill="#171717"/>
</svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="6" fill="#FFBD00"/>
<path d="M12.5 15H7.5C7.295 15 7.125 14.83 7.125 14.625C7.125 14.42 7.295 14.25 7.5 14.25H12.5C12.705 14.25 12.875 14.42 12.875 14.625C12.875 14.83 12.705 15 12.5 15Z" fill="white"/>
<path d="M14.1722 6.75953L12.1722 8.18953C11.9072 8.37953 11.5272 8.26453 11.4122 7.95953L10.4672 5.43953C10.3072 5.00453 9.69218 5.00453 9.53218 5.43953L8.58218 7.95453C8.46718 8.26453 8.09218 8.37953 7.82718 8.18453L5.82718 6.75453C5.42718 6.47453 4.89718 6.86953 5.06218 7.33453L7.14218 13.1595C7.21218 13.3595 7.40218 13.4895 7.61218 13.4895H12.3772C12.5872 13.4895 12.7772 13.3545 12.8472 13.1595L14.9272 7.33453C15.0972 6.86953 14.5672 6.47453 14.1722 6.75953ZM11.2472 11.3745H8.74718C8.54218 11.3745 8.37218 11.2045 8.37218 10.9995C8.37218 10.7945 8.54218 10.6245 8.74718 10.6245H11.2472C11.4522 10.6245 11.6222 10.7945 11.6222 10.9995C11.6222 11.2045 11.4522 11.3745 11.2472 11.3745Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,11 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="none"/>
<g clip-path="url(#clip0_474_78597)">
<path d="M19.7487 11.6337C19.0487 8.55366 16.362 7.16699 14.002 7.16699C14.002 7.16699 14.002 7.16699 13.9953 7.16699C11.642 7.16699 8.94868 8.54699 8.24868 11.627C7.46868 15.067 9.57535 17.9803 11.482 19.8137C12.1887 20.4937 13.0954 20.8337 14.002 20.8337C14.9087 20.8337 15.8154 20.4937 16.5154 19.8137C18.422 17.9803 20.5287 15.0737 19.7487 11.6337ZM14.002 14.9737C12.842 14.9737 11.902 14.0337 11.902 12.8737C11.902 11.7137 12.842 10.7737 14.002 10.7737C15.162 10.7737 16.102 11.7137 16.102 12.8737C16.102 14.0337 15.162 14.9737 14.002 14.9737Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_474_78597">
<rect width="16" height="16" fill="white" transform="translate(6 6)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 891 B

View File

@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M17.8091 7.33301H17.1758C16.9158 7.33301 16.7024 7.54634 16.7024 7.80634C16.7024 8.07301 16.9158 8.28634 17.1758 8.28634H17.8091C18.9491 8.28634 19.8758 9.21301 19.8758 10.3463V14.2663C19.6358 14.1397 19.3691 14.073 19.0824 14.073H16.5424C15.5824 14.073 14.7958 14.853 14.7958 15.8197V16.8597H13.2091V15.8197C13.2091 14.853 12.4224 14.073 11.4624 14.073H8.92245C8.63578 14.073 8.36911 14.1397 8.12911 14.2663V10.3463C8.12911 9.21301 9.05578 8.28634 10.1958 8.28634H10.8291C11.0891 8.28634 11.3024 8.07301 11.3024 7.80634C11.3024 7.54634 11.0891 7.33301 10.8291 7.33301H10.1958C8.52911 7.33301 7.17578 8.68634 7.17578 10.3463V15.8197V18.9197C7.17578 19.8863 7.96245 20.6663 8.92245 20.6663H11.4624C12.4224 20.6663 13.2091 19.8863 13.2091 18.9197V17.8063H14.7958V18.9197C14.7958 19.8863 15.5824 20.6663 16.5424 20.6663H19.0824C20.0424 20.6663 20.8291 19.8863 20.8291 18.9197V15.8197V10.3463C20.8291 8.68634 19.4758 7.33301 17.8091 7.33301Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M16.762 7.97336L10.742 9.97336C6.69536 11.3267 6.69536 13.5334 10.742 14.88L12.5287 15.4734L13.122 17.26C14.4687 21.3067 16.682 21.3067 18.0287 17.26L20.0354 11.2467C20.9287 8.5467 19.462 7.07336 16.762 7.97336ZM16.9754 11.56L14.442 14.1067C14.342 14.2067 14.2154 14.2534 14.0887 14.2534C13.962 14.2534 13.8354 14.2067 13.7354 14.1067C13.542 13.9134 13.542 13.5934 13.7354 13.4L16.2687 10.8534C16.462 10.66 16.782 10.66 16.9754 10.8534C17.1687 11.0467 17.1687 11.3667 16.9754 11.56Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 662 B

View File

@ -0,0 +1,6 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M15.811 18.6651C16.0762 18.5617 16.3826 18.7877 16.3257 19.0667C15.9991 20.3333 15.2657 20.6667 14.3657 20.6667H13.6391C12.7391 20.6667 11.9991 20.3333 11.6791 19.06C11.6242 18.7806 11.9291 18.5565 12.1942 18.6603C12.7639 18.8835 13.3729 19 13.9991 19C14.6282 19 15.2403 18.8876 15.811 18.6651Z" fill="white"/>
<path d="M16.3285 8.93301C16.3874 9.21481 16.0768 9.44428 15.8089 9.33882C15.2504 9.11895 14.641 8.99967 14.0018 8.99967C13.3634 8.99967 12.7552 9.12043 12.1963 9.34052C11.931 9.44496 11.6249 9.21901 11.6818 8.93967C12.0018 7.66634 12.7418 7.33301 13.6418 7.33301H14.3685C15.2685 7.33301 16.0018 7.66634 16.3285 8.93301Z" fill="white"/>
<path d="M14.0013 9.66699C11.608 9.66699 9.66797 11.607 9.66797 14.0003C9.66797 15.4003 10.328 16.6403 11.3546 17.4337H11.3613C12.0946 18.0003 13.008 18.3337 14.0013 18.3337C15.008 18.3337 15.928 17.9937 16.6613 17.4203H16.668C17.6813 16.627 18.3346 15.387 18.3346 14.0003C18.3346 11.607 16.3946 9.66699 14.0013 9.66699ZM15.288 15.587C15.188 15.687 15.0613 15.7337 14.9346 15.7337C14.808 15.7337 14.6813 15.687 14.5813 15.587L13.648 14.6537C13.5546 14.5603 13.5013 14.4337 13.5013 14.3003V12.4403C13.5013 12.167 13.728 11.9403 14.0013 11.9403C14.2746 11.9403 14.5013 12.167 14.5013 12.4403V14.0937L15.288 14.8803C15.4813 15.0737 15.4813 15.3937 15.288 15.587Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M17.0987 9.81348H10.8987C8.93203 9.81348 7.33203 11.4135 7.33203 13.3801V17.1001C7.33203 19.0668 8.93203 20.6668 10.8987 20.6668H17.0987C19.0654 20.6668 20.6654 19.0668 20.6654 17.1001V13.3801C20.6654 11.4135 19.0654 9.81348 17.0987 9.81348ZM15.6654 14.0135C15.6654 13.6468 15.9654 13.3468 16.332 13.3468C16.6987 13.3468 16.9987 13.6468 16.9987 14.0135C16.9987 14.3801 16.6987 14.6868 16.332 14.6868C15.9654 14.6868 15.6654 14.3935 15.6654 14.0268V14.0135ZM12.752 16.7135C12.652 16.8135 12.5254 16.8601 12.3987 16.8601C12.272 16.8601 12.1454 16.8135 12.0454 16.7135L11.3587 16.0268L10.6987 16.6868C10.5987 16.7868 10.472 16.8335 10.3454 16.8335C10.2187 16.8335 10.092 16.7868 9.99203 16.6868C9.7987 16.4935 9.7987 16.1735 9.99203 15.9801L10.652 15.3201L10.012 14.6801C9.8187 14.4868 9.8187 14.1668 10.012 13.9735C10.2054 13.7801 10.5254 13.7801 10.7187 13.9735L11.3587 14.6135L12.0187 13.9535C12.212 13.7601 12.532 13.7601 12.7254 13.9535C12.9187 14.1468 12.9187 14.4668 12.7254 14.6601L12.0654 15.3201L12.752 16.0068C12.9454 16.2001 12.9454 16.5201 12.752 16.7135ZM15.0254 16.0001C14.6587 16.0001 14.352 15.7001 14.352 15.3335C14.352 14.9668 14.6454 14.6668 15.012 14.6668H15.0254C15.392 14.6668 15.692 14.9668 15.692 15.3335C15.692 15.7001 15.3987 16.0001 15.0254 16.0001ZM16.332 17.3135C15.9654 17.3135 15.6654 17.0201 15.6654 16.6535V16.6401C15.6654 16.2735 15.9654 15.9735 16.332 15.9735C16.6987 15.9735 16.9987 16.2735 16.9987 16.6401C16.9987 17.0068 16.7054 17.3135 16.332 17.3135ZM17.652 16.0001C17.2854 16.0001 16.9787 15.7001 16.9787 15.3335C16.9787 14.9668 17.272 14.6668 17.6387 14.6668H17.652C18.0187 14.6668 18.3187 14.9668 18.3187 15.3335C18.3187 15.7001 18.0254 16.0001 17.652 16.0001Z" fill="white"/>
<path d="M15.0929 7.80634L15.0863 8.43301C15.0796 9.01967 14.5929 9.50634 13.9996 9.50634C13.8996 9.50634 13.8396 9.57301 13.8396 9.65967C13.8396 9.74634 13.9063 9.81301 13.9929 9.81301H12.9196C12.9129 9.76634 12.9062 9.71301 12.9062 9.65967C12.9062 9.05967 13.3929 8.57301 13.9862 8.57301C14.0862 8.57301 14.1529 8.50634 14.1529 8.41967L14.1596 7.79301C14.1663 7.53967 14.3729 7.33301 14.6263 7.33301H14.6329C14.8929 7.33301 15.0929 7.54634 15.0929 7.80634Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M19.3644 13.087L16.2511 11.747L15.5578 11.4537C15.4511 11.4004 15.3578 11.2604 15.3578 11.1404V9.10036C15.3578 8.46036 14.8844 7.70036 14.3111 7.40703C14.1111 7.30703 13.8711 7.30703 13.6711 7.40703C13.1044 7.70036 12.6311 8.46703 12.6311 9.10703V11.147C12.6311 11.267 12.5378 11.407 12.4311 11.4604L8.63109 13.0937C8.21109 13.267 7.87109 13.7937 7.87109 14.247V15.127C7.87109 15.6937 8.29776 15.9737 8.82443 15.747L12.1644 14.307C12.4244 14.1937 12.6378 14.3337 12.6378 14.6204V15.3604V16.5604C12.6378 16.7137 12.5511 16.9337 12.4444 17.0404L10.8978 18.5937C10.7378 18.7537 10.6644 19.067 10.7378 19.2937L11.0378 20.2004C11.1578 20.5937 11.6044 20.7804 11.9711 20.5937L13.5578 19.2604C13.7978 19.0537 14.1911 19.0537 14.4311 19.2604L16.0178 20.5937C16.3844 20.7737 16.8311 20.5937 16.9644 20.2004L17.2644 19.2937C17.3378 19.0737 17.2644 18.7537 17.1044 18.5937L15.5578 17.0404C15.4444 16.9337 15.3578 16.7137 15.3578 16.5604V14.6204C15.3578 14.3337 15.5644 14.2004 15.8311 14.307L19.1711 15.747C19.6978 15.9737 20.1244 15.6937 20.1244 15.127V14.247C20.1244 13.7937 19.7844 13.267 19.3644 13.087Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M19.6875 11.6672C19.6875 11.9064 19.4892 12.1047 19.25 12.1047H8.75C8.51083 12.1047 8.3125 11.9064 8.3125 11.6672C8.3125 11.428 8.51083 11.2297 8.75 11.2297H9.345L9.56667 10.1739C9.77667 9.15303 10.2142 8.21387 11.9525 8.21387H16.0475C17.7858 8.21387 18.2233 9.15303 18.4333 10.1739L18.655 11.2297H19.25C19.4892 11.2297 19.6875 11.428 19.6875 11.6672Z" fill="white"/>
<path d="M19.9405 14.9687C19.853 14.0062 19.5964 12.9795 17.7239 12.9795H10.2805C8.40804 12.9795 8.15721 14.0062 8.06387 14.9687L7.73721 18.5212C7.69637 18.9645 7.84221 19.4078 8.14554 19.7403C8.45471 20.0787 8.89221 20.2712 9.35887 20.2712H10.4555C11.4005 20.2712 11.5814 19.7287 11.698 19.3728L11.8147 19.0228C11.9489 18.6203 11.9839 18.5212 12.5089 18.5212H15.4955C16.0205 18.5212 16.038 18.5795 16.1897 19.0228L16.3064 19.3728C16.423 19.7287 16.6039 20.2712 17.5489 20.2712H18.6455C19.1064 20.2712 19.5497 20.0787 19.8589 19.7403C20.1622 19.4078 20.308 18.9645 20.2672 18.5212L19.9405 14.9687ZM12.2522 16.1878H10.5022C10.263 16.1878 10.0647 15.9895 10.0647 15.7503C10.0647 15.5112 10.263 15.3128 10.5022 15.3128H12.2522C12.4914 15.3128 12.6897 15.5112 12.6897 15.7503C12.6897 15.9895 12.4914 16.1878 12.2522 16.1878ZM17.5022 16.1878H15.7522C15.513 16.1878 15.3147 15.9895 15.3147 15.7503C15.3147 15.5112 15.513 15.3128 15.7522 15.3128H17.5022C17.7414 15.3128 17.9397 15.5112 17.9397 15.7503C17.9397 15.9895 17.7414 16.1878 17.5022 16.1878Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.5352 12.6064C17.7818 12.6064 17.1085 12.9731 16.6885 13.5331C16.2685 12.9731 15.5952 12.6064 14.8418 12.6064C13.5685 12.6064 12.5352 13.6464 12.5352 14.9264C12.5352 15.4198 12.6152 15.8798 12.7485 16.2998C13.4018 18.3731 15.4285 19.6198 16.4285 19.9598C16.5685 20.0064 16.8018 20.0064 16.9418 19.9598C17.9418 19.6198 19.9685 18.3798 20.6218 16.2998C20.7618 15.8731 20.8352 15.4198 20.8352 14.9264C20.8418 13.6464 19.8085 12.6064 18.5352 12.6064Z" fill="white"/>
<path d="M19.832 11.5597C19.832 11.7131 19.6787 11.8131 19.532 11.7731C18.632 11.5397 17.6454 11.7331 16.8987 12.2664C16.752 12.3731 16.552 12.3731 16.412 12.2664C15.8854 11.8797 15.2454 11.6664 14.572 11.6664C12.852 11.6664 11.452 13.0731 11.452 14.8064C11.452 16.6864 12.352 18.0931 13.2587 19.0331C13.3054 19.0797 13.2654 19.1597 13.2054 19.1331C11.3854 18.5131 7.33203 15.9397 7.33203 11.5597C7.33203 9.62641 8.88536 8.06641 10.8054 8.06641C11.9454 8.06641 12.952 8.61307 13.5854 9.45974C14.2254 8.61307 15.232 8.06641 16.3654 8.06641C18.2787 8.06641 19.832 9.62641 19.832 11.5597Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M16.392 9.33301H16.1654V7.33301C16.1654 7.05967 15.9387 6.83301 15.6654 6.83301C15.392 6.83301 15.1654 7.05967 15.1654 7.33301V9.33301H12.832V7.33301C12.832 7.05967 12.6054 6.83301 12.332 6.83301C12.0587 6.83301 11.832 7.05967 11.832 7.33301V9.33301H11.6054C10.9054 9.33301 10.332 9.90634 10.332 10.6063V13.9997C10.332 15.4663 11.332 16.6663 12.9987 16.6663H13.4987V20.6663C13.4987 20.9397 13.7254 21.1663 13.9987 21.1663C14.272 21.1663 14.4987 20.9397 14.4987 20.6663V16.6663H14.9987C16.6654 16.6663 17.6654 15.4663 17.6654 13.9997V10.6063C17.6654 9.90634 17.092 9.33301 16.392 9.33301Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 767 B

View File

@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M17.5363 21.0612H10.4749C10.1854 21.0612 9.94531 20.8211 9.94531 20.5316C9.94531 20.242 10.1854 20.002 10.4749 20.002H17.5363C17.8258 20.002 18.0659 20.242 18.0659 20.5316C18.0659 20.8211 17.8258 21.0612 17.5363 21.0612Z" fill="white"/>
<path d="M19.8978 9.42458L17.0733 11.4441C16.699 11.7125 16.1624 11.5501 15.9999 11.1193L14.6653 7.56037C14.4394 6.94602 13.5708 6.94602 13.3449 7.56037L12.0032 11.1123C11.8408 11.5501 11.3112 11.7125 10.9369 11.4371L8.11236 9.41752C7.54745 9.02208 6.79894 9.57993 7.03196 10.2366L9.96951 18.4632C10.0684 18.7456 10.3367 18.9292 10.6333 18.9292H17.3628C17.6594 18.9292 17.9277 18.7386 18.0266 18.4632L20.9641 10.2366C21.2042 9.57993 20.4557 9.02208 19.8978 9.42458ZM15.7669 15.9423H12.2362C11.9467 15.9423 11.7066 15.7022 11.7066 15.4126C11.7066 15.1231 11.9467 14.883 12.2362 14.883H15.7669C16.0564 14.883 16.2965 15.1231 16.2965 15.4126C16.2965 15.7022 16.0564 15.9423 15.7669 15.9423Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,6 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M20.1668 18.3137L19.0668 18.5737C18.8202 18.6337 18.6268 18.8204 18.5735 19.0671L18.3402 20.0471C18.2135 20.5804 17.5335 20.7471 17.1802 20.3271L15.1868 18.0337C15.0268 17.8471 15.1135 17.5537 15.3535 17.4937C16.5335 17.2071 17.5935 16.5471 18.3735 15.6071C18.5002 15.4537 18.7268 15.4337 18.8668 15.5737L20.3468 17.0537C20.8535 17.5604 20.6735 18.1937 20.1668 18.3137Z" fill="white"/>
<path d="M7.79816 18.3137L8.89816 18.5737C9.14482 18.6337 9.33816 18.8204 9.39149 19.0671L9.62482 20.0471C9.75149 20.5804 10.4315 20.7471 10.7848 20.3271L12.7782 18.0337C12.9382 17.8471 12.8515 17.5537 12.6115 17.4937C11.4315 17.2071 10.3715 16.5471 9.59149 15.6071C9.46482 15.4537 9.23816 15.4337 9.09816 15.5737L7.61816 17.0537C7.11149 17.5604 7.29149 18.1937 7.79816 18.3137Z" fill="white"/>
<path d="M13.9987 7.33301C11.4187 7.33301 9.33203 9.41967 9.33203 11.9997C9.33203 12.9663 9.6187 13.853 10.112 14.593C10.832 15.6597 11.972 16.413 13.2987 16.6063C13.5254 16.6463 13.7587 16.6663 13.9987 16.6663C14.2387 16.6663 14.472 16.6463 14.6987 16.6063C16.0254 16.413 17.1654 15.6597 17.8854 14.593C18.3787 13.853 18.6654 12.9663 18.6654 11.9997C18.6654 9.41967 16.5787 7.33301 13.9987 7.33301ZM16.0387 11.853L15.4854 12.4063C15.392 12.4997 15.3387 12.6797 15.372 12.813L15.532 13.4997C15.6587 14.0397 15.372 14.253 14.892 13.9663L14.2254 13.573C14.1054 13.4997 13.9054 13.4997 13.7854 13.573L13.1187 13.9663C12.6387 14.2463 12.352 14.0397 12.4787 13.4997L12.6387 12.813C12.6654 12.6863 12.6187 12.4997 12.5254 12.4063L11.9587 11.853C11.632 11.5263 11.7387 11.1997 12.192 11.1263L12.9054 11.0063C13.0254 10.9863 13.1654 10.8797 13.2187 10.773L13.612 9.98634C13.8254 9.55967 14.172 9.55967 14.3854 9.98634L14.7787 10.773C14.832 10.8797 14.972 10.9863 15.0987 11.0063L15.812 11.1263C16.2587 11.1997 16.3654 11.5263 16.0387 11.853Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M16.7742 12.667C16.7742 12.667 16.6742 13.3337 14.6076 16.0003C12.6676 18.5137 15.4743 20.427 15.8076 20.647C15.8276 20.6603 15.8476 20.6603 15.8743 20.647C16.3276 20.367 21.3742 17.1203 16.7742 12.667Z" fill="white"/>
<path d="M15.1732 11.1934C15.1732 9.66004 14.5732 8.26004 13.9732 7.46004C13.7732 7.26004 13.4398 7.32671 13.3732 7.59337C13.1065 8.59337 12.3065 10.7267 10.3732 13.26C7.90651 16.46 10.1732 19.9267 12.5065 20.5934C13.7732 20.9267 12.1732 19.9267 11.9732 17.86C11.7732 15.26 15.1732 13.3267 15.1732 11.1934Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M14.3312 9.65319C14.3845 9.65319 14.4379 9.65319 14.4979 9.65986V7.68652C14.4979 7.41319 14.2712 7.18652 13.9979 7.18652C13.7245 7.18652 13.4979 7.41319 13.4979 7.68652V9.65986C13.5512 9.65319 13.6045 9.65319 13.6645 9.65319C10.5912 9.80652 8.14453 12.3399 8.14453 15.4532V16.7465C8.14453 17.4799 8.74453 18.0799 9.47786 18.0799H18.5179C19.2512 18.0799 19.8512 17.4799 19.8512 16.7465V15.4532C19.8512 12.3399 17.4045 9.80652 14.3312 9.65319Z" fill="white"/>
<path d="M15.8207 18.7402C16.0407 18.7402 16.2007 18.9469 16.1474 19.1602C15.8941 20.1136 15.0274 20.8136 14.0007 20.8136C12.9741 20.8136 12.1074 20.1136 11.8541 19.1602C11.8007 18.9469 11.9607 18.7402 12.1807 18.7402H15.8207Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 864 B

View File

@ -0,0 +1,6 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M16.7783 12.7069H11.2183C10.4316 12.7069 10.1583 12.1802 10.6183 11.5402L13.3983 7.64687C13.7249 7.18021 14.2716 7.18021 14.5983 7.64687L17.3783 11.5402C17.8383 12.1802 17.5649 12.7069 16.7783 12.7069Z" fill="white"/>
<path d="M17.7264 18.0004H10.2731C9.21973 18.0004 8.85973 17.3004 9.47973 16.447L12.1397 12.707H15.8597L18.5197 16.447C19.1397 17.3004 18.7797 18.0004 17.7264 18.0004Z" fill="white"/>
<path d="M14.5 18V20.6667C14.5 20.94 14.2733 21.1667 14 21.1667C13.7267 21.1667 13.5 20.94 13.5 20.6667V18H14.5Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@ -0,0 +1,9 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#none"/>
<path d="M19.9276 14.667V15.3337H19.3476C18.8542 15.3337 18.4542 15.7337 18.4542 16.2337V16.4337C18.4542 16.9337 18.0542 17.3337 17.5542 17.3337C17.0609 17.3337 16.6609 16.9337 16.6609 16.4337V16.2337C16.6609 15.7337 16.2542 15.3337 15.7609 15.3337C15.2676 15.3337 14.8676 15.7337 14.8676 16.2337V16.4337C14.8676 16.9337 14.4609 17.3337 13.9676 17.3337C13.4742 17.3337 13.0676 16.9337 13.0676 16.4337V16.2337C13.0676 15.7337 12.6676 15.3337 12.1742 15.3337C11.6809 15.3337 11.2742 15.7337 11.2742 16.2337V16.4337C11.2742 16.9337 10.8742 17.3337 10.3809 17.3337C9.88089 17.3337 9.48089 16.9337 9.48089 16.4337V16.2203C9.48089 15.727 9.08755 15.327 8.60089 15.3203H8.07422V14.667C8.07422 13.747 8.76755 12.967 9.70755 12.7403C9.89422 12.6937 10.0876 12.667 10.2942 12.667H17.7076C17.9142 12.667 18.1076 12.6937 18.2942 12.7403C19.2342 12.967 19.9276 13.747 19.9276 14.667Z" fill="white"/>
<path d="M18.2937 10.7797V11.7197C18.1004 11.6797 17.907 11.6663 17.707 11.6663H10.2937C10.0937 11.6663 9.90036 11.6863 9.70703 11.7263V10.7797C9.70703 9.97967 10.427 9.33301 11.3204 9.33301H16.6804C17.5737 9.33301 18.2937 9.97967 18.2937 10.7797Z" fill="white"/>
<path d="M11.832 8.36693V9.34026H11.3187C11.1454 9.34026 10.9854 9.36026 10.832 9.40026V8.36693C10.832 8.13359 11.0587 7.93359 11.332 7.93359C11.6054 7.93359 11.832 8.13359 11.832 8.36693Z" fill="white"/>
<path d="M17.168 8.21973V9.39973C17.0146 9.35306 16.8546 9.33306 16.6813 9.33306H16.168V8.21973C16.168 7.94639 16.3946 7.71973 16.668 7.71973C16.9413 7.71973 17.168 7.94639 17.168 8.21973Z" fill="white"/>
<path d="M14.5 7.87967V9.33301H13.5V7.87967C13.5 7.57967 13.7267 7.33301 14 7.33301C14.2733 7.33301 14.5 7.57967 14.5 7.87967Z" fill="white"/>
<path d="M20.6654 20.167C20.6654 20.4403 20.4387 20.667 20.1654 20.667H7.83203C7.5587 20.667 7.33203 20.4403 7.33203 20.167C7.33203 19.8936 7.5587 19.667 7.83203 19.667H8.07203V16.3203H8.4787V16.367C8.4787 17.2603 9.06536 18.087 9.9387 18.2803C10.6187 18.4403 11.2654 18.2203 11.7054 17.787C11.9587 17.5336 12.372 17.527 12.6254 17.7803C12.972 18.1203 13.4454 18.3336 13.9654 18.3336C14.4854 18.3336 14.9587 18.127 15.3054 17.7803C15.5587 17.5336 15.9654 17.5336 16.2254 17.787C16.6587 18.2203 17.3054 18.4403 17.992 18.2803C18.8654 18.087 19.452 17.2603 19.452 16.367V16.3336H19.9254V19.667H20.1654C20.4387 19.667 20.6654 19.8936 20.6654 20.167Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="8" height="10" viewBox="0 0 8 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M6.7881 4.14174V3.56457C6.7881 2.82926 6.7881 0.599609 4 0.599609C1.2119 0.599609 1.2119 2.82926 1.2119 3.56457V4.14174C0.304836 4.35522 0 4.97984 0 6.33186V7.12252C0 8.86196 0.505579 9.39961 2.14127 9.39961H5.85873C7.49442 9.39961 8 8.86196 8 7.12252V6.33186C8 4.97984 7.69516 4.35522 6.7881 4.14174ZM4 7.59692C3.54647 7.59692 3.18217 7.20949 3.18217 6.72719C3.18217 6.24489 3.54647 5.85746 4 5.85746C4.45353 5.85746 4.81782 6.24489 4.81782 6.72719C4.81782 7.20949 4.45353 7.59692 4 7.59692ZM5.67286 4.05477H2.32714V3.56457C2.32714 2.41021 2.5948 1.78559 4 1.78559C5.4052 1.78559 5.67286 2.41021 5.67286 3.56457V4.05477Z" fill="#171717"/>
</svg>

After

Width:  |  Height:  |  Size: 764 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.33203 8.58728V7.41394C1.33203 6.72061 1.8987 6.14728 2.5987 6.14728C3.80536 6.14728 4.2987 5.29394 3.69203 4.24728C3.34536 3.64728 3.55203 2.86728 4.1587 2.52061L5.31203 1.86061C5.8387 1.54728 6.5187 1.73394 6.83203 2.26061L6.90536 2.38728C7.50536 3.43394 8.49203 3.43394 9.0987 2.38728L9.17203 2.26061C9.48536 1.73394 10.1654 1.54728 10.692 1.86061L11.8454 2.52061C12.452 2.86728 12.6587 3.64728 12.312 4.24728C11.7054 5.29394 12.1987 6.14728 13.4054 6.14728C14.0987 6.14728 14.672 6.71394 14.672 7.41394V8.58728C14.672 9.28061 14.1054 9.85394 13.4054 9.85394C12.1987 9.85394 11.7054 10.7073 12.312 11.7539C12.6587 12.3606 12.452 13.1339 11.8454 13.4806L10.692 14.1406C10.1654 14.4539 9.48536 14.2673 9.17203 13.7406L9.0987 13.6139C8.4987 12.5673 7.51203 12.5673 6.90536 13.6139L6.83203 13.7406C6.5187 14.2673 5.8387 14.4539 5.31203 14.1406L4.1587 13.4806C3.55203 13.1339 3.34536 12.3539 3.69203 11.7539C4.2987 10.7073 3.80536 9.85394 2.5987 9.85394C1.8987 9.85394 1.33203 9.28061 1.33203 8.58728Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path d="M2.85932 5.99507C3.11967 5.73472 3.54178 5.73472 3.80213 5.99507L7.9974 10.1903L12.1927 5.99507C12.453 5.73472 12.8751 5.73472 13.1355 5.99507C13.3958 6.25542 13.3958 6.67753 13.1355 6.93788L8.4688 11.6045C8.34378 11.7296 8.17421 11.7998 7.9974 11.7998C7.82059 11.7998 7.65102 11.7296 7.52599 11.6045L2.85932 6.93788C2.59898 6.67753 2.59898 6.25542 2.85932 5.99507Z" fill="#171717"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 519 B

View File

@ -0,0 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8.33366V6.66699C5 3.90866 5.83333 1.66699 10 1.66699C14.1667 1.66699 15 3.90866 15 6.66699V8.33366" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.168 18.333H5.83464C2.5013 18.333 1.66797 17.4997 1.66797 14.1663V12.4997C1.66797 9.16634 2.5013 8.33301 5.83464 8.33301H14.168C17.5013 8.33301 18.3346 9.16634 18.3346 12.4997V14.1663C18.3346 17.4997 17.5013 18.333 14.168 18.333Z" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.3301 13.3337H13.3375" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.99412 13.3337H10.0016" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.66209 13.3337H6.66957" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 993 B

View File

@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.66797 18.333H18.3346" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 1.66699C11.3333 2.20033 12.8333 2.20033 14.1667 1.66699V4.16699C12.8333 4.70033 11.3333 4.70033 10 4.16699V1.66699Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 4.16699V6.66699" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.1654 6.66699H5.83203C4.16536 6.66699 3.33203 7.50033 3.33203 9.16699V18.3337H16.6654V9.16699C16.6654 7.50033 15.832 6.66699 14.1654 6.66699Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.81641 10H16.1831" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.65625 10V18.3333" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M9.99219 10V18.3333" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M13.3242 10V18.3333" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -331,6 +331,7 @@
"signInGithub": "Sign in with Github", "signInGithub": "Sign in with Github",
"signInDiscord": "Sign in with Discord", "signInDiscord": "Sign in with Discord",
"more": "More", "more": "More",
"create": "Create",
"close": "Close" "close": "Close"
}, },
"label": { "label": {
@ -1010,6 +1011,7 @@
"checklistFieldName": "Checklist", "checklistFieldName": "Checklist",
"relationFieldName": "Relation", "relationFieldName": "Relation",
"summaryFieldName": "AI Summary", "summaryFieldName": "AI Summary",
"timeFieldName": "Time",
"translateFieldName": "AI Translate", "translateFieldName": "AI Translate",
"tagFieldName": "AI Tags", "tagFieldName": "AI Tags",
"translateTo": "Translate to", "translateTo": "Translate to",
@ -1887,5 +1889,30 @@
"fromTrashHint": "From trash", "fromTrashHint": "From trash",
"noResultsHint": "We didn't find what you're looking for, try searching for another term.", "noResultsHint": "We didn't find what you're looking for, try searching for another term.",
"clearSearchTooltip": "Clear search field" "clearSearchTooltip": "Clear search field"
},
"space": {
"delete": "Delete space",
"deleteConfirmation": "Are you sure you want to delete this space?",
"deleteConfirmationDescription": "This action cannot be undone, and will remove the pages and data in this space.",
"rename": "Rename space",
"changeIcon": "Change icon",
"manage": "Manage space",
"addNewSpace": "Add new space",
"collapseAllSubPages": "Collapse all subpages",
"createNewSpace": "Create new space",
"createSpaceDescription": "Separate your tabs for life, work, project and more",
"spaceName": "Space name",
"permission": "Permission",
"publicPermission": "Public",
"publicPermissionDescription": "All workspace members with full access",
"privatePermission": "Private",
"privatePermissionDescription": "Only you can access this space",
"spaceIconBackground": "Background color",
"spaceIcon": "Icon",
"dangerZone": "Danger Zone",
"unableToDeleteLastSpace": "Cannot delete the last space",
"unableToDeleteSpaceNotCreatedByYou": "Cannot delete a space created by others",
"enableSpacesForYourWorkspace": "Enable spaces for your workspace",
"title": "Spaces"
} }
} }

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -180,28 +180,10 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"getrandom 0.2.10",
"reqwest",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"tsify",
"url",
"uuid",
"wasm-bindgen",
]
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -212,34 +194,6 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"bytes",
"futures",
"serde",
"serde_json",
"serde_repr",
"thiserror",
]
[[package]]
name = "appflowy-cloud-billing-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client?rev=9f9c2d1ad180987a31d18c6c067a56a5fa1f6da6#9f9c2d1ad180987a31d18c6c067a56a5fa1f6da6"
dependencies = [
"client-api",
"reqwest",
"serde",
"serde_json",
"shared-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"tokio",
"yrs",
]
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.7.1" version = "1.7.1"
@ -710,11 +664,11 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"async-trait", "async-trait",
"bincode", "bincode",
"brotli", "brotli",
@ -725,12 +679,12 @@ dependencies = [
"collab-entity", "collab-entity",
"collab-rt-entity", "collab-rt-entity",
"collab-rt-protocol", "collab-rt-protocol",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"futures-core", "futures-core",
"futures-util", "futures-util",
"getrandom 0.2.10", "getrandom 0.2.10",
"gotrue", "gotrue",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"mime", "mime",
"parking_lot 0.12.1", "parking_lot 0.12.1",
"prost", "prost",
@ -741,7 +695,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serde_urlencoded", "serde_urlencoded",
"shared-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "shared-entity",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-retry", "tokio-retry",
@ -757,7 +711,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -966,7 +920,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -976,7 +930,7 @@ dependencies = [
"collab", "collab",
"collab-entity", "collab-entity",
"collab-rt-protocol", "collab-rt-protocol",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"prost", "prost",
"prost-build", "prost-build",
"protoc-bin-vendored", "protoc-bin-vendored",
@ -991,7 +945,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1311,29 +1265,10 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"bincode",
"chrono",
"collab-entity",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"tracing",
"uuid",
"validator",
]
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"bincode", "bincode",
"chrono", "chrono",
"collab-entity", "collab-entity",
@ -2137,7 +2072,6 @@ name = "flowy-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"appflowy-cloud-billing-client",
"assert-json-diff", "assert-json-diff",
"bytes", "bytes",
"chrono", "chrono",
@ -2597,12 +2531,12 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
"getrandom 0.2.10", "getrandom 0.2.10",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"infra", "infra",
"reqwest", "reqwest",
"serde", "serde",
@ -2614,24 +2548,10 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"chrono",
"jsonwebtoken",
"lazy_static",
"serde",
"serde_json",
]
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"chrono", "chrono",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
@ -2993,7 +2913,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -5077,41 +4997,17 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358#44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9d3d28ad8937712cc688c20be7c0ee6e4d14a168#9d3d28ad8937712cc688c20be7c0ee6e4d14a168"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "app-error",
"appflowy-ai-client 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "appflowy-ai-client",
"bytes", "bytes",
"chrono", "chrono",
"collab-entity", "collab-entity",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "database-entity",
"futures", "futures",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=44ffc45c50e7fe362bd3f5ab4c7452e045d6b358)", "gotrue-entity",
"log",
"pin-project",
"reqwest",
"serde",
"serde_json",
"serde_repr",
"thiserror",
"uuid",
]
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471#ff4384fbd07a4b7394a9af8c9159cd65715d3471"
dependencies = [
"anyhow",
"app-error 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"appflowy-ai-client 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"bytes",
"chrono",
"collab-entity",
"database-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"futures",
"gotrue-entity 0.1.0 (git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ff4384fbd07a4b7394a9af8c9159cd65715d3471)",
"log", "log",
"pin-project", "pin-project",
"reqwest", "reqwest",

View File

@ -94,8 +94,7 @@ yrs = "0.18.8"
# Run the script.add_workspace_members: # Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { version = "0.2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9d3d28ad8937712cc688c20be7c0ee6e4d14a168" }
appflowy-cloud-billing-client = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud-Billing-Client", rev = "9f9c2d1ad180987a31d18c6c067a56a5fa1f6da6" }
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
@ -122,9 +121,6 @@ lto = false
incremental = false incremental = false
[patch.crates-io] [patch.crates-io]
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "44ffc45c50e7fe362bd3f5ab4c7452e045d6b358" }
shared-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "ff4384fbd07a4b7394a9af8c9159cd65715d3471" }
# TODO(Lucas.Xu) Upgrade to the latest version of RocksDB once PR(https://github.com/rust-rocksdb/rust-rocksdb/pull/869) is merged. # TODO(Lucas.Xu) Upgrade to the latest version of RocksDB once PR(https://github.com/rust-rocksdb/rust-rocksdb/pull/869) is merged.
# Currently, using the following revision id. This commit is patched to fix the 32-bit build issue and it's checked out from 0.21.0, not 0.22.0. # Currently, using the following revision id. This commit is patched to fix the 32-bit build issue and it's checked out from 0.21.0, not 0.22.0.
rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec131b9d82dc94e178fe8efc0c147b09" } rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec131b9d82dc94e178fe8efc0c147b09" }

View File

@ -657,6 +657,12 @@ impl<'a> TestRowBuilder<'a> {
checklist_field.id.clone() checklist_field.id.clone()
} }
pub fn insert_time_cell(&mut self, time: i64) -> String {
let time_field = self.field_with_type(&FieldType::Time);
self.cell_build.insert_number_cell(&time_field.id, time);
time_field.id.clone()
}
pub fn field_with_type(&self, field_type: &FieldType) -> Field { pub fn field_with_type(&self, field_type: &FieldType) -> Field {
self self
.fields .fields

View File

@ -450,7 +450,8 @@ pub enum FieldType {
Relation = 10, Relation = 10,
Summary = 11, Summary = 11,
Translate = 12, Translate = 12,
Tag = 13, Time = 13,
Tag = 14,
} }
impl Display for FieldType { impl Display for FieldType {
@ -493,6 +494,7 @@ impl FieldType {
FieldType::Summary => "Summarize", FieldType::Summary => "Summarize",
FieldType::Translate => "Translate", FieldType::Translate => "Translate",
FieldType::Tag => "Tag", FieldType::Tag => "Tag",
FieldType::Time => "Time",
}; };
s.to_string() s.to_string()
} }
@ -552,6 +554,10 @@ impl FieldType {
matches!(self, FieldType::Relation) matches!(self, FieldType::Relation)
} }
pub fn is_time(&self) -> bool {
matches!(self, FieldType::Time)
}
pub fn can_be_group(&self) -> bool { pub fn can_be_group(&self) -> bool {
self.is_select_option() || self.is_checkbox() || self.is_url() self.is_select_option() || self.is_checkbox() || self.is_url()
} }

View File

@ -6,6 +6,7 @@ mod number_filter;
mod relation_filter; mod relation_filter;
mod select_option_filter; mod select_option_filter;
mod text_filter; mod text_filter;
mod time_filter;
mod util; mod util;
pub use checkbox_filter::*; pub use checkbox_filter::*;
@ -16,4 +17,5 @@ pub use number_filter::*;
pub use relation_filter::*; pub use relation_filter::*;
pub use select_option_filter::*; pub use select_option_filter::*;
pub use text_filter::*; pub use text_filter::*;
pub use time_filter::*;
pub use util::*; pub use util::*;

View File

@ -0,0 +1,23 @@
use flowy_derive::ProtoBuf;
use crate::entities::NumberFilterConditionPB;
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct TimeFilterPB {
#[pb(index = 1)]
pub condition: NumberFilterConditionPB,
#[pb(index = 2)]
pub content: String,
}
impl ParseFilterData for TimeFilterPB {
fn parse(condition: u8, content: String) -> Self {
TimeFilterPB {
condition: NumberFilterConditionPB::try_from(condition)
.unwrap_or(NumberFilterConditionPB::Equal),
content,
}
}
}

View File

@ -10,7 +10,7 @@ use validator::Validate;
use crate::entities::{ use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, RelationFilterPB, CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, RelationFilterPB,
SelectOptionFilterPB, TextFilterPB, SelectOptionFilterPB, TextFilterPB, TimeFilterPB,
}; };
use crate::services::filter::{Filter, FilterChangeset, FilterInner}; use crate::services::filter::{Filter, FilterChangeset, FilterInner};
@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB {
.cloned::<TextFilterPB>() .cloned::<TextFilterPB>()
.unwrap() .unwrap()
.try_into(), .try_into(),
FieldType::Time => condition_and_content
.cloned::<TimeFilterPB>()
.unwrap()
.try_into(),
FieldType::Translate => condition_and_content FieldType::Translate => condition_and_content
.cloned::<TextFilterPB>() .cloned::<TextFilterPB>()
.unwrap() .unwrap()
@ -164,6 +168,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
FieldType::Summary => { FieldType::Summary => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
}, },
FieldType::Time => {
BoxAny::new(TimeFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Translate => { FieldType::Translate => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
}, },

View File

@ -17,7 +17,8 @@ macro_rules! impl_into_field_type {
10 => FieldType::Relation, 10 => FieldType::Relation,
11 => FieldType::Summary, 11 => FieldType::Summary,
12 => FieldType::Translate, 12 => FieldType::Translate,
13 => FieldType::Tag, 13 => FieldType::Time,
14 => FieldType::Tag,
_ => { _ => {
tracing::error!("🔴Can't parse FieldType from value: {}", ty); tracing::error!("🔴Can't parse FieldType from value: {}", ty);
FieldType::RichText FieldType::RichText

View File

@ -7,6 +7,7 @@ mod select_option_entities;
mod summary_entities; mod summary_entities;
mod tag_entities; mod tag_entities;
mod text_entities; mod text_entities;
mod time_entities;
mod timestamp_entities; mod timestamp_entities;
mod translate_entities; mod translate_entities;
mod url_entities; mod url_entities;
@ -20,6 +21,7 @@ pub use select_option_entities::*;
pub use summary_entities::*; pub use summary_entities::*;
pub use tag_entities::*; pub use tag_entities::*;
pub use text_entities::*; pub use text_entities::*;
pub use time_entities::*;
pub use timestamp_entities::*; pub use timestamp_entities::*;
pub use translate_entities::*; pub use translate_entities::*;
pub use url_entities::*; pub use url_entities::*;

View File

@ -0,0 +1,28 @@
use crate::services::field::TimeTypeOption;
use flowy_derive::ProtoBuf;
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct TimeTypeOptionPB {
#[pb(index = 1)]
pub dummy: String,
}
impl From<TimeTypeOption> for TimeTypeOptionPB {
fn from(_data: TimeTypeOption) -> Self {
Self {
dummy: "".to_string(),
}
}
}
impl From<TimeTypeOptionPB> for TimeTypeOption {
fn from(_data: TimeTypeOptionPB) -> Self {
Self
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct TimeCellDataPB {
#[pb(index = 2)]
pub time: i64,
}

View File

@ -222,7 +222,7 @@ impl<'a> CellBuilder<'a> {
FieldType::RichText => { FieldType::RichText => {
cells.insert(field_id, insert_text_cell(cell_str, field)); cells.insert(field_id, insert_text_cell(cell_str, field));
}, },
FieldType::Number => { FieldType::Number | FieldType::Time => {
if let Ok(num) = cell_str.parse::<i64>() { if let Ok(num) = cell_str.parse::<i64>() {
cells.insert(field_id, insert_number_cell(num, field)); cells.insert(field_id, insert_number_cell(num, field));
} }

View File

@ -7,6 +7,7 @@ pub mod selection_type_option;
pub mod summary_type_option; pub mod summary_type_option;
pub mod tag_type_option; pub mod tag_type_option;
pub mod text_type_option; pub mod text_type_option;
pub mod time_type_option;
pub mod timestamp_type_option; pub mod timestamp_type_option;
pub mod translate_type_option; pub mod translate_type_option;
mod type_option; mod type_option;
@ -21,6 +22,7 @@ pub use number_type_option::*;
pub use relation_type_option::*; pub use relation_type_option::*;
pub use selection_type_option::*; pub use selection_type_option::*;
pub use text_type_option::*; pub use text_type_option::*;
pub use time_type_option::*;
pub use timestamp_type_option::*; pub use timestamp_type_option::*;
pub use type_option::*; pub use type_option::*;
pub use type_option_cell::*; pub use type_option_cell::*;

View File

@ -79,14 +79,15 @@ impl CellDataDecoder for RichTextTypeOption {
| FieldType::SingleSelect | FieldType::SingleSelect
| FieldType::MultiSelect | FieldType::MultiSelect
| FieldType::Checkbox | FieldType::Checkbox
| FieldType::URL => Some(StringCellData::from(stringify_cell(cell, field))), | FieldType::URL
| FieldType::Summary
| FieldType::Translate
| FieldType::Time
| FieldType::Tag => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Checklist FieldType::Checklist
| FieldType::LastEditedTime | FieldType::LastEditedTime
| FieldType::CreatedTime | FieldType::CreatedTime
| FieldType::Relation => None, | FieldType::Relation => None,
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Translate => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Tag => Some(StringCellData::from(stringify_cell(cell, field))),
} }
} }

View File

@ -0,0 +1,6 @@
mod time;
mod time_entities;
mod time_filter;
pub use time::*;
pub use time_entities::*;

View File

@ -0,0 +1,115 @@
use crate::entities::{TimeCellDataPB, TimeFilterPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
TimeCellData, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform,
};
use crate::services::sort::SortCondition;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct TimeTypeOption;
impl TypeOption for TimeTypeOption {
type CellData = TimeCellData;
type CellChangeset = TimeCellChangeset;
type CellProtobufType = TimeCellDataPB;
type CellFilter = TimeFilterPB;
}
impl From<TypeOptionData> for TimeTypeOption {
fn from(_data: TypeOptionData) -> Self {
Self
}
}
impl From<TimeTypeOption> for TypeOptionData {
fn from(_data: TimeTypeOption) -> Self {
TypeOptionDataBuilder::new().build()
}
}
impl TypeOptionCellDataSerde for TimeTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
if let Some(time) = cell_data.0 {
return TimeCellDataPB { time };
}
TimeCellDataPB {
time: i64::default(),
}
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(TimeCellData::from(cell))
}
}
impl TimeTypeOption {
pub fn new() -> Self {
Self
}
}
impl TypeOptionTransform for TimeTypeOption {}
impl CellDataDecoder for TimeTypeOption {
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
self.parse_cell(cell)
}
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
if let Some(time) = cell_data.0 {
return time.to_string();
}
"".to_string()
}
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
let time_cell_data = self.parse_cell(cell).ok()?;
Some(time_cell_data.0.unwrap() as f64)
}
}
pub type TimeCellChangeset = String;
impl CellDataChangeset for TimeTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let str = changeset.trim().to_string();
let cell_data = TimeCellData(str.parse::<i64>().ok());
Ok((Cell::from(&cell_data), cell_data))
}
}
impl TypeOptionCellDataFilter for TimeTypeOption {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
filter.is_visible(cell_data.0)
}
}
impl TypeOptionCellDataCompare for TimeTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
let order = cell_data.0.cmp(&other_cell_data.0);
sort_condition.evaluate_order(order)
}
}

Some files were not shown because too many files have changed in this diff Show More