feat: sidebar UI Revamp on mobile (#5418)

* chore: replace settings icon and expand icon

* feat: use tabbar view to control the spaces

* feat: improve space UI design on mobile

* feat: improve recent space UI design on mobile

* feat: improve recent space UI design on mobile

* feat: improve favorite space UI design on mobile

* feat: improve header UI design on mobile

* feat: expand header height

* feat: update BottomNavigationBarItem icon

* fix: recent views and favorite views doesn't reload after switching workspace

* feat: improve recent view UI design on mobile

* feat: improve recent/favorite view UI design on mobile

* feat: add empty placeholder for recent/favorite space

* feat: long press on recent card to show bottom sheet

* feat: support removing page from recent

* feat: add trash button

* chore: remove recent top padding

* feat: support user avatar

* feat: support ... and + in space page

* chore: disable user avatar

* feat: optimize title display on mobile

* feat: support ... menu on Space page

* chore: add tab padding

* chore: cache image

* chore: optimize the mobile card view height

* feat: reoder tab on mobile

* fix: some emojis may not display correctly on Android devices

* fix: ignore the last edit time on test
This commit is contained in:
Lucas.Xu 2024-05-30 09:56:44 +08:00 committed by GitHub
parent 189f0e4b58
commit ace729eb78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 1928 additions and 372 deletions

View File

@ -75,4 +75,15 @@ class KVKeys {
///
/// The value is a double string.
static const String scaleFactor = 'scaleFactor';
/// The key for saving the last opened space
///
/// The value is a int string.
static const String lastOpenedSpace = 'lastOpenedSpace';
/// The key for saving the space order
///
/// The value is a json string with the following format:
/// [0, 1, 2]
static const String spaceOrder = 'spaceOrder';
}

View File

@ -23,7 +23,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
text: LocaleKeys.document_menuName.tr(),
leftIcon: const FlowySvg(
FlowySvgs.document_s,
size: Size.square(20),
size: Size.square(18),
),
showTopBorder: false,
onTap: () => onAction(ViewLayoutPB.Document),
@ -32,7 +32,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
text: LocaleKeys.grid_menuName.tr(),
leftIcon: const FlowySvg(
FlowySvgs.grid_s,
size: Size.square(20),
size: Size.square(18),
),
showTopBorder: false,
onTap: () => onAction(ViewLayoutPB.Grid),
@ -41,7 +41,7 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
text: LocaleKeys.board_menuName.tr(),
leftIcon: const FlowySvg(
FlowySvgs.board_s,
size: Size.square(20),
size: Size.square(18),
),
showTopBorder: false,
onTap: () => onAction(ViewLayoutPB.Board),
@ -49,8 +49,8 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
FlowyOptionTile.text(
text: LocaleKeys.calendar_menuName.tr(),
leftIcon: const FlowySvg(
FlowySvgs.date_s,
size: Size.square(20),
FlowySvgs.calendar_s,
size: Size.square(18),
),
showTopBorder: false,
onTap: () => onAction(ViewLayoutPB.Calendar),

View File

@ -1,9 +1,17 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/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_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
enum MobileBottomSheetType {
view,
@ -14,11 +22,13 @@ class MobileViewItemBottomSheet extends StatefulWidget {
const MobileViewItemBottomSheet({
super.key,
required this.view,
required this.actions,
this.defaultType = MobileBottomSheetType.view,
});
final ViewPB view;
final MobileBottomSheetType defaultType;
final List<MobileViewItemBottomSheetBodyAction> actions;
@override
State<MobileViewItemBottomSheet> createState() =>
@ -27,12 +37,14 @@ class MobileViewItemBottomSheet extends StatefulWidget {
class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
MobileBottomSheetType type = MobileBottomSheetType.view;
final fToast = FToast();
@override
void initState() {
super.initState();
type = widget.defaultType;
fToast.init(AppGlobals.context);
}
@override
@ -40,6 +52,7 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
switch (type) {
case MobileBottomSheetType.view:
return MobileViewItemBottomSheetBody(
actions: widget.actions,
isFavorite: widget.view.isFavorite,
onAction: (action) {
switch (action) {
@ -59,7 +72,6 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
case MobileViewItemBottomSheetBodyAction.delete:
Navigator.pop(context);
context.read<ViewBloc>().add(const ViewEvent.delete());
break;
case MobileViewItemBottomSheetBodyAction.addToFavorites:
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
@ -68,6 +80,11 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
.read<FavoriteBloc>()
.add(FavoriteEvent.toggle(widget.view));
break;
case MobileViewItemBottomSheetBodyAction.removeFromRecent:
_removeFromRecent(context);
break;
case MobileViewItemBottomSheetBodyAction.divider:
break;
}
},
);
@ -83,4 +100,74 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
);
}
}
Future<void> _removeFromRecent(BuildContext context) async {
final viewId = context.read<ViewBloc>().view.id;
final recentViewsBloc = context.read<RecentViewsBloc>();
Navigator.pop(context);
await _showConfirmDialog(
onDelete: () {
recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId]));
fToast.showToast(
child: const _RemoveToast(),
positionedToastBuilder: (context, child) {
return Positioned.fill(
top: 450,
child: child,
);
},
);
},
);
}
Future<void> _showConfirmDialog({required VoidCallback onDelete}) async {
await showFlowyCupertinoConfirmDialog(
title: LocaleKeys.sideBar_removePageFromRecent.tr(),
leftButton: FlowyText.regular(
LocaleKeys.button_cancel.tr(),
color: const Color(0xFF1456F0),
),
rightButton: FlowyText.medium(
LocaleKeys.button_delete.tr(),
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (context) {
onDelete();
Navigator.pop(context);
},
);
}
}
class _RemoveToast extends StatelessWidget {
const _RemoveToast();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: const Color(0xE5171717),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const FlowySvg(
FlowySvgs.success_s,
blendMode: null,
),
const HSpace(8.0),
FlowyText.regular(
LocaleKeys.sideBar_removeSuccess.tr(),
fontSize: 16.0,
color: Colors.white,
),
],
),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -11,6 +11,8 @@ enum MobileViewItemBottomSheetBodyAction {
delete,
addToFavorites,
removeFromFavorites,
divider,
removeFromRecent,
}
class MobileViewItemBottomSheetBody extends StatelessWidget {
@ -18,63 +20,124 @@ class MobileViewItemBottomSheetBody extends StatelessWidget {
super.key,
this.isFavorite = false,
required this.onAction,
required this.actions,
});
final bool isFavorite;
final void Function(MobileViewItemBottomSheetBodyAction action) onAction;
final List<MobileViewItemBottomSheetBodyAction> actions;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
MobileQuickActionButton(
text: LocaleKeys.button_rename.tr(),
icon: FlowySvgs.m_rename_s,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.rename,
),
),
_divider(),
MobileQuickActionButton(
text: isFavorite
? LocaleKeys.button_removeFromFavorites.tr()
: LocaleKeys.button_addToFavorites.tr(),
icon: isFavorite
? FlowySvgs.m_favorite_selected_lg
: FlowySvgs.m_favorite_unselected_lg,
iconColor: isFavorite ? Colors.yellow : null,
onTap: () => onAction(
isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
),
),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.button_duplicate.tr(),
icon: FlowySvgs.m_duplicate_s,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.duplicate,
),
),
_divider(),
MobileQuickActionButton(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
icon: FlowySvgs.m_delete_s,
iconColor: Theme.of(context).colorScheme.error,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.delete,
),
),
_divider(),
],
children:
actions.map((action) => _buildActionButton(context, action)).toList(),
);
}
Widget _divider() => const Divider(
height: 8.5,
thickness: 0.5,
);
Widget _buildActionButton(
BuildContext context,
MobileViewItemBottomSheetBodyAction action,
) {
switch (action) {
case MobileViewItemBottomSheetBodyAction.rename:
return FlowyOptionTile.text(
text: LocaleKeys.button_rename.tr(),
leftIcon: const FlowySvg(
FlowySvgs.view_item_rename_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.rename,
),
);
case MobileViewItemBottomSheetBodyAction.duplicate:
return FlowyOptionTile.text(
text: LocaleKeys.button_duplicate.tr(),
leftIcon: const FlowySvg(
FlowySvgs.duplicate_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.duplicate,
),
);
case MobileViewItemBottomSheetBodyAction.share:
return FlowyOptionTile.text(
text: LocaleKeys.button_share.tr(),
leftIcon: const FlowySvg(
FlowySvgs.share_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.share,
),
);
case MobileViewItemBottomSheetBodyAction.delete:
return FlowyOptionTile.text(
text: LocaleKeys.button_delete.tr(),
textColor: Theme.of(context).colorScheme.error,
leftIcon: FlowySvg(
FlowySvgs.delete_s,
size: const Size.square(18),
color: Theme.of(context).colorScheme.error,
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.delete,
),
);
case MobileViewItemBottomSheetBodyAction.addToFavorites:
return FlowyOptionTile.text(
text: LocaleKeys.button_addToFavorites.tr(),
leftIcon: const FlowySvg(
FlowySvgs.favorite_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.addToFavorites,
),
);
case MobileViewItemBottomSheetBodyAction.removeFromFavorites:
return FlowyOptionTile.text(
text: LocaleKeys.button_removeFromFavorites.tr(),
leftIcon: const FlowySvg(
FlowySvgs.favorite_section_remove_from_favorite_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.removeFromFavorites,
),
);
case MobileViewItemBottomSheetBodyAction.removeFromRecent:
return FlowyOptionTile.text(
text: LocaleKeys.button_removeFromRecent.tr(),
leftIcon: const FlowySvg(
FlowySvgs.remove_from_recent_s,
size: Size.square(18),
),
showTopBorder: false,
showBottomBorder: false,
onTap: () => onAction(
MobileViewItemBottomSheetBodyAction.removeFromRecent,
),
);
case MobileViewItemBottomSheetBodyAction.divider:
return const Divider(height: 0.5);
}
}
}

View File

