From cbe452a73d926d016b6d69dc00116e555c8df4e5 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 14 Jun 2024 13:15:41 +0800 Subject: [PATCH] feat: improve page thumbnail on mobile (#5535) * feat: improve view thumbnail * chore: blur the bottom navigation bar * feat: improve date format * feat: support url avatar * fix: remove duplicated divider --- .../bottom_sheet_block_action_widget.dart | 3 +- .../home/favorite_folder/favorite_space.dart | 7 +- .../home/home_space/home_space.dart | 9 ++- .../presentation/home/mobile_home_page.dart | 1 + .../home/recent_folder/recent_space.dart | 13 ++- .../home/shared/mobile_view_card.dart | 81 +++++++++++++++---- .../presentation/home/tab/_tab_bar.dart | 4 +- .../mobile_bottom_navigation_bar.dart | 49 +++++++---- .../collaborator_avater_stack.dart | 9 +-- .../presentation/document_collaborators.dart | 74 +++++++++++++---- .../resources/flowy_icons/16x/m_layout.svg | 7 +- .../flowy_icons/24x/m_board_thumbnail.svg | 5 ++ .../flowy_icons/24x/m_calendar_thumbnail.svg | 3 + .../flowy_icons/24x/m_chat_thumbnail.svg | 14 ++++ .../flowy_icons/24x/m_document_thumbnail.svg | 3 + .../flowy_icons/24x/m_grid_thumbnail.svg | 3 + 16 files changed, 219 insertions(+), 66 deletions(-) create mode 100644 frontend/resources/flowy_icons/24x/m_board_thumbnail.svg create mode 100644 frontend/resources/flowy_icons/24x/m_calendar_thumbnail.svg create mode 100644 frontend/resources/flowy_icons/24x/m_chat_thumbnail.svg create mode 100644 frontend/resources/flowy_icons/24x/m_document_thumbnail.svg create mode 100644 frontend/resources/flowy_icons/24x/m_grid_thumbnail.svg diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart index 629dfd2675..7f7abdfab8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart @@ -33,6 +33,7 @@ class BlockActionBottomSheet extends StatelessWidget { FlowySvgs.arrow_up_s, size: Size.square(20), ), + showTopBorder: false, onTap: () => onAction(BlockActionBottomSheetType.insertAbove), ), FlowyOptionTile.text( @@ -48,7 +49,7 @@ class BlockActionBottomSheet extends StatelessWidget { FlowyOptionTile.text( showTopBorder: false, text: LocaleKeys.button_duplicate.tr(), - leftIcon: const FlowySvg(FlowySvgs.m_field_copy_s), + leftIcon: const FlowySvg(FlowySvgs.m_duplicate_s), onTap: () => onAction(BlockActionBottomSheetType.duplicate), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart index 684442a74a..86d445a3f5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart @@ -95,8 +95,11 @@ class _FavoriteViews extends StatelessWidget { return Scrollbar( child: ListView.separated( key: const PageStorageKey('favorite_views_page_storage_key'), - padding: const EdgeInsets.symmetric( - horizontal: HomeSpaceViewSizes.mHorizontalPadding, + padding: EdgeInsets.only( + left: HomeSpaceViewSizes.mHorizontalPadding, + right: HomeSpaceViewSizes.mHorizontalPadding, + bottom: HomeSpaceViewSizes.mVerticalPadding + + MediaQuery.of(context).padding.bottom, ), itemBuilder: (context, index) { final view = favoriteViews[index]; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart index 6ba27a6766..965c396d42 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart @@ -28,9 +28,12 @@ class _MobileHomeSpaceState extends State return Scrollbar( child: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: HomeSpaceViewSizes.mHorizontalPadding, - vertical: HomeSpaceViewSizes.mVerticalPadding, + padding: EdgeInsets.only( + left: HomeSpaceViewSizes.mHorizontalPadding, + right: HomeSpaceViewSizes.mHorizontalPadding, + top: HomeSpaceViewSizes.mVerticalPadding, + bottom: HomeSpaceViewSizes.mVerticalPadding + + MediaQuery.of(context).padding.bottom, ), child: MobileFolders( user: widget.userProfile, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index d01fada530..1f3ec66dec 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -57,6 +57,7 @@ class MobileHomeScreen extends StatelessWidget { return Scaffold( body: SafeArea( + bottom: false, child: Provider.value( value: userProfile, child: MobileHomePage( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart index 3523d75720..5fa3b6e27f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart @@ -1,5 +1,6 @@ import 'package:appflowy/mobile/presentation/home/shared/empty_placeholder.dart'; import 'package:appflowy/mobile/presentation/home/shared/mobile_view_card.dart'; +import 'package:appflowy/util/theme_extension.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'; @@ -63,12 +64,18 @@ class _RecentViews extends StatelessWidget { @override Widget build(BuildContext context) { + final borderColor = Theme.of(context).isLightMode + ? const Color(0x00e2e2e4) + : const Color(0x1AFFFFFF); return SlidableAutoCloseBehavior( child: Scrollbar( child: ListView.separated( key: const PageStorageKey('recent_views_page_storage_key'), - padding: const EdgeInsets.symmetric( - horizontal: HomeSpaceViewSizes.mHorizontalPadding, + padding: EdgeInsets.only( + left: HomeSpaceViewSizes.mHorizontalPadding, + right: HomeSpaceViewSizes.mHorizontalPadding, + bottom: HomeSpaceViewSizes.mVerticalPadding + + MediaQuery.of(context).padding.bottom, ), itemBuilder: (context, index) { final sectionView = recentViews[index]; @@ -77,7 +84,7 @@ class _RecentViews extends StatelessWidget { decoration: BoxDecoration( border: Border( bottom: BorderSide( - color: Theme.of(context).dividerColor, + color: borderColor, width: 0.5, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart index 58f3963ddb..584bd381ed 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_view_card.dart @@ -10,6 +10,10 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da 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/util/theme_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; +import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -23,6 +27,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'package:string_validator/string_validator.dart'; +import 'package:time/time.dart'; enum MobileViewCardType { recent, @@ -153,6 +158,7 @@ class MobileViewCard extends StatelessWidget { return ClipRRect( borderRadius: BorderRadius.circular(8), child: _ViewCover( + layout: view.layout, coverTypeV1: state.coverTypeV1, coverTypeV2: state.coverTypeV2, value: state.coverValue, @@ -185,6 +191,7 @@ class MobileViewCard extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: 16.0, fontWeight: FontWeight.w600, + height: 1.3, ), ), ], @@ -203,40 +210,45 @@ class MobileViewCard extends StatelessWidget { } Widget _buildLastViewed(BuildContext context) { + final textColor = Theme.of(context).isLightMode + ? const Color(0xFF171717) + : Colors.white.withOpacity(0.45); if (timestamp == null) { return const SizedBox.shrink(); } final date = _formatTimestamp( + context, timestamp!.toInt() * 1000, ); return FlowyText.regular( date, - fontSize: 12.0, - color: Theme.of(context).hintColor, + fontSize: 13.0, + color: textColor, ); } - String _formatTimestamp(int timestamp) { + String _formatTimestamp(BuildContext context, int timestamp) { final now = DateTime.now(); final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); final difference = now.difference(dateTime); final String date; + final dateFormate = + context.read().state.dateFormat; + final timeFormate = + context.read().state.timeFormat; + if (difference.inMinutes < 1) { date = LocaleKeys.sideBar_justNow.tr(); - } else if (difference.inHours < 1) { + } else if (difference.inHours < 1 && dateTime.isToday) { // 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 if (difference.inHours >= 1 && dateTime.isToday) { + // in same day + date = timeFormate.formatTime(dateTime); } else { - // Other cases (previous years) - date = DateFormat('M/d/yyyy, h:mm a').format(dateTime); + date = dateFormate.formatDate(dateTime, false); } if (difference.inHours >= 1) { @@ -249,20 +261,20 @@ class MobileViewCard extends StatelessWidget { class _ViewCover extends StatelessWidget { const _ViewCover({ + required this.layout, required this.coverTypeV1, this.coverTypeV2, this.value, }); + final ViewLayoutPB layout; final CoverType coverTypeV1; final PageStyleCoverImageType? coverTypeV2; final String? value; @override Widget build(BuildContext context) { - final placeholder = Container( - color: const Color(0xFFE1FBFF), - ); + final placeholder = _buildPlaceholder(context); final value = this.value; if (value == null) { return placeholder; @@ -273,6 +285,45 @@ class _ViewCover extends StatelessWidget { return _buildCoverV1(context, value, placeholder); } + Widget _buildPlaceholder(BuildContext context) { + final isLightMode = Theme.of(context).isLightMode; + final (svg, color) = switch (layout) { + ViewLayoutPB.Document => ( + FlowySvgs.m_document_thumbnail_m, + isLightMode ? const Color(0xCCEDFBFF) : const Color(0x33658B90) + ), + ViewLayoutPB.Grid => ( + FlowySvgs.m_grid_thumbnail_m, + isLightMode ? const Color(0xFFF5F4FF) : const Color(0x338B80AD) + ), + ViewLayoutPB.Board => ( + FlowySvgs.m_board_thumbnail_m, + isLightMode ? const Color(0x7FE0FDD9) : const Color(0x3372936B), + ), + ViewLayoutPB.Calendar => ( + FlowySvgs.m_calendar_thumbnail_m, + isLightMode ? const Color(0xFFFFF7F0) : const Color(0x33A68B77) + ), + ViewLayoutPB.Chat => ( + FlowySvgs.m_chat_thumbnail_m, + isLightMode ? const Color(0x66FFE6FD) : const Color(0x33987195) + ), + _ => ( + FlowySvgs.m_document_thumbnail_m, + isLightMode ? Colors.black : Colors.white + ) + }; + return ColoredBox( + color: color, + child: Center( + child: FlowySvg( + svg, + blendMode: null, + ), + ), + ); + } + Widget _buildCoverV2(BuildContext context, String value, Widget placeholder) { final type = coverTypeV2; if (type == null) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart index adf13ed667..5602f46f89 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart @@ -21,8 +21,8 @@ class MobileSpaceTabBar extends StatelessWidget { Widget build(BuildContext context) { final baseStyle = Theme.of(context).textTheme.bodyMedium; final labelStyle = baseStyle?.copyWith( - fontWeight: FontWeight.w500, - fontSize: 15.0, + fontWeight: FontWeight.w600, + fontSize: 16.0, ); final unselectedLabelStyle = baseStyle?.copyWith( fontWeight: FontWeight.w400, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart index 411623cb87..600d65ad13 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart @@ -1,7 +1,10 @@ +import 'dart:ui'; + 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/util/theme_extension.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; @@ -46,22 +49,40 @@ class MobileBottomNavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { + final isLightMode = Theme.of(context).isLightMode; + final backgroundColor = isLightMode + ? Colors.white.withOpacity(0.95) + : const Color(0x0023262b).withOpacity(0.95); return Scaffold( body: navigationShell, - bottomNavigationBar: Theme( - data: Theme.of(context).copyWith( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - child: BottomNavigationBar( - showSelectedLabels: false, - showUnselectedLabels: false, - enableFeedback: false, - type: BottomNavigationBarType.fixed, - elevation: 0, - items: _items, - currentIndex: navigationShell.currentIndex, - onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex), + extendBody: true, + bottomNavigationBar: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 2, + sigmaY: 2, + ), + child: DecoratedBox( + decoration: BoxDecoration( + border: isLightMode + ? Border( + top: BorderSide(color: Theme.of(context).dividerColor), + ) + : null, + color: backgroundColor, + ), + child: BottomNavigationBar( + showSelectedLabels: false, + showUnselectedLabels: false, + enableFeedback: false, + type: BottomNavigationBarType.fixed, + elevation: 0, + items: _items, + backgroundColor: Colors.transparent, + currentIndex: navigationShell.currentIndex, + onTap: (int bottomBarIndex) => _onTap(context, bottomBarIndex), + ), + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart index 72913a68ed..8fa15af8b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:avatar_stack/avatar_stack.dart'; import 'package:avatar_stack/positions.dart'; +import 'package:flutter/material.dart'; class CollaboratorAvatarStack extends StatelessWidget { const CollaboratorAvatarStack({ @@ -31,14 +30,14 @@ class CollaboratorAvatarStack extends StatelessWidget { Widget build(BuildContext context) { final settings = this.settings ?? RestrictedPositions( - maxCoverage: 0.3, - minCoverage: 0.2, + maxCoverage: 0.4, + minCoverage: 0.3, align: StackAlign.right, laying: StackLaying.first, ); final border = BorderSide( - color: borderColor ?? Theme.of(context).colorScheme.onPrimary, + color: borderColor ?? Theme.of(context).dividerColor, width: borderWidth ?? 2.0, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart index 9da14f7b3a..1e96e5648b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart'; import 'package:appflowy/plugins/document/application/document_collaborators_bloc.dart'; import 'package:appflowy/plugins/document/presentation/collaborator_avater_stack.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -7,6 +8,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:string_validator/string_validator.dart'; class DocumentCollaborators extends StatelessWidget { const DocumentCollaborators({ @@ -66,21 +68,11 @@ class DocumentCollaborators extends StatelessWidget { ), ); }, - avatars: collaborators - .map( - (c) => FlowyTooltip( - message: c.userName, - child: CircleAvatar( - backgroundColor: c.cursorColor.tryToColor(), - child: FlowyText( - c.userName.characters.firstOrNull ?? ' ', - fontSize: fontSize, - color: Colors.black, - ), - ), - ), - ) - .toList(), + avatars: [ + ...collaborators.map( + (c) => _UserAvatar(fontSize: fontSize, user: c, width: width), + ), + ], ), ); }, @@ -88,3 +80,55 @@ class DocumentCollaborators extends StatelessWidget { ); } } + +class _UserAvatar extends StatelessWidget { + const _UserAvatar({ + this.fontSize, + required this.user, + required this.width, + }); + + final DocumentAwarenessMetadata user; + final double? fontSize; + final double width; + + @override + Widget build(BuildContext context) { + final Widget child; + if (isURL(user.userAvatar)) { + child = _buildUrlAvatar(context); + } else { + child = _buildNameAvatar(context); + } + return FlowyTooltip( + message: user.userName, + child: child, + ); + } + + Widget _buildNameAvatar(BuildContext context) { + return CircleAvatar( + backgroundColor: user.cursorColor.tryToColor(), + child: FlowyText( + user.userName.characters.firstOrNull ?? ' ', + fontSize: fontSize, + color: Colors.black, + ), + ); + } + + Widget _buildUrlAvatar(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(width), + child: CircleAvatar( + backgroundColor: user.cursorColor.tryToColor(), + child: Image.network( + user.userAvatar, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildNameAvatar(context), + ), + ), + ); + } +} diff --git a/frontend/resources/flowy_icons/16x/m_layout.svg b/frontend/resources/flowy_icons/16x/m_layout.svg index 7d9ba0d9dd..258e5f7288 100644 --- a/frontend/resources/flowy_icons/16x/m_layout.svg +++ b/frontend/resources/flowy_icons/16x/m_layout.svg @@ -1,8 +1,3 @@ - - - - - - + diff --git a/frontend/resources/flowy_icons/24x/m_board_thumbnail.svg b/frontend/resources/flowy_icons/24x/m_board_thumbnail.svg new file mode 100644 index 0000000000..ecfec44bbf --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_board_thumbnail.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_calendar_thumbnail.svg b/frontend/resources/flowy_icons/24x/m_calendar_thumbnail.svg new file mode 100644 index 0000000000..128f7b8377 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_calendar_thumbnail.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_chat_thumbnail.svg b/frontend/resources/flowy_icons/24x/m_chat_thumbnail.svg new file mode 100644 index 0000000000..f0ff36290a --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_chat_thumbnail.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_document_thumbnail.svg b/frontend/resources/flowy_icons/24x/m_document_thumbnail.svg new file mode 100644 index 0000000000..bf4ce416d9 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_document_thumbnail.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_grid_thumbnail.svg b/frontend/resources/flowy_icons/24x/m_grid_thumbnail.svg new file mode 100644 index 0000000000..36f9f1196c --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_grid_thumbnail.svg @@ -0,0 +1,3 @@ + + +