@ -65,8 +65,21 @@ enum MobilePaneActionType {
],
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final isFavorite = state.view.isFavorite;
return MobileViewItemBottomSheet(
view: viewBloc.state.view,
actions: [
isFavorite
? MobileViewItemBottomSheetBodyAction
.removeFromFavorites
: MobileViewItemBottomSheetBodyAction
.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.rename,
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.delete,
],
);
},
),

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
@ -10,6 +8,7 @@ import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.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 'package:flutter_slidable/flutter_slidable.dart';
@ -17,14 +16,15 @@ class MobileFavoritePageFolder extends StatelessWidget {
const MobileFavoritePageFolder({
super.key,
required this.userProfile,
required this.workspaceId,
});
final UserProfilePB userProfile;
final String workspaceId;
@override
Widget build(BuildContext context) {
final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
'';
return MultiBlocProvider(
providers: [
BlocProvider(
@ -67,7 +67,8 @@ class MobileFavoritePageFolder extends StatelessWidget {
MobileFavoriteFolder(
showHeader: false,
forceExpanded: true,
views: favoriteState.views,
views:
favoriteState.views.map((e) => e.item).toList(),
),
const VSpace(100.0),
],

View File

@ -64,8 +64,6 @@ class MobileFavoriteScreen extends StatelessWidget {
builder: (context, state) {
return MobileFavoritePage(
userProfile: userProfile,
workspaceId: state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
);
},
),
@ -81,11 +79,9 @@ class MobileFavoritePage extends StatelessWidget {
const MobileFavoritePage({
super.key,
required this.userProfile,
required this.workspaceId,
});
final UserProfilePB userProfile;
final String workspaceId;
@override
Widget build(BuildContext context) {
@ -108,7 +104,6 @@ class MobileFavoritePage extends StatelessWidget {
Expanded(
child: MobileFavoritePageFolder(
userProfile: userProfile,
workspaceId: workspaceId,
),
),
],

View File

@ -0,0 +1,114 @@
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.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/user/prelude.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileFavoriteSpace extends StatelessWidget {
const MobileFavoriteSpace({
super.key,
required this.userProfile,
});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
'';
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SidebarSectionsBloc()
..add(SidebarSectionsEvent.initial(userProfile, workspaceId)),
),
BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: BlocListener<UserWorkspaceBloc, UserWorkspaceState>(
listener: (context, state) =>
context.read<FavoriteBloc>().add(const FavoriteEvent.initial()),
child: MultiBlocListener(
listeners: [
BlocListener<SidebarSectionsBloc, SidebarSectionsState>(
listenWhen: (p, c) =>
p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
listener: (context, state) =>
context.pushView(state.lastCreatedRootView!),
),
],
child: Builder(
builder: (context) {
final favoriteState = context.watch<FavoriteBloc>().state;
if (favoriteState.isLoading) {
return const SizedBox.shrink();
}
if (favoriteState.views.isEmpty) {
return const EmptySpacePlaceholder(
type: MobileViewCardType.favorite,
);
}
return _FavoriteViews(
favoriteViews: favoriteState.views.reversed.toList(),
);
},
),
),
),
);
}
}
class _FavoriteViews extends StatelessWidget {
const _FavoriteViews({
required this.favoriteViews,
});
final List<SectionViewPB> favoriteViews;
@override
Widget build(BuildContext context) {
return Scrollbar(
child: ListView.separated(
key: const PageStorageKey('favorite_views_page_storage_key'),
padding: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
),
itemBuilder: (context, index) {
final view = favoriteViews[index];
return Container(
padding: const EdgeInsets.symmetric(vertical: 24.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 0.5,
),
),
),
child: MobileViewCard(
key: ValueKey(view.item.id),
view: view.item,
timestamp: view.timestamp,
type: MobileViewCardType.favorite,
),
);
},
separatorBuilder: (context, index) => const HSpace(8),
itemCount: favoriteViews.length,
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:appflowy/mobile/presentation/home/mobile_folders.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileHomeSpace extends StatelessWidget {
const MobileHomeSpace({super.key, required this.userProfile});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
'';
return Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
vertical: HomeSpaceViewSizes.mVerticalPadding,
),
child: MobileFolders(
user: userProfile,
workspaceId: workspaceId,
showFavorite: false,
),
),
),
);
}
}

View File

@ -1,5 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
@ -11,6 +13,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:go_router/go_router.dart';
// Contains Public And Private Sections
class MobileFolders extends StatelessWidget {
@ -87,7 +90,8 @@ class MobileFolders extends StatelessWidget {
views: state.section.publicViews,
),
],
const VSpace(8.0),
const VSpace(4.0),
const _TrashButton(),
],
),
);
@ -97,3 +101,28 @@ class MobileFolders extends StatelessWidget {
);
}
}
class _TrashButton extends StatelessWidget {
const _TrashButton();
@override
Widget build(BuildContext context) {
return SizedBox(
height: 52,
child: FlowyButton(
expand: true,
margin: const EdgeInsets.symmetric(vertical: 8),
leftIcon: const FlowySvg(
FlowySvgs.m_delete_s,
),
leftIconSize: const Size.square(18),
iconPadding: 10.0,
text: FlowyText.regular(
LocaleKeys.trash_text.tr(),
fontSize: 16.0,
),
onTap: () => context.push(MobileHomeTrashPage.routeName),
),
);
}
}

View File

@ -1,23 +1,20 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/mobile_folders.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.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_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class MobileHomeScreen extends StatelessWidget {
@ -84,66 +81,49 @@ class MobileHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => UserWorkspaceBloc(userProfile: userProfile)
..add(
const UserWorkspaceEvent.initial(),
),
),
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
BlocProvider(
create: (context) =>
FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
buildWhen: (previous, current) =>
previous.currentWorkspace?.workspaceId !=
current.currentWorkspace?.workspaceId,
listener: (context, state) => getIt<CachedRecentService>().reset(),
builder: (context, state) {
if (state.currentWorkspace == null) {
return const SizedBox.shrink();
}
return Column(
children: [
// Header
Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
left: HomeSpaceViewSizes.mHorizontalPadding,
right: 8.0,
top: Platform.isAndroid ? 8.0 : 0.0,
),
child: MobileHomePageHeader(
userProfile: userProfile,
),
),
const Divider(),
// Folder
Expanded(
child: Scrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Recent files
const MobileRecentFolder(),
// Folders
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: MobileFolders(
user: userProfile,
workspaceId:
state.currentWorkspace?.workspaceId ??
workspaceSetting.workspaceId,
showFavorite: false,
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: _TrashButton(),
),
],
),
),
child: BlocProvider(
create: (context) =>
SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
child: MobileSpaceTab(
userProfile: userProfile,
),
),
),
@ -154,25 +134,3 @@ class MobileHomePage extends StatelessWidget {
);
}
}
class _TrashButton extends StatelessWidget {
const _TrashButton();
@override
Widget build(BuildContext context) {
return FlowyButton(
expand: true,
margin: const EdgeInsets.symmetric(vertical: 8),
leftIcon: FlowySvg(
FlowySvgs.m_delete_m,
color: Theme.of(context).colorScheme.onSurface,
),
leftIconSize: const Size.square(24),
text: FlowyText.medium(
LocaleKeys.trash_text.tr(),
fontSize: 18.0,
),
onTap: () => context.push(MobileHomeTrashPage.routeName),
);
}
}

View File

@ -35,7 +35,7 @@ class MobileHomePageHeader extends StatelessWidget {
final isCollaborativeWorkspace =
context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn;
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
constraints: const BoxConstraints(minHeight: 56),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -44,11 +44,14 @@ class MobileHomePageHeader extends StatelessWidget {
? _MobileWorkspace(userProfile: userProfile)
: _MobileUser(userProfile: userProfile),
),
IconButton(
onPressed: () => context.push(
GestureDetector(
onTap: () => context.push(
MobileHomeSettingPage.routeName,
),
icon: const FlowySvg(FlowySvgs.m_setting_m),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: FlowySvg(FlowySvgs.m_setting_m),
),
),
],
),
@ -119,7 +122,6 @@ class _MobileWorkspace extends StatelessWidget {
},
child: Row(
children: [
const HSpace(2.0),
SizedBox.square(
dimension: 34.0,
child: WorkspaceIcon(
@ -142,7 +144,7 @@ class _MobileWorkspace extends StatelessWidget {
children: [
Row(
children: [
FlowyText.medium(
FlowyText.semibold(
currentWorkspace.name,
fontSize: 16.0,
overflow: TextOverflow.ellipsis,
@ -151,7 +153,7 @@ class _MobileWorkspace extends StatelessWidget {
const FlowySvg(FlowySvgs.list_dropdown_s),
],
),
FlowyText.medium(
FlowyText.regular(
userProfile.email.isNotEmpty
? userProfile.email
: userProfile.name,

View File

@ -38,7 +38,8 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
builder: (context, state) {
final ids = <String>{};
List<ViewPB> recentViews = state.views.reversed.toList();
List<ViewPB> recentViews =
state.views.reversed.map((e) => e.item).toList();
recentViews.retainWhere((element) => ids.add(element.id));
// only keep the first 20 items.

View File

@ -0,0 +1,86 @@
import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
import 'package:appflowy/workspace/application/recent/prelude.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.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 MobileRecentSpace extends StatelessWidget {
const MobileRecentSpace({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
RecentViewsBloc()..add(const RecentViewsEvent.initial()),
child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) {
if (state.isLoading) {
return const SizedBox.shrink();
}
final recentViews = _filterRecentViews(state.views);
if (recentViews.isEmpty) {
return const Center(
child: EmptySpacePlaceholder(type: MobileViewCardType.recent),
);
}
return _RecentViews(recentViews: recentViews);
},
),
);
}
List<SectionViewPB> _filterRecentViews(List<SectionViewPB> recentViews) {
final ids = <String>{};
final filteredRecentViews = recentViews.reversed.toList();
filteredRecentViews.retainWhere((e) => ids.add(e.item.id));
return filteredRecentViews;
}
}
class _RecentViews extends StatelessWidget {
const _RecentViews({
required this.recentViews,
});
final List<SectionViewPB> recentViews;
@override
Widget build(BuildContext context) {
return Scrollbar(
child: ListView.separated(
key: const PageStorageKey('recent_views_page_storage_key'),
padding: const EdgeInsets.symmetric(
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
),
itemBuilder: (context, index) {
final sectionView = recentViews[index];
return Container(
padding: const EdgeInsets.symmetric(vertical: 24.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 0.5,
),
),
),
child: MobileViewCard(
key: ValueKey(sectionView.item.id),
view: sectionView.item,
timestamp: sectionView.timestamp,
type: MobileViewCardType.recent,
),
);
},
separatorBuilder: (context, index) => const HSpace(8),
itemCount: recentViews.length,
),
);
}
}

View File

@ -9,7 +9,6 @@ import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package: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';
@ -36,29 +35,28 @@ class MobileSectionFolder extends StatelessWidget {
builder: (context, state) {
return Column(
children: [
MobileSectionFolderHeader(
title: title,
isExpanded: context.read<FolderBloc>().state.isExpanded,
onPressed: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
onAdded: () {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name:
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
index: 0,
viewSection: spaceType.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
);
},
),
const VSpace(8.0),
const Divider(
height: 1,
SizedBox(
height: HomeSpaceViewSizes.mViewHeight,
child: MobileSectionFolderHeader(
title: title,
isExpanded: context.read<FolderBloc>().state.isExpanded,
onPressed: () => context
.read<FolderBloc>()
.add(const FolderEvent.expandOrUnExpand()),
onAdded: () {
context.read<SidebarSectionsBloc>().add(
SidebarSectionsEvent.createRootViewInSection(
name: LocaleKeys.menuAppHeader_defaultNewPageName
.tr(),
index: 0,
viewSection: spaceType.toViewSectionPB,
),
);
context.read<FolderBloc>().add(
const FolderEvent.expandOrUnExpand(isExpanded: true),
);
},
),
),
if (state.isExpanded)
...views.map(

View File

@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@ -29,24 +30,23 @@ class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
@override
Widget build(BuildContext context) {
const iconSize = 32.0;
return Row(
children: [
Expanded(
child: FlowyButton(
text: FlowyText.semibold(
text: FlowyText.medium(
widget.title,
fontSize: 20.0,
fontSize: 16.0,
),
margin: const EdgeInsets.symmetric(vertical: 8),
expandText: false,
iconPadding: 2,
mainAxisAlignment: MainAxisAlignment.start,
rightIcon: AnimatedRotation(
duration: const Duration(milliseconds: 200),
turns: _turns,
child: const Icon(
Icons.keyboard_arrow_down_rounded,
color: Colors.grey,
child: const FlowySvg(
FlowySvgs.m_spaces_expand_s,
),
),
onTap: () {
@ -60,12 +60,10 @@ class _MobileSectionFolderHeaderState extends State<MobileSectionFolderHeader> {
FlowyIconButton(
key: mobileCreateNewPageButtonKey,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
iconPadding: const EdgeInsets.all(2),
height: iconSize,
width: iconSize,
height: HomeSpaceViewSizes.mViewButtonDimension,
width: HomeSpaceViewSizes.mViewButtonDimension,
icon: const FlowySvg(
FlowySvgs.add_s,
size: Size.square(iconSize),
FlowySvgs.m_space_add_s,
),
onPressed: widget.onAdded,
),

View File

@ -0,0 +1,55 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmptySpacePlaceholder extends StatelessWidget {
const EmptySpacePlaceholder({super.key, required this.type});
final MobileViewCardType type;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 48.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FlowySvg(
FlowySvgs.m_empty_page_xl,
),
const VSpace(16.0),
FlowyText.medium(
_emptyPageText,
fontSize: 18.0,
textAlign: TextAlign.center,
),
const VSpace(8.0),
FlowyText.regular(
_emptyPageSubText,
fontSize: 17.0,
maxLines: 10,
textAlign: TextAlign.center,
lineHeight: 1.3,
color: Theme.of(context).hintColor,
),
],
),
);
}
String get _emptyPageText => switch (type) {
MobileViewCardType.recent => LocaleKeys.sideBar_emptyRecent.tr(),
MobileViewCardType.favorite => LocaleKeys.sideBar_emptyFavorite.tr(),
};
String get _emptyPageSubText => switch (type) {
MobileViewCardType.recent =>
LocaleKeys.sideBar_emptyRecentDescription.tr(),
MobileViewCardType.favorite =>
LocaleKeys.sideBar_emptyFavoriteDescription.tr(),
};
}

View File

@ -0,0 +1,396 @@
import 'dart:io';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.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_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:string_validator/string_validator.dart';
enum MobileViewCardType {
recent,
favorite;
String get lastOperationHintText => switch (this) {
MobileViewCardType.recent => LocaleKeys.sideBar_lastViewed.tr(),
MobileViewCardType.favorite => LocaleKeys.sideBar_favoriteAt.tr(),
};
}
class MobileViewCard extends StatelessWidget {
const MobileViewCard({
super.key,
required this.view,
this.timestamp,
required this.type,
});
final ViewPB view;
final Int64? timestamp;
final MobileViewCardType type;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ViewBloc>(
create: (context) =>
ViewBloc(view: view)..add(const ViewEvent.initial()),
),
BlocProvider(
create: (context) =>
RecentViewBloc(view: view)..add(const RecentViewEvent.initial()),
),
],
child: BlocBuilder<RecentViewBloc, RecentViewState>(
builder: (context, state) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapUp: (_) => context.pushView(view),
onLongPressUp: () => _showActionSheet(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(child: _buildDescription(context, state)),
const HSpace(20.0),
SizedBox(
width: 84,
height: 60,
child: _buildCover(context, state),
),
],
),
);
},
),
);
}
Widget _buildDescription(BuildContext context, RecentViewState state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// page icon & page title
_buildTitle(context, state),
const VSpace(12.0),
// author & last viewed
_buildNameAndLastViewed(context, state),
],
);
}
Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) {
final supportAvatar = isURL(state.icon);
if (!supportAvatar) {
return _buildLastViewed(context);
}
return Row(
children: [
_buildAvatar(context, state),
Flexible(child: _buildAuthor(context, state)),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 3.0),
child: FlowySvg(FlowySvgs.dot_s),
),
_buildLastViewed(context),
],
);
}
Widget _buildAvatar(BuildContext context, RecentViewState state) {
final userProfile = Provider.of<UserProfilePB?>(context);
final iconUrl = userProfile?.iconUrl;
if (iconUrl == null ||
iconUrl.isEmpty ||
view.createdBy != userProfile?.id) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(top: 2, bottom: 2, right: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: SizedBox.square(
dimension: 16.0,
child: FlowyNetworkImage(
url: iconUrl,
),
),
),
);
}
Widget _buildCover(BuildContext context, RecentViewState state) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: _ViewCover(
coverTypeV1: state.coverTypeV1,
coverTypeV2: state.coverTypeV2,
value: state.coverValue,
),
);
}
Widget _buildTitle(BuildContext context, RecentViewState state) {
final name = state.name;
final icon = state.icon;
return RichText(
maxLines: 3,
overflow: TextOverflow.ellipsis,
text: TextSpan(
children: [
TextSpan(
text: icon,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 17.0,
fontWeight: FontWeight.w600,
fontFamily: GoogleFonts.notoColorEmoji().fontFamily,
),
),
const TextSpan(text: ' '),
TextSpan(
text: name,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
Widget _buildAuthor(BuildContext context, RecentViewState state) {
return FlowyText.regular(
// view.createdBy.toString(),
'Lucas',
fontSize: 12.0,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
);
}
Widget _buildLastViewed(BuildContext context) {
if (timestamp == null) {
return const SizedBox.shrink();
}
final date = _formatTimestamp(
timestamp!.toInt() * 1000,
);
return FlowyText.regular(
date,
fontSize: 12.0,
color: Theme.of(context).hintColor,
);
}
String _formatTimestamp(int timestamp) {
final now = DateTime.now();
final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final difference = now.difference(dateTime);
final String date;
if (difference.inMinutes < 1) {
date = LocaleKeys.sideBar_justNow.tr();
} else if (difference.inHours < 1) {
// Less than 1 hour
date = LocaleKeys.sideBar_minutesAgo
.tr(namedArgs: {'count': difference.inMinutes.toString()});
} else if (difference.inHours >= 1 && difference.inHours < 24) {
// Between 1 hour and 24 hours
date = DateFormat('h:mm a').format(dateTime);
} else if (difference.inDays >= 1 && dateTime.year == now.year) {
// More than 24 hours but within the current year
date = DateFormat('M/d, h:mm a').format(dateTime);
} else {
// Other cases (previous years)
date = DateFormat('M/d/yyyy, h:mm a').format(dateTime);
}
if (difference.inHours >= 1) {
return '${type.lastOperationHintText} $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) {
final isFavorite = state.view.isFavorite;
return MobileViewItemBottomSheet(
view: viewBloc.state.view,
actions: _buildActions(isFavorite),
);
},
),
);
},
);
}
List<MobileViewItemBottomSheetBodyAction> _buildActions(bool isFavorite) {
switch (type) {
case MobileViewCardType.recent:
return [
isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent,
];
case MobileViewCardType.favorite:
return [
MobileViewItemBottomSheetBodyAction.removeFromFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
];
}
}
}
class _ViewCover extends StatelessWidget {
const _ViewCover({
required this.coverTypeV1,
this.coverTypeV2,
this.value,
});
final CoverType coverTypeV1;
final PageStyleCoverImageType? coverTypeV2;
final String? value;
@override
Widget build(BuildContext context) {
final placeholder = Container(
color: const Color(0xFFE1FBFF),
);
final value = this.value;
if (value == null) {
return placeholder;
}
if (coverTypeV2 != null) {
return _buildCoverV2(context, value, placeholder);
}
return _buildCoverV1(context, value, placeholder);
}
Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) {
final type = coverTypeV2;
if (type == null) {
return placeholder;
}
if (type == PageStyleCoverImageType.customImage ||
type == PageStyleCoverImageType.unsplashImage) {
final userProfilePB = Provider.of<UserProfilePB?>(context);
return FlowyNetworkImage(
url: value,
userProfilePB: userProfilePB,
);
}
if (type == PageStyleCoverImageType.builtInImage) {
return Image.asset(
PageStyleCoverImageType.builtInImagePath(value),
fit: BoxFit.cover,
);
}
if (type == PageStyleCoverImageType.pureColor) {
final color = value.coverColor(context);
if (color != null) {
return ColoredBox(
color: color,
);
}
}
if (type == PageStyleCoverImageType.gradientColor) {
return Container(
decoration: BoxDecoration(
gradient: FlowyGradientColor.fromId(value).linear,
),
);
}
if (type == PageStyleCoverImageType.localImage) {
return Image.file(
File(value),
fit: BoxFit.cover,
);
}
return placeholder;
}
Widget _buildCoverV1(BuildContext context, String value, Widget placeholder) {
switch (coverTypeV1) {
case CoverType.file:
if (isURL(value)) {
final userProfilePB = Provider.of<UserProfilePB?>(context);
return FlowyNetworkImage(
url: value,
userProfilePB: userProfilePB,
);
}
final imageFile = File(value);
if (!imageFile.existsSync()) {
return placeholder;
}
return Image.file(
imageFile,
);
case CoverType.asset:
return Image.asset(
value,
fit: BoxFit.cover,
);
case CoverType.color:
final color = value.tryToColor() ?? Colors.white;
return Container(
color: color,
);
case CoverType.none:
return placeholder;
}
}
}

View File

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
class RoundUnderlineTabIndicator extends Decoration {
const RoundUnderlineTabIndicator({
this.borderRadius,
this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
this.insets = EdgeInsets.zero,
required this.width,
});
final BorderRadius? borderRadius;
final BorderSide borderSide;
final EdgeInsetsGeometry insets;
final double width;
@override
Decoration? lerpFrom(Decoration? a, double t) {
if (a is UnderlineTabIndicator) {
return UnderlineTabIndicator(
borderSide: BorderSide.lerp(a.borderSide, borderSide, t),
insets: EdgeInsetsGeometry.lerp(a.insets, insets, t)!,
);
}
return super.lerpFrom(a, t);
}
@override
Decoration? lerpTo(Decoration? b, double t) {
if (b is UnderlineTabIndicator) {
return UnderlineTabIndicator(
borderSide: BorderSide.lerp(borderSide, b.borderSide, t),
insets: EdgeInsetsGeometry.lerp(insets, b.insets, t)!,
);
}
return super.lerpTo(b, t);
}
@override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _UnderlinePainter(this, borderRadius, onChanged);
}
Rect _indicatorRectFor(Rect rect, TextDirection textDirection) {
final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
final center = indicator.center.dx;
return Rect.fromLTWH(
center - width / 2.0,
indicator.bottom - borderSide.width,
width,
borderSide.width,
);
}
@override
Path getClipPath(Rect rect, TextDirection textDirection) {
if (borderRadius != null) {
return Path()
..addRRect(
borderRadius!.toRRect(_indicatorRectFor(rect, textDirection)),
);
}
return Path()..addRect(_indicatorRectFor(rect, textDirection));
}
}
class _UnderlinePainter extends BoxPainter {
_UnderlinePainter(
this.decoration,
this.borderRadius,
super.onChanged,
);
final RoundUnderlineTabIndicator decoration;
final BorderRadius? borderRadius;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
assert(configuration.size != null);
final Rect rect = offset & configuration.size!;
final TextDirection textDirection = configuration.textDirection!;
final Paint paint;
if (borderRadius != null) {
paint = Paint()..color = decoration.borderSide.color;
final Rect indicator = decoration._indicatorRectFor(rect, textDirection);
final RRect rrect = RRect.fromRectAndCorners(
indicator,
topLeft: borderRadius!.topLeft,
topRight: borderRadius!.topRight,
bottomRight: borderRadius!.bottomRight,
bottomLeft: borderRadius!.bottomLeft,
);
canvas.drawRRect(rrect, paint);
} else {
paint = decoration.borderSide.toPaint()..strokeCap = StrokeCap.round;
final Rect indicator = decoration
._indicatorRectFor(rect, textDirection)
.deflate(decoration.borderSide.width / 2.0);
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
}
}
}

View File

@ -0,0 +1,56 @@
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:flutter/material.dart';
import 'package:reorderable_tabbar/reorderable_tabbar.dart';
class MobileSpaceTabBar extends StatelessWidget {
const MobileSpaceTabBar({
super.key,
this.height = 38.0,
required this.tabController,
required this.tabs,
required this.onReorder,
});
final double height;
final List<MobileSpaceTabType> tabs;
final TabController tabController;
final OnReorder onReorder;
@override
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.bodyMedium;
final labelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 15.0,
);
final unselectedLabelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 15.0,
);
return Container(
height: height,
padding: const EdgeInsets.only(left: 8.0),
child: ReorderableTabBar(
controller: tabController,
tabs: tabs.map((e) => Tab(text: e.tr)).toList(),
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Theme.of(context).primaryColor,
isScrollable: true,
labelStyle: labelStyle,
labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),
unselectedLabelStyle: unselectedLabelStyle,
overlayColor: WidgetStateProperty.all(Colors.transparent),
indicator: RoundUnderlineTabIndicator(
width: 28.0,
borderSide: BorderSide(
color: Theme.of(context).primaryColor,
width: 3,
),
),
onReorder: onReorder,
),
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart';
import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dart';
import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
class MobileSpaceTab extends StatefulWidget {
const MobileSpaceTab({super.key, required this.userProfile});
final UserProfilePB userProfile;
@override
State<MobileSpaceTab> createState() => _MobileSpaceTabState();
}
class _MobileSpaceTabState extends State<MobileSpaceTab>
with SingleTickerProviderStateMixin {
TabController? tabController;
@override
void dispose() {
tabController?.removeListener(_onTabChange);
tabController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Provider.value(
value: widget.userProfile,
child: BlocBuilder<SpaceOrderBloc, SpaceOrderState>(
builder: (context, state) {
if (state.isLoading) {
return const SizedBox.shrink();
}
_initTabController(state);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MobileSpaceTabBar(
tabController: tabController!,
tabs: state.tabsOrder,
onReorder: (from, to) {
context.read<SpaceOrderBloc>().add(
SpaceOrderEvent.reorder(from, to),
);
},
),
const HSpace(12.0),
Expanded(
child: TabBarView(
controller: tabController,
children: _buildTabs(state),
),
),
],
);
},
),
);
}
void _initTabController(SpaceOrderState state) {
if (tabController != null) {
return;
}
tabController = TabController(
length: state.tabsOrder.length,
vsync: this,
initialIndex: state.tabsOrder.indexOf(state.defaultTab),
);
tabController?.addListener(_onTabChange);
}
void _onTabChange() {
if (tabController == null) {
return;
}
context.read<SpaceOrderBloc>().add(
SpaceOrderEvent.open(
tabController!.index,
),
);
}
List<Widget> _buildTabs(SpaceOrderState state) {
return state.tabsOrder.map((tab) {
switch (tab) {
case MobileSpaceTabType.recent:
return const MobileRecentSpace();
case MobileSpaceTabType.spaces:
return MobileHomeSpace(userProfile: widget.userProfile);
case MobileSpaceTabType.favorites:
return MobileFavoriteSpace(userProfile: widget.userProfile);
default:
throw Exception('Unknown tab type: $tab');
}
}).toList();
}
}

View File

@ -0,0 +1,127 @@
import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:bloc/bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'space_order_bloc.freezed.dart';
enum MobileSpaceTabType {
// DO NOT CHANGE THE ORDER
spaces,
recent,
favorites;
String get tr {
switch (this) {
case MobileSpaceTabType.recent:
return LocaleKeys.sideBar_RecentSpace.tr();
case MobileSpaceTabType.spaces:
return LocaleKeys.sideBar_Spaces.tr();
case MobileSpaceTabType.favorites:
return LocaleKeys.sideBar_favoriteSpace.tr();
}
}
}
class SpaceOrderBloc extends Bloc<SpaceOrderEvent, SpaceOrderState> {
SpaceOrderBloc() : super(const SpaceOrderState()) {
on<SpaceOrderEvent>(
(event, emit) async {
await event.when(
initial: () async {
final tabsOrder = await _getTabsOrder();
final defaultTab = await _getDefaultTab();
emit(
state.copyWith(
tabsOrder: tabsOrder,
defaultTab: defaultTab,
isLoading: false,
),
);
},
open: (index) async {
final tab = state.tabsOrder[index];
await _setDefaultTab(tab);
},
reorder: (from, to) async {
final tabsOrder = List.of(state.tabsOrder);
tabsOrder.insert(to, tabsOrder.removeAt(from));
await _setTabsOrder(tabsOrder);
emit(state.copyWith(tabsOrder: tabsOrder));
},
);
},
);
}
final _storage = getIt<KeyValueStorage>();
Future<MobileSpaceTabType> _getDefaultTab() async {
try {
return await _storage.getWithFormat<MobileSpaceTabType>(
KVKeys.lastOpenedSpace, (value) {
return MobileSpaceTabType.values[int.parse(value)];
}) ??
MobileSpaceTabType.spaces;
} catch (e) {
return MobileSpaceTabType.spaces;
}
}
Future<void> _setDefaultTab(MobileSpaceTabType tab) async {
await _storage.set(
KVKeys.lastOpenedSpace,
tab.index.toString(),
);
}
Future<List<MobileSpaceTabType>> _getTabsOrder() async {
try {
return await _storage.getWithFormat<List<MobileSpaceTabType>>(
KVKeys.spaceOrder, (value) {
final order = jsonDecode(value).cast<int>();
if (order.isEmpty) {
return MobileSpaceTabType.values;
}
return order
.map((e) => MobileSpaceTabType.values[e])
.cast<MobileSpaceTabType>()
.toList();
}) ??
MobileSpaceTabType.values;
} catch (e) {
return MobileSpaceTabType.values;
}
}
Future<void> _setTabsOrder(List<MobileSpaceTabType> tabsOrder) async {
await _storage.set(
KVKeys.spaceOrder,
jsonEncode(tabsOrder.map((e) => e.index).toList()),
);
}
}
@freezed
class SpaceOrderEvent with _$SpaceOrderEvent {
const factory SpaceOrderEvent.initial() = Initial;
const factory SpaceOrderEvent.open(int index) = Open;
const factory SpaceOrderEvent.reorder(int from, int to) = Reorder;
}
@freezed
class SpaceOrderState with _$SpaceOrderState {
const factory SpaceOrderState({
@Default(MobileSpaceTabType.spaces) MobileSpaceTabType defaultTab,
@Default(MobileSpaceTabType.values) List<MobileSpaceTabType> tabsOrder,
@Default(true) bool isLoading,
}) = _SpaceOrderState;
factory SpaceOrderState.initial() => const SpaceOrderState();
}

View File

@ -20,49 +20,40 @@ class MobileBottomNavigationBar extends StatelessWidget {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
enableFeedback: true,
type: BottomNavigationBarType.fixed,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
// There is no text shown on the bottom navigation bar, but Exception will be thrown if label is null here.
label: 'home',
icon: const FlowySvg(FlowySvgs.m_home_unselected_lg),
activeIcon: FlowySvg(
FlowySvgs.m_home_selected_lg,
color: style.colorScheme.primary,
bottomNavigationBar: Theme(
data: ThemeData(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
enableFeedback: false,
type: BottomNavigationBarType.fixed,
elevation: 0,
items: <BottomNavigationBarItem>[
const BottomNavigationBarItem(
label: 'home',
icon: FlowySvg(FlowySvgs.m_home_unselected_m),
activeIcon:
FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null),
),
),
const BottomNavigationBarItem(
label: 'favorite',
icon: FlowySvg(FlowySvgs.m_favorite_unselected_lg),
activeIcon: FlowySvg(
FlowySvgs.m_favorite_selected_lg,
blendMode: null,
const BottomNavigationBarItem(
label: 'add',
icon: FlowySvg(FlowySvgs.m_home_add_m),
),
),
// Enable this when search is ready.
// BottomNavigationBarItem(
// label: 'search',
// icon: const FlowySvg(FlowySvgs.m_search_lg),
// activeIcon: FlowySvg(
// FlowySvgs.m_search_lg,
// color: style.colorScheme.primary,
// ),
// ),
BottomNavigationBarItem(
label: 'notification',
icon: const FlowySvg(FlowySvgs.m_notification_unselected_lg),
activeIcon: FlowySvg(
FlowySvgs.m_notification_selected_lg,
color: style.colorScheme.primary,
BottomNavigationBarItem(
label: 'notification',
icon: const FlowySvg(FlowySvgs.m_home_notification_m),
activeIcon: FlowySvg(
FlowySvgs.m_home_notification_m,
color: style.colorScheme.primary,
),
),
),
],
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
],
currentIndex: navigationShell.currentIndex,
onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex),
),
),
);
}

View File

@ -1,11 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item_add_button.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.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/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/view/draggable_view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@ -17,8 +19,6 @@ import 'package:flutter_slidable/flutter_slidable.dart';
typedef ViewItemOnSelected = void Function(ViewPB);
typedef ActionPaneBuilder = ActionPane Function(BuildContext context);
const _itemHeight = 48.0;
class MobileViewItem extends StatelessWidget {
const MobileViewItem({
super.key,
@ -177,48 +177,10 @@ class InnerMobileViewItem extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
child,
const Divider(
height: 1,
),
...children,
],
);
} else {
child = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
child,
const Divider(
height: 1,
),
Container(
height: _itemHeight,
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(left: (level + 2) * leftPadding),
child: FlowyText.medium(
LocaleKeys.noPagesInside.tr(),
color: Colors.grey,
),
),
),
const Divider(
height: 1,
),
],
);
}
} else {
child = Column(
mainAxisSize: MainAxisSize.min,
children: [
child,
const Divider(
height: 1,
),
],
);
}
// wrap the child with DraggableItem if isDraggable is true
@ -226,7 +188,6 @@ class InnerMobileViewItem extends StatelessWidget {
child = DraggableViewItem(
isFirstChild: isFirstChild,
view: view,
// FIXME: use better color
centerHighlightColor: Colors.blue.shade200,
topHighlightColor: Colors.blue.shade200,
bottomHighlightColor: Colors.blue.shade200,
@ -296,15 +257,15 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
final children = [
// expand icon
_buildLeftIcon(),
const HSpace(4),
const HSpace(6),
// icon
_buildViewIcon(),
const HSpace(8),
// title
Expanded(
child: FlowyText.medium(
child: FlowyText.regular(
widget.view.name,
fontSize: 18.0,
fontSize: 16.0,
overflow: TextOverflow.ellipsis,
),
),
@ -317,6 +278,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
// only support add button for document layout
if (!widget.isFeedback && widget.view.layout == ViewLayoutPB.Document) {
// + button
children.add(_buildViewMoreButton(context));
children.add(_buildViewAddButton(context));
}
@ -324,7 +286,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
borderRadius: BorderRadius.circular(4.0),
onTap: () => widget.onSelected(widget.view),
child: SizedBox(
height: _itemHeight,
height: HomeSpaceViewSizes.mViewHeight,
child: Padding(
padding: EdgeInsets.only(left: widget.level * widget.leftPadding),
child: Row(
@ -349,12 +311,12 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
Widget _buildViewIcon() {
final icon = widget.view.icon.value.isNotEmpty
? EmojiText(
emoji: widget.view.icon.value,
fontSize: 24.0,
? FlowyText.emoji(
widget.view.icon.value,
fontSize: 20.0,
)
: SizedBox.square(
dimension: 26.0,
dimension: 18.0,
child: widget.view.defaultIcon(),
);
return icon;
@ -368,13 +330,17 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
return const _DotIconWidget();
}
if (context.read<ViewBloc>().state.view.childViews.isEmpty) {
return HSpace(widget.leftPadding);
}
return GestureDetector(
child: AnimatedRotation(
duration: const Duration(milliseconds: 250),
turns: widget.isExpanded ? 0 : -0.25,
child: const Icon(
Icons.keyboard_arrow_down_rounded,
size: 28,
child: const FlowySvg(
FlowySvgs.m_expand_s,
blendMode: null,
),
),
onTap: () {
@ -418,6 +384,51 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
},
);
}
// + button
Widget _buildViewMoreButton(BuildContext context) {
return MobileViewMoreButton(onPressed: () => _showMoreActions(context));
}
Future<void> _showMoreActions(BuildContext context) async {
final viewBloc = context.read<ViewBloc>();
final favoriteBloc = context.read<FavoriteBloc>();
await showMobileBottomSheet(
context,
showHeader: true,
title: widget.view.name,
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
builder: (context) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: viewBloc),
BlocProvider.value(value: favoriteBloc),
],
child: BlocBuilder<ViewBloc, ViewState>(
builder: (context, state) {
final isFavorite = state.view.isFavorite;
return MobileViewItemBottomSheet(
view: viewBloc.state.view,
actions: [
isFavorite
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.rename,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.delete,
],
);
},
),
);
},
);
}
}
class _DotIconWidget extends StatelessWidget {

View File

@ -1,9 +1,8 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
const _iconSize = 32.0;
class MobileViewAddButton extends StatelessWidget {
const MobileViewAddButton({
super.key,
@ -15,12 +14,31 @@ class MobileViewAddButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FlowyIconButton(
iconPadding: const EdgeInsets.all(2),
width: _iconSize,
height: _iconSize,
width: HomeSpaceViewSizes.mViewButtonDimension,
height: HomeSpaceViewSizes.mViewButtonDimension,
icon: const FlowySvg(
FlowySvgs.add_s,
size: Size.square(_iconSize),
FlowySvgs.m_space_add_s,
),
onPressed: onPressed,
);
}
}
class MobileViewMoreButton extends StatelessWidget {
const MobileViewMoreButton({
super.key,
required this.onPressed,
});
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return FlowyIconButton(
width: HomeSpaceViewSizes.mViewButtonDimension,
height: HomeSpaceViewSizes.mViewButtonDimension,
icon: const FlowySvg(
FlowySvgs.m_space_more_s,
),
onPressed: onPressed,
);

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class MobileQuickActionButton extends StatelessWidget {
const MobileQuickActionButton({
@ -32,20 +31,20 @@ class MobileQuickActionButton extends StatelessWidget {
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
splashColor: Colors.transparent,
child: Container(
height: 44,
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
FlowySvg(
icon,
size: const Size.square(20),
size: const Size.square(18),
color: enable ? iconColor : Theme.of(context).disabledColor,
),
const HSpace(12),
Expanded(
child: FlowyText(
child: FlowyText.regular(
text,
fontSize: 15,
fontSize: 16,
color: enable ? textColor : Theme.of(context).disabledColor,
),
),

View File

@ -1,6 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
enum ConfirmDialogActionAlignment {
@ -85,3 +87,46 @@ Future<T?> showFlowyMobileConfirmDialog<T>(
},
);
}
Future<T?> showFlowyCupertinoConfirmDialog<T>({
BuildContext? context,
required String title,
required Widget leftButton,
required Widget rightButton,
void Function(BuildContext context)? onLeftButtonPressed,
void Function(BuildContext context)? onRightButtonPressed,
}) {
return showDialog(
context: context ?? AppGlobals.context,
builder: (context) => CupertinoAlertDialog(
title: FlowyText.medium(
title,
fontSize: 18,
maxLines: 10,
lineHeight: 1.3,
),
actions: [
CupertinoDialogAction(
onPressed: () {
if (onLeftButtonPressed != null) {
onLeftButtonPressed(context);
} else {
Navigator.of(context).pop();
}
},
child: leftButton,
),
CupertinoDialogAction(
onPressed: () {
if (onRightButtonPressed != null) {
onRightButtonPressed(context);
} else {
Navigator.of(context).pop();
}
},
child: rightButton,
),
],
),
);
}

View File

@ -101,9 +101,13 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
return const EdgeInsets.only(top: 12.0, bottom: 4.0);
},
placeholderText: (node) => LocaleKeys.blockPlaceholders_heading.tr(
args: [node.attributes[HeadingBlockKeys.level].toString()],
),
placeholderText: (node) {
int level = node.attributes[HeadingBlockKeys.level] ?? 6;
level = level.clamp(1, 6);
return LocaleKeys.blockPlaceholders_heading.tr(
args: [level.toString()],
);
},
),
textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level),
),

View File

@ -16,6 +16,7 @@ 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-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:auto_size_text_field/auto_size_text_field.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart';
@ -133,9 +134,11 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
if (documentFontFamily != null && fontFamily != documentFontFamily) {
fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily;
}
return TextField(
return AutoSizeTextField(
controller: textEditingController,
focusNode: focusNode,
minFontSize: 18.0,
decoration: const InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
@ -151,6 +154,7 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
fontFamily: fontFamily,
color:
state.cover.isNone || state.cover.isPresets ? null : Colors.white,
overflow: TextOverflow.ellipsis,
),
onChanged: _rename,
onSubmitted: _rename,

View File

@ -1,7 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart';
@ -20,6 +18,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
// const _channel = "InlinePageReference";
@ -65,8 +64,11 @@ class InlinePageReferenceService extends InlineActionsDelegate {
_recentViewsInitialized = true;
final views =
(await _recentService.recentViews()).reversed.toSet().toList();
final views = (await _recentService.recentViews())
.reversed
.map((e) => e.item)
.toSet()
.toList();
// Filter by viewLayout
views.retainWhere(

View File

@ -253,9 +253,9 @@ class _ApplicationWidgetState extends State<ApplicationWidget> {
}
class AppGlobals {
// static GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey();
static GlobalKey<NavigatorState> rootNavKey = GlobalKey();
static NavigatorState get nav => rootNavKey.currentState!;
static BuildContext get context => rootNavKey.currentContext!;
}
class ApplicationBlocObserver extends BlocObserver {

View File

@ -40,17 +40,20 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> {
emit(
result.fold(
(favoriteViews) {
final views = favoriteViews.items.map((v) => v.item).toList();
final pinnedViews = views.where((v) => v.isPinned).toList();
final views = favoriteViews.items.toList();
final pinnedViews =
views.where((v) => v.item.isPinned).toList();
final unpinnedViews =
views.where((v) => !v.isPinned).toList();
views.where((v) => !v.item.isPinned).toList();
return state.copyWith(
isLoading: false,
views: views,
pinnedViews: pinnedViews,
unpinnedViews: unpinnedViews,
);
},
(error) => state.copyWith(
isLoading: false,
views: [],
),
),
@ -105,12 +108,11 @@ class FavoriteEvent with _$FavoriteEvent {
@freezed
class FavoriteState with _$FavoriteState {
const factory FavoriteState({
required List<ViewPB> views,
@Default([]) List<ViewPB> pinnedViews,
@Default([]) List<ViewPB> unpinnedViews,
@Default([]) List<SectionViewPB> views,
@Default([]) List<SectionViewPB> pinnedViews,
@Default([]) List<SectionViewPB> unpinnedViews,
@Default(true) bool isLoading,
}) = _FavoriteState;
factory FavoriteState.initial() => const FavoriteState(
views: [],
);
factory FavoriteState.initial() => const FavoriteState();
}

View File

@ -1,13 +1,12 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/recent_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:flutter/foundation.dart';
/// This is a lazy-singleton to share recent views across the application.
///
@ -23,21 +22,23 @@ class CachedRecentService {
Completer<void> _completer = Completer();
ValueNotifier<List<ViewPB>> notifier = ValueNotifier(const []);
ValueNotifier<List<SectionViewPB>> notifier = ValueNotifier(const []);
List<ViewPB> get _recentViews => notifier.value;
set _recentViews(List<ViewPB> value) => notifier.value = value;
List<SectionViewPB> get _recentViews => notifier.value;
set _recentViews(List<SectionViewPB> value) => notifier.value = value;
final _listener = RecentViewsListener();
Future<List<ViewPB>> recentViews() async {
Future<List<SectionViewPB>> recentViews() async {
if (_isInitialized) return _recentViews;
_isInitialized = true;
_listener.start(recentViewsUpdated: _recentViewsUpdated);
final result = await _readRecentViews();
_recentViews = result.toNullable()?.items ?? const [];
_recentViews = await _readRecentViews().fold(
(s) => s.items,
(_) => [],
);
_completer.complete();
return _recentViews;
@ -55,7 +56,7 @@ class CachedRecentService {
),
).send();
Future<FlowyResult<RepeatedViewPB, FlowyError>> _readRecentViews() =>
Future<FlowyResult<RepeatedRecentViewPB, FlowyError>> _readRecentViews() =>
FolderEventReadRecentViews().send();
bool _isInitialized = false;
@ -74,11 +75,12 @@ class CachedRecentService {
void _recentViewsUpdated(
FlowyResult<RepeatedViewIdPB, FlowyError> result,
) {
) async {
final viewIds = result.toNullable();
if (viewIds != null) {
_readRecentViews().then(
(views) => _recentViews = views.toNullable()?.items ?? const [],
_recentViews = await _readRecentViews().fold(
(s) => s.items,
(_) => [],
);
}
}

View File

@ -35,7 +35,12 @@ class RecentViewsBloc extends Bloc<RecentViewsEvent, RecentViewsState> {
await _service.updateRecentViews(e.viewIds, false);
},
fetchRecentViews: (e) async {
emit(state.copyWith(views: await _service.recentViews()));
emit(
state.copyWith(
isLoading: false,
views: await _service.recentViews(),
),
);
},
resetRecentViews: (e) async {
await _service.reset();
@ -63,8 +68,10 @@ class RecentViewsEvent with _$RecentViewsEvent {
@freezed
class RecentViewsState with _$RecentViewsState {
const factory RecentViewsState({required List<ViewPB> views}) =
_RecentViewsState;
const factory RecentViewsState({
required List<SectionViewPB> views,
@Default(true) bool isLoading,
}) = _RecentViewsState;
factory RecentViewsState.initial() => const RecentViewsState(views: []);
}

View File

@ -1,5 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/user/application/user_listener.dart';
@ -12,6 +10,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:protobuf/protobuf.dart';
@ -51,6 +50,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
'init workspace, current workspace: ${currentWorkspace?.workspaceId}, '
'workspaces: ${workspaces.map((e) => e.workspaceId)}, isCollabWorkspaceOn: $isCollabWorkspaceOn',
);
final members = await _fetchMembers(currentWorkspace?.workspaceId);
if (currentWorkspace != null && result.$3 == true) {
Log.info('init open workspace: ${currentWorkspace.workspaceId}');
await _userService.openWorkspace(currentWorkspace.workspaceId);
@ -61,6 +61,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
workspaces: workspaces,
isCollabWorkspaceOn: isCollabWorkspaceOn,
actionResult: null,
members: members,
),
);
},
@ -198,6 +199,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
),
(e) => state.currentWorkspace,
);
final members = await _fetchMembers(currentWorkspace?.workspaceId);
result
..onSuccess((s) {
@ -212,6 +214,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
emit(
state.copyWith(
currentWorkspace: currentWorkspace,
members: members,
actionResult: UserWorkspaceActionResult(
actionType: UserWorkspaceActionType.open,
isLoading: false,
@ -415,6 +418,17 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
..name = workspace.name
..createdAtTimestamp = workspace.createTime;
}
Future<List<WorkspaceMemberPB>> _fetchMembers(
String? workspaceId,
) async {
if (workspaceId == null) {
return [];
}
return _userService
.getWorkspaceMembers(workspaceId)
.fold((s) => s.items, (_) => []);
}
}
@freezed
@ -477,6 +491,7 @@ class UserWorkspaceState with _$UserWorkspaceState {
@Default([]) List<UserWorkspacePB> workspaces,
@Default(null) UserWorkspaceActionResult? actionResult,
@Default(false) bool isCollabWorkspaceOn,
@Default([]) List<WorkspaceMemberPB> members,
}) = _UserWorkspaceState;
factory UserWorkspaceState.initial() => const UserWorkspaceState();

View File

@ -41,7 +41,7 @@ extension ViewExtension on ViewPB {
Widget defaultIcon() => FlowySvg(
switch (layout) {
ViewLayoutPB.Board => FlowySvgs.board_s,
ViewLayoutPB.Calendar => FlowySvgs.date_s,
ViewLayoutPB.Calendar => FlowySvgs.calendar_s,
ViewLayoutPB.Grid => FlowySvgs.grid_s,
ViewLayoutPB.Document => FlowySvgs.document_s,
_ => FlowySvgs.document_s,

View File

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
@ -8,6 +6,7 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RecentViewsList extends StatelessWidget {
@ -24,7 +23,7 @@ class RecentViewsList extends StatelessWidget {
builder: (context, state) {
// We remove duplicates by converting the list to a set first
final List<ViewPB> recentViews =
state.views.reversed.toSet().toList();
state.views.reversed.map((e) => e.item).toSet().toList();
return ListView.separated(
shrinkWrap: true,

View File

@ -18,4 +18,10 @@ class HomeInsets {
class HomeSpaceViewSizes {
static const double leftPadding = 16.0;
static const double viewHeight = 30.0;
// mobile, m represents mobile
static const double mViewHeight = 48.0;
static const double mViewButtonDimension = 34.0;
static const double mHorizontalPadding = 20.0;
static const double mVerticalPadding = 12.0;
}

View File

@ -82,7 +82,12 @@ class _FavoriteFolderState extends State<FavoriteFolder> {
return [];
}
return context.read<FavoriteBloc>().state.pinnedViews.map(
return context
.read<FavoriteBloc>()
.state
.pinnedViews
.map((e) => e.item)
.map(
(view) => ViewItem(
key: ValueKey(
'${FolderSpaceType.favorite.name} ${view.id}',

View File

@ -38,7 +38,9 @@ class SidebarFolder extends StatelessWidget {
}
return Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 10),
child: FavoriteFolder(views: state.views),
child: FavoriteFolder(
views: state.views.map((e) => e.item).toList(),
),
);
},
),

View File

@ -20,7 +20,7 @@ class ViewFavoriteButton extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<FavoriteBloc, FavoriteState>(
builder: (context, state) {
final isFavorite = state.views.any((v) => v.id == view.id);
final isFavorite = state.views.any((v) => v.item.id == view.id);
return Listener(
onPointerDown: (_) =>
context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view)),

View File

@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:string_validator/string_validator.dart';
class UserAvatar extends StatelessWidget {
const UserAvatar({
@ -28,40 +29,79 @@ class UserAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (iconUrl.isEmpty) {
final String nameOrDefault = _userName(name);
final Color color = ColorGenerator(name).toColor();
const initialsCount = 2;
return _buildEmptyAvatar(context);
} else if (isURL(iconUrl)) {
return _buildUrlAvatar(context);
} else {
return _buildEmojiAvatar(context);
}
}
// Taking the first letters of the name components and limiting to 2 elements
final nameInitials = nameOrDefault
.split(' ')
.where((element) => element.isNotEmpty)
.take(initialsCount)
.map((element) => element[0].toUpperCase())
.join();
Widget _buildEmptyAvatar(BuildContext context) {
final String nameOrDefault = _userName(name);
final Color color = ColorGenerator(name).toColor();
const initialsCount = 2;
return Container(
width: size,
height: size,
alignment: Alignment.center,
// Taking the first letters of the name components and limiting to 2 elements
final nameInitials = nameOrDefault
.split(' ')
.where((element) => element.isNotEmpty)
.take(initialsCount)
.map((element) => element[0].toUpperCase())
.join();
return Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isHovering
? Border.all(
color: _darken(color),
width: 4,
)
: null,
),
child: FlowyText.regular(
nameInitials,
color: Colors.black,
fontSize: fontSize,
),
);
}
Widget _buildUrlAvatar(BuildContext context) {
return SizedBox.square(
dimension: size,
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isHovering
? Border.all(
color: _darken(color),
color: Theme.of(context).colorScheme.primary,
width: 4,
)
: null,
),
child: FlowyText.regular(
nameInitials,
color: Colors.black,
fontSize: fontSize,
child: ClipRRect(
borderRadius: Corners.s5Border,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: Image.network(
iconUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_buildEmptyAvatar(context),
),
),
),
);
}
),
);
}
Widget _buildEmojiAvatar(BuildContext context) {
return SizedBox.square(
dimension: size,
child: DecoratedBox(

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
const String _emojiFontFamily = 'noto color emoji';
@ -19,6 +20,7 @@ class FlowyText extends StatelessWidget {
final double? lineHeight;
final bool withTooltip;
final StrutStyle? strutStyle;
final bool isEmoji;
const FlowyText(
this.text, {
@ -35,6 +37,7 @@ class FlowyText extends StatelessWidget {
this.fallbackFontFamily,
this.lineHeight,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
});
@ -51,6 +54,7 @@ class FlowyText extends StatelessWidget {
this.fallbackFontFamily,
this.lineHeight,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
}) : fontWeight = FontWeight.w400,
fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12;
@ -69,6 +73,7 @@ class FlowyText extends StatelessWidget {
this.fallbackFontFamily,
this.lineHeight,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
}) : fontWeight = FontWeight.w400;
@ -86,6 +91,7 @@ class FlowyText extends StatelessWidget {
this.fallbackFontFamily,
this.lineHeight,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
}) : fontWeight = FontWeight.w500;
@ -103,6 +109,7 @@ class FlowyText extends StatelessWidget {
this.fallbackFontFamily,
this.lineHeight,
this.withTooltip = false,
this.isEmoji = false,
this.strutStyle,
}) : fontWeight = FontWeight.w600;
@ -120,15 +127,25 @@ class FlowyText extends StatelessWidget {
this.lineHeight,
this.withTooltip = false,
this.strutStyle = const StrutStyle(forceStrutHeight: true),
this.isEmoji = true,
this.fontFamily,
}) : fontWeight = FontWeight.w400,
fontFamily = _emojiFontFamily,
fallbackFontFamily = null;
@override
Widget build(BuildContext context) {
Widget child;
double fontSize =
var fontFamily = this.fontFamily;
var fallbackFontFamily = this.fallbackFontFamily;
if (isEmoji) {
fontFamily = _loadEmojiFontFamilyIfNeeded();
if (fontFamily != null && fallbackFontFamily == null) {
fallbackFontFamily = [fontFamily];
}
}
var fontSize =
this.fontSize ?? Theme.of(context).textTheme.bodyMedium!.fontSize!;
if (Platform.isLinux && fontFamily == _emojiFontFamily) {
fontSize = fontSize * 0.8;
@ -171,4 +188,12 @@ class FlowyText extends StatelessWidget {
return child;
}
String? _loadEmojiFontFamilyIfNeeded() {
if (Platform.isLinux || Platform.isAndroid) {
return GoogleFonts.notoColorEmoji().fontFamily;
}
return null;
}
}

View File

@ -20,6 +20,7 @@ dependencies:
loading_indicator: ^3.1.0
async:
url_launcher: ^6.1.11
google_fonts: ^6.1.0
# Federated Platform Interface
flowy_infra_ui_platform_interface:

View File

@ -104,6 +104,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
auto_size_text_field:
dependency: "direct main"
description:
name: auto_size_text_field
sha256: c4ba8714ba4216ca122acac1573581dac499f3162c9218a28b573dca73721b3f
url: "https://pub.dev"
source: hosted
version: "2.2.3"
avatar_stack:
dependency: "direct main"
description:
@ -1569,6 +1577,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
reorderable_tabbar:
dependency: "direct main"
description:
name: reorderable_tabbar
sha256: dd19d7b6f60f0dec4be02ba0a2c860f9acbe5a392cb8b5b8c1417cbfcbfe923f
url: "https://pub.dev"
source: hosted
version: "1.0.6"
reorderables:
dependency: "direct main"
description:
@ -1992,6 +2008,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0+1"
tab_indicator_styler:
dependency: transitive
description:
name: tab_indicator_styler
sha256: "9e7e90367e20f71f3882fc6578fdcced35ab1c66ab20fcb623cdcc20d2796c76"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
table_calendar:
dependency: "direct main"
description:

View File

@ -136,6 +136,8 @@ dependencies:
flutter_animate: ^4.5.0
permission_handler: ^11.3.1
scaled_app: ^2.3.0
auto_size_text_field: ^2.2.3
reorderable_tabbar: ^1.0.6
dev_dependencies:
flutter_lints: ^3.0.1

View File

@ -1,6 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.8 2H3.2C2.53726 2 2 2.55964 2 3.25V5.75C2 6.44036 2.53726 7 3.2 7H12.8C13.4627 7 14 6.44036 14 5.75V3.25C14 2.55964 13.4627 2 12.8 2Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.8 9H3.2C2.53726 9 2 9.55964 2 10.25V12.75C2 13.4404 2.53726 14 3.2 14H12.8C13.4627 14 14 13.4404 14 12.75V10.25C14 9.55964 13.4627 9 12.8 9Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4.5" cy="4.5" r="0.5" fill="#333333"/>
<circle cx="4.5" cy="11.5" r="0.5" fill="#333333"/>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.90313 14.7769V3.22312C7.90313 2.12625 7.43513 1.6875 6.27244 1.6875H3.31819C2.1555 1.6875 1.6875 2.12625 1.6875 3.22312V14.7769C1.6875 15.8737 2.1555 16.3125 3.31819 16.3125H6.27244C7.43513 16.3125 7.90313 15.8737 7.90313 14.7769Z" stroke="#171717" stroke-opacity="0.7" stroke-width="1.09687" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.3133 9.65812V3.22312C16.3133 2.12625 15.8453 1.6875 14.6826 1.6875H11.7283C10.5657 1.6875 10.0977 2.12625 10.0977 3.22312V9.65812C10.0977 10.755 10.5657 11.1937 11.7283 11.1937H14.6826C15.8453 11.1937 16.3133 10.755 16.3133 9.65812Z" stroke="#171717" stroke-opacity="0.7" stroke-width="1.09687" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 641 B

After

Width:  |  Height:  |  Size: 814 B

View File

@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.83008 1.125V3.5" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.1699 1.125V3.5" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.26758 6.73877H15.7259" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2.0625" y="2.4375" width="13.875" height="13.875" rx="2.4375" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125"/>
</svg>

After

Width:  |  Height:  |  Size: 721 B

View File

@ -1,3 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 2C3.67157 2 3 2.67157 3 3.5V12.5C3 13.3284 3.67157 14 4.5 14H11.5C12.3284 14 13 13.3284 13 12.5V7.16667C13 6.62574 12.8246 6.09941 12.5 5.66667L10.5 3C10.0279 2.37049 9.28689 2 8.5 2H4.5ZM4 3.5C4 3.22386 4.22386 3 4.5 3H8.5V6C8.5 6.82843 9.17157 7.5 10 7.5H12V12.5C12 12.7761 11.7761 13 11.5 13H4.5C4.22386 13 4 12.7761 4 12.5V3.5ZM11.8437 6.5C11.8032 6.41843 11.7552 6.34029 11.7 6.26667L9.7 3.6C9.64012 3.52016 9.57303 3.44726 9.5 3.38194V6C9.5 6.27614 9.72386 6.5 10 6.5H11.8437Z" fill="#333333"/>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.11523 4.5C3.11523 3.15381 4.20654 2.0625 5.55273 2.0625H9.96357C10.1512 2.0625 10.3292 2.14513 10.4504 2.28838L12.3569 4.5432L14.7021 7.6626C14.8242 7.82503 14.8902 8.02274 14.8902 8.22596V13.5C14.8902 14.8462 13.7989 15.9375 12.4527 15.9375H5.55274C4.20654 15.9375 3.11523 14.8462 3.11523 13.5V4.5Z" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125"/>
<path d="M10.0527 2.25V6.525C10.0527 7.35343 10.7243 8.025 11.5527 8.025H14.9277" stroke="#171717" stroke-opacity="0.7" stroke-width="1.05"/>
</svg>

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 619 B

View File

@ -0,0 +1,3 @@
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.4" cx="4" cy="4" r="1" fill="#171717"/>
</svg>

After

Width:  |  Height:  |  Size: 158 B

View File

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2H3C2.44772 2 2 2.44772 2 3V6C2 6.55228 2.44772 7 3 7H6C6.55228 7 7 6.55228 7 6V3C7 2.44772 6.55228 2 6 2Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 2H10C9.44772 2 9 2.44772 9 3V6C9 6.55228 9.44772 7 10 7H13C13.5523 7 14 6.55228 14 6V3C14 2.44772 13.5523 2 13 2Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 9H10C9.44772 9 9 9.44772 9 10V13C9 13.5523 9.44772 14 10 14H13C13.5523 14 14 13.5523 14 13V10C14 9.44772 13.5523 9 13 9Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 9H3C2.44772 9 2 9.44772 2 10V13C2 13.5523 2.44772 14 3 14H6C6.55228 14 7 13.5523 7 13V10C7 9.44772 6.55228 9 6 9Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.03571" y="2.03571" width="13.9286" height="13.9286" rx="2.32143" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143"/>
<path d="M6.64453 2.03564V15.9642" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.57227 6.50146H15.4294" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.57227 11.501H15.4294" stroke="#171717" stroke-opacity="0.7" stroke-width="1.07143" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 676 B

View File

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.96667 5.59993C9.23333 5.79993 9.23333 6.19993 8.96667 6.39993L4.3 9.89993C3.97038 10.1471 3.5 9.91195 3.5 9.49993L3.5 2.49993C3.5 2.08791 3.97038 1.85272 4.3 2.09993L8.96667 5.59993Z" fill="#8F959E"/>
</svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.39909 10.9667C8.19909 11.2333 7.79909 11.2333 7.59909 10.9667L4.09909 6.3C3.85188 5.97038 4.08707 5.5 4.49909 5.5L11.4991 5.5C11.9111 5.5 12.1463 5.97038 11.8991 6.3L8.39909 10.9667Z" fill="#8F959E"/>
</svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@ -0,0 +1,6 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<rect x="2.25" y="8.3252" width="13.5" height="1.35" rx="0.675" fill="#171717"/>
<rect x="8.32422" y="15.75" width="13.5" height="1.35" rx="0.675" transform="rotate(-90 8.32422 15.75)" fill="#171717"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.5">
<path d="M4.76211 9.0002C4.76211 9.68365 4.20806 10.2377 3.52461 10.2377C2.84116 10.2377 2.28711 9.68365 2.28711 9.0002C2.28711 8.31674 2.84116 7.7627 3.52461 7.7627C4.20806 7.7627 4.76211 8.31674 4.76211 9.0002Z" fill="#171717"/>
<path d="M10.2367 9.0002C10.2367 9.68365 9.68267 10.2377 8.99922 10.2377C8.31577 10.2377 7.76172 9.68365 7.76172 9.0002C7.76172 8.31674 8.31577 7.7627 8.99922 7.7627C9.68267 7.7627 10.2367 8.31674 10.2367 9.0002Z" fill="#171717"/>
<path d="M15.7113 9.0002C15.7113 9.68365 15.1573 10.2377 14.4738 10.2377C13.7904 10.2377 13.2363 9.68365 13.2363 9.0002C13.2363 8.31674 13.7904 7.7627 14.4738 7.7627C15.1573 7.7627 15.7113 8.31674 15.7113 9.0002Z" fill="#171717"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 819 B

View File

@ -0,0 +1,8 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-2-outside-1_136_3860" maskUnits="userSpaceOnUse" x="4" y="7" width="10" height="6" fill="black">
<rect fill="white" x="4" y="7" width="10" height="6"/>
<path d="M5.14645 7.64645C5.34171 7.45118 5.65829 7.45118 5.85355 7.64645L9 10.7929L12.1464 7.64645C12.3417 7.45118 12.6583 7.45118 12.8536 7.64645C13.0488 7.84171 13.0488 8.15829 12.8536 8.35355L9.35355 11.8536C9.25979 11.9473 9.13261 12 9 12C8.86739 12 8.74021 11.9473 8.64645 11.8536L5.14645 8.35355C4.95118 8.15829 4.95118 7.84171 5.14645 7.64645Z"/>
</mask>
<path d="M5.14645 7.64645C5.34171 7.45118 5.65829 7.45118 5.85355 7.64645L9 10.7929L12.1464 7.64645C12.3417 7.45118 12.6583 7.45118 12.8536 7.64645C13.0488 7.84171 13.0488 8.15829 12.8536 8.35355L9.35355 11.8536C9.25979 11.9473 9.13261 12 9 12C8.86739 12 8.74021 11.9473 8.64645 11.8536L5.14645 8.35355C4.95118 8.15829 4.95118 7.84171 5.14645 7.64645Z" fill="#171717"/>
<path d="M5.85355 7.64645L5.78284 7.71716H5.78284L5.85355 7.64645ZM5.14645 7.64645L5.21716 7.71716L5.14645 7.64645ZM9 10.7929L9.07071 10.8636C9.05196 10.8824 9.02652 10.8929 9 10.8929C8.97348 10.8929 8.94804 10.8824 8.92929 10.8636L9 10.7929ZM12.1464 7.64645L12.2172 7.71716L12.1464 7.64645ZM12.8536 7.64645L12.7828 7.71716L12.8536 7.64645ZM12.8536 8.35355L12.7828 8.28284L12.8536 8.35355ZM9.35355 11.8536L9.42426 11.9243V11.9243L9.35355 11.8536ZM8.64645 11.8536L8.71716 11.7828V11.7828L8.64645 11.8536ZM5.14645 8.35355L5.07574 8.42426H5.07574L5.14645 8.35355ZM5.78284 7.71716C5.62663 7.56095 5.37337 7.56095 5.21716 7.71716L5.07574 7.57574C5.31005 7.34142 5.68995 7.34142 5.92426 7.57574L5.78284 7.71716ZM8.92929 10.8636L5.78284 7.71716L5.92426 7.57574L9.07071 10.7222L8.92929 10.8636ZM12.2172 7.71716L9.07071 10.8636L8.92929 10.7222L12.0757 7.57574L12.2172 7.71716ZM12.7828 7.71716C12.6266 7.56095 12.3734 7.56095 12.2172 7.71716L12.0757 7.57574C12.3101 7.34142 12.6899 7.34142 12.9243 7.57574L12.7828 7.71716ZM12.7828 8.28284C12.9391 8.12663 12.9391 7.87337 12.7828 7.71716L12.9243 7.57574C13.1586 7.81005 13.1586 8.18995 12.9243 8.42426L12.7828 8.28284ZM9.28284 11.7828L12.7828 8.28284L12.9243 8.42426L9.42426 11.9243L9.28284 11.7828ZM9 11.9C9.10609 11.9 9.20783 11.8579 9.28284 11.7828L9.42426 11.9243C9.31174 12.0368 9.15913 12.1 9 12.1V11.9ZM8.71716 11.7828C8.79217 11.8579 8.89391 11.9 9 11.9V12.1C8.84087 12.1 8.68826 12.0368 8.57574 11.9243L8.71716 11.7828ZM5.21716 8.28284L8.71716 11.7828L8.57574 11.9243L5.07574 8.42426L5.21716 8.28284ZM5.21716 7.71716C5.06095 7.87337 5.06095 8.12663 5.21716 8.28284L5.07574 8.42426C4.84142 8.18995 4.84142 7.81005 5.07574 7.57574L5.21716 7.71716Z" fill="#171717" fill-opacity="0.8" mask="url(#path-2-outside-1_136_3860)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.93945 16.5C13.0645 16.5 16.4395 13.125 16.4395 9C16.4395 4.875 13.0645 1.5 8.93945 1.5C4.81445 1.5 1.43945 4.875 1.43945 9C1.43945 13.125 4.81445 16.5 8.93945 16.5Z" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.93945 9H11.9395" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -0,0 +1,4 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5013 18.3334C15.1037 18.3334 18.8346 14.6025 18.8346 10.0001C18.8346 5.39771 15.1037 1.66675 10.5013 1.66675C5.89893 1.66675 2.16797 5.39771 2.16797 10.0001C2.16797 14.6025 5.89893 18.3334 10.5013 18.3334Z" stroke="white" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.75 10.625L9.58883 13.4524L15.0833 7.5" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1,5 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.333 22C17.833 22 22.333 17.5 22.333 12C22.333 6.5 17.833 2 12.333 2C6.83301 2 2.33301 6.5 2.33301 12C2.33301 17.5 6.83301 22 12.333 22Z" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.33301 12H16.333" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.333 16V8" stroke="#171717" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 560 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0196 2.91016C8.7096 2.91016 6.0196 5.60016 6.0196 8.91016V11.8002C6.0196 12.4102 5.7596 13.3402 5.4496 13.8602L4.2996 15.7702C3.5896 16.9502 4.0796 18.2602 5.3796 18.7002C9.6896 20.1402 14.3396 20.1402 18.6496 18.7002C19.8596 18.3002 20.3896 16.8702 19.7296 15.7702L18.5796 13.8602C18.2796 13.3402 18.0196 12.4102 18.0196 11.8002V8.91016C18.0196 5.61016 15.3196 2.91016 12.0196 2.91016Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M13.8699 3.19994C13.5599 3.10994 13.2399 3.03994 12.9099 2.99994C11.9499 2.87994 11.0299 2.94994 10.1699 3.19994C10.4599 2.45994 11.1799 1.93994 12.0199 1.93994C12.8599 1.93994 13.5799 2.45994 13.8699 3.19994Z" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.0195 19.0601C15.0195 20.7101 13.6695 22.0601 12.0195 22.0601C11.1995 22.0601 10.4395 21.7201 9.89953 21.1801C9.35953 20.6401 9.01953 19.8801 9.01953 19.0601" stroke="#171717" stroke-width="1.5" stroke-miterlimit="10"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.24 2H5.34C3.15 2 2 3.15 2 5.33V7.23C2 9.41 3.15 10.56 5.33 10.56H7.23C9.41 10.56 10.56 9.41 10.56 7.23V5.33C10.57 3.15 9.42 2 7.24 2Z" fill="#00C8FF"/>
<path d="M18.6704 2H16.7704C14.5904 2 13.4404 3.15 13.4404 5.33V7.23C13.4404 9.41 14.5904 10.56 16.7704 10.56H18.6704C20.8504 10.56 22.0004 9.41 22.0004 7.23V5.33C22.0004 3.15 20.8504 2 18.6704 2Z" fill="#00C8FF"/>
<path d="M18.6704 13.4302H16.7704C14.5904 13.4302 13.4404 14.5802 13.4404 16.7602V18.6602C13.4404 20.8402 14.5904 21.9902 16.7704 21.9902H18.6704C20.8504 21.9902 22.0004 20.8402 22.0004 18.6602V16.7602C22.0004 14.5802 20.8504 13.4302 18.6704 13.4302Z" fill="#00C8FF"/>
<path d="M7.24 13.4302H5.34C3.15 13.4302 2 14.5802 2 16.7602V18.6602C2 20.8502 3.15 22.0002 5.33 22.0002H7.23C9.41 22.0002 10.56 20.8502 10.56 18.6702V16.7702C10.57 14.5802 9.42 13.4302 7.24 13.4302Z" fill="#00C8FF"/>
</svg>

After

Width:  |  Height:  |  Size: 969 B

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.06 5.32771V5.33V7.23C10.06 8.23613 9.79557 8.92232 9.35895 9.35895C8.92232 9.79557 8.23613 10.06 7.23 10.06H5.33C4.32387 10.06 3.63768 9.79557 3.20105 9.35895C2.76443 8.92232 2.5 8.23613 2.5 7.23V5.33C2.5 4.32387 2.76441 3.63797 3.20191 3.20145C3.63959 2.76474 4.32828 2.5 5.34 2.5H7.24C8.24631 2.5 8.93213 2.76451 9.3673 3.20066C9.80228 3.63661 10.0646 4.322 10.06 5.32771Z" stroke="#191919"/>
<path d="M18.6704 2H16.7704C14.5904 2 13.4404 3.15 13.4404 5.33V7.23C13.4404 9.41 14.5904 10.56 16.7704 10.56H18.6704C20.8504 10.56 22.0004 9.41 22.0004 7.23V5.33C22.0004 3.15 20.8504 2 18.6704 2Z" fill="#191919"/>
<path d="M18.6704 13.4302H16.7704C14.5904 13.4302 13.4404 14.5802 13.4404 16.7602V18.6602C13.4404 20.8402 14.5904 21.9902 16.7704 21.9902H18.6704C20.8504 21.9902 22.0004 20.8402 22.0004 18.6602V16.7602C22.0004 14.5802 20.8504 13.4302 18.6704 13.4302Z" fill="#191919"/>
<path d="M7.24 13.4302H5.34C3.15 13.4302 2 14.5802 2 16.7602V18.6602C2 20.8502 3.15 22.0002 5.33 22.0002H7.23C9.41 22.0002 10.56 20.8502 10.56 18.6702V16.7702C10.57 14.5802 9.42 13.4302 7.24 13.4302Z" fill="#191919"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,3 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.96704 2.45522C7.54904 2.90522 6.32605 3.63923 5.24805 4.64323C4.91505 4.95323 4.81305 5.46123 5.02905 5.86123C5.83005 7.34223 4.99204 8.92723 3.18504 9.01823C2.74304 9.04023 2.35205 9.36823 2.24805 9.79923C2.06905 10.5442 1.99805 11.1682 1.99805 11.9862C1.99805 12.6732 2.07204 13.4512 2.21704 14.1432C2.30704 14.5752 2.68305 14.8862 3.12305 14.9242C4.94105 15.0812 5.84105 16.4682 5.02905 18.2362C4.84905 18.6292 4.93105 19.0992 5.24805 19.3932C6.31005 20.3752 7.53004 21.0682 8.96704 21.5182C9.37704 21.6462 9.84004 21.4912 10.092 21.1432C11.204 19.6042 12.817 19.5992 13.873 21.1432C14.122 21.5062 14.578 21.6812 14.998 21.5492C16.385 21.1122 17.677 20.3692 18.748 19.3932C19.078 19.0922 19.165 18.6052 18.967 18.2052C18.135 16.5262 19.0921 14.9852 20.8101 14.9552C21.2661 14.9472 21.6721 14.6482 21.7791 14.2052C21.9521 13.4882 21.998 12.8642 21.998 11.9862C21.998 11.2322 21.909 10.4892 21.748 9.76823C21.646 9.31123 21.2471 8.98723 20.7791 8.98623C19.0881 8.98323 18.14 7.32123 18.967 5.86123C19.197 5.45523 19.1251 4.95723 18.7791 4.64323C17.6891 3.65323 16.36 2.87922 14.967 2.45522C14.539 2.32522 14.084 2.48523 13.842 2.86123C12.876 4.36223 11.072 4.38823 10.123 2.89323C9.88005 2.50923 9.39904 2.31822 8.96704 2.45522ZM15.045 4.57923C15.728 4.86523 16.267 5.16423 16.886 5.63323C16.16 7.93023 17.391 10.3162 19.941 10.8972C20.004 11.3102 19.998 11.5602 19.998 11.9862C19.998 12.4952 20.0051 12.6742 19.9471 13.0422C17.4081 13.5682 16.152 15.8872 16.859 18.3382C16.251 18.7792 15.816 19.0852 15.053 19.3802C13.261 17.5562 10.7731 17.4762 8.94305 19.3922C8.22905 19.0782 7.68006 18.7992 7.12506 18.3302C7.81306 15.8412 6.65005 13.6922 4.06805 13.0402C3.95305 12.5842 3.99905 11.2962 4.06505 10.9052C6.73505 10.2652 7.78705 7.90223 7.12405 5.62623C7.71005 5.18523 8.23706 4.86423 8.92206 4.58623C10.6471 6.34123 13.237 6.51623 15.045 4.57923ZM11.998 7.98623C9.78905 7.98623 7.99805 9.77723 7.99805 11.9862C7.99805 14.1962 9.78905 15.9862 11.998 15.9862C14.207 15.9862 15.998 14.1962 15.998 11.9862C15.998 9.77723 14.207 7.98623 11.998 7.98623ZM11.998 9.98623C13.103 9.98623 13.998 10.8822 13.998 11.9862C13.998 13.0912 13.103 13.9862 11.998 13.9862C10.893 13.9862 9.99805 13.0912 9.99805 11.9862C9.99805 10.8822 10.893 9.98623 11.998 9.98623Z" fill="#2F3030"/>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.7">
<path d="M10 12.5C11.3807 12.5 12.5 11.3807 12.5 10C12.5 8.61929 11.3807 7.5 10 7.5C8.61929 7.5 7.5 8.61929 7.5 10C7.5 11.3807 8.61929 12.5 10 12.5Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.66602 10.7334V9.26669C1.66602 8.40003 2.37435 7.68336 3.24935 7.68336C4.75768 7.68336 5.37435 6.6167 4.61602 5.30836C4.18268 4.55836 4.44102 3.58336 5.19935 3.15003L6.64102 2.32503C7.29935 1.93336 8.14935 2.1667 8.54102 2.82503L8.63268 2.98336C9.38268 4.2917 10.616 4.2917 11.3743 2.98336L11.466 2.82503C11.8577 2.1667 12.7077 1.93336 13.366 2.32503L14.8077 3.15003C15.566 3.58336 15.8243 4.55836 15.391 5.30836C14.6327 6.6167 15.2493 7.68336 16.7577 7.68336C17.6243 7.68336 18.341 8.39169 18.341 9.26669V10.7334C18.341 11.6 17.6327 12.3167 16.7577 12.3167C15.2493 12.3167 14.6327 13.3834 15.391 14.6917C15.8243 15.45 15.566 16.4167 14.8077 16.85L13.366 17.675C12.7077 18.0667 11.8577 17.8334 11.466 17.175L11.3743 17.0167C10.6243 15.7084 9.39102 15.7084 8.63268 17.0167L8.54102 17.175C8.14935 17.8334 7.29935 18.0667 6.64102 17.675L5.19935 16.85C4.44102 16.4167 4.18268 15.4417 4.61602 14.6917C5.37435 13.3834 4.75768 12.3167 3.24935 12.3167C2.37435 12.3167 1.66602 11.6 1.66602 10.7334Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,6 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.4">
<path d="M18 44H30C40 44 44 40 44 30V18C44 8 40 4 30 4H18C8 4 4 8 4 18V30C4 40 8 44 18 44Z" stroke="#171717" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 26.0005H11.52C13.04 26.0005 14.42 26.8605 15.1 28.2205L16.88 31.8005C18 34.0005 20 34.0005 20.48 34.0005H27.54C29.06 34.0005 30.44 33.1405 31.12 31.7805L32.9 28.2005C33.58 26.8405 34.96 25.9805 36.48 25.9805H43.96" stroke="#171717" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 611 B

View File

@ -245,7 +245,20 @@
"recent": "Recent",
"today": "Today",
"thisWeek": "This week",
"others": "Others"
"others": "Others",
"justNow": "just now",
"minutesAgo": "{count} minutes ago",
"lastViewed": "Last viewed",
"favoriteAt": "Favorited at",
"emptyRecent": "No Recent Documents",
"emptyRecentDescription": "As you view documents, they will appear here for easy retrieval",
"emptyFavorite": "No Favorite Documents",
"emptyFavoriteDescription": "Start exploring and mark documents as favorites. Theyll be listed here for quick access!",
"removePageFromRecent": "Remove this page from the Recent?",
"removeSuccess": "Removed successfully",
"favoriteSpace": "Favorites",
"RecentSpace": "Recent",
"Spaces": "Spaces"
},
"notifications": {
"export": {
@ -284,6 +297,7 @@
"update": "Update",
"share": "Share",
"removeFromFavorites": "Remove from favorites",
"removeFromRecent": "Remove from recent",
"addToFavorites": "Add to favorites",
"rename": "Rename",
"helpCenter": "Help Center",

View File

@ -158,7 +158,9 @@ impl FolderTest {
assert_eq!(self.child_view, view, "View not equal");
},
FolderScript::ReadView(view_id) => {
let view = read_view(sdk, &view_id).await;
let mut view = read_view(sdk, &view_id).await;
// Ignore the last edited time
view.last_edited = 0;
self.child_view = view;
},
FolderScript::UpdateView {

View File

@ -60,6 +60,18 @@ pub struct ViewPB {
#[pb(index = 9, one_of)]
pub extra: Option<String>,
// user_id
#[pb(index = 10, one_of)]
pub created_by: Option<i64>,
// timestamp
#[pb(index = 11)]
pub last_edited: i64,
// user_id
#[pb(index = 12, one_of)]
pub last_edited_by: Option<i64>,
}
pub fn view_pb_without_child_views(view: View) -> ViewPB {
@ -73,6 +85,9 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB {
icon: view.icon.clone().map(|icon| icon.into()),
is_favorite: view.is_favorite,
extra: view.extra,
created_by: view.created_by,
last_edited: view.last_edited_time,
last_edited_by: view.last_edited_by,
}
}
@ -87,6 +102,9 @@ pub fn view_pb_without_child_views_from_arc(view: Arc<View>) -> ViewPB {
icon: view.icon.clone().map(|icon| icon.into()),
is_favorite: view.is_favorite,
extra: view.extra.clone(),
created_by: view.created_by,
last_edited: view.last_edited_time,
last_edited_by: view.last_edited_by,
}
}
@ -105,6 +123,9 @@ pub fn view_pb_with_child_views(view: Arc<View>, child_views: Vec<Arc<View>>) ->
icon: view.icon.clone().map(|icon| icon.into()),
is_favorite: view.is_favorite,
extra: view.extra.clone(),
created_by: view.created_by,
last_edited: view.last_edited_time,
last_edited_by: view.last_edited_by,
}
}
@ -155,11 +176,17 @@ pub struct RepeatedViewPB {
#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)]
pub struct RepeatedFavoriteViewPB {
#[pb(index = 1)]
pub items: Vec<FavoriteViewPB>,
pub items: Vec<SectionViewPB>,
}
#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)]
pub struct FavoriteViewPB {
pub struct RepeatedRecentViewPB {
#[pb(index = 1)]
pub items: Vec<SectionViewPB>,
}
#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)]
pub struct SectionViewPB {
#[pb(index = 1)]
pub item: ViewPB,
#[pb(index = 2)]

View File

@ -284,7 +284,7 @@ pub(crate) async fn read_favorites_handler(
let mut views = vec![];
for item in favorite_items {
if let Ok(view) = folder.get_view_pb(&item.id).await {
views.push(FavoriteViewPB {
views.push(SectionViewPB {
item: view,
timestamp: item.timestamp,
});
@ -296,16 +296,19 @@ pub(crate) async fn read_favorites_handler(
#[tracing::instrument(level = "debug", skip(folder), err)]
pub(crate) async fn read_recent_views_handler(
folder: AFPluginState<Weak<FolderManager>>,
) -> DataResult<RepeatedViewPB, FlowyError> {
) -> DataResult<RepeatedRecentViewPB, FlowyError> {
let folder = upgrade_folder(folder)?;
let recent_items = folder.get_my_recent_sections().await;
let mut views = vec![];
for item in recent_items {
if let Ok(view) = folder.get_view_pb(&item.id).await {
views.push(view);
views.push(SectionViewPB {
item: view,
timestamp: item.timestamp,
});
}
}
data_result_ok(RepeatedViewPB { items: views })
data_result_ok(RepeatedRecentViewPB { items: views })
}
#[tracing::instrument(level = "debug", skip(folder), err)]

View File

@ -155,7 +155,7 @@ pub enum FolderEvent {
#[event(input = "UpdateViewIconPayloadPB")]
UpdateViewIcon = 35,
#[event(output = "RepeatedViewPB")]
#[event(output = "RepeatedRecentViewPB")]
ReadRecentViews = 36,
// used for add or remove recent views, like history