fix: sidebar issues (#5444)

* fix: cannot open the view in favorite menu

* fix: pinned docs should also show up in the search list

* fix: the search result doesn't update after deleting query

* fix: remove widget button

* fix: translation

* fix: replace icon selector

* fix: translations

* fix: recent page title sycn slowly

* fix: unable to favorite database on mobile

* fix: auto focus on search textfield when opening favorite menu

* feat: extend the expand icon hit test area

* fix: mobile tab bar dark mode issue

* fix: keep tab views alive

* chore: update frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>

---------

Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com>
This commit is contained in:
Lucas.Xu 2024-05-31 10:34:23 +08:00 committed by GitHub
parent 8b04506ac4
commit 2c0cdfa6c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 332 additions and 245 deletions

View File

@ -57,7 +57,19 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
} }
}, },
); );
// only document supports the cover
if (view.layout != ViewLayoutPB.Document) {
emit(
state.copyWith(
name: view.name,
icon: view.icon.value,
),
);
}
final cover = getCoverV2(); final cover = getCoverV2();
if (cover != null) { if (cover != null) {
emit( emit(
state.copyWith( state.copyWith(

View File

@ -11,7 +11,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class MobileFavoriteSpace extends StatelessWidget { class MobileFavoriteSpace extends StatefulWidget {
const MobileFavoriteSpace({ const MobileFavoriteSpace({
super.key, super.key,
required this.userProfile, required this.userProfile,
@ -19,8 +19,18 @@ class MobileFavoriteSpace extends StatelessWidget {
final UserProfilePB userProfile; final UserProfilePB userProfile;
@override
State<MobileFavoriteSpace> createState() => _MobileFavoriteSpaceState();
}
class _MobileFavoriteSpaceState extends State<MobileFavoriteSpace>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
final workspaceId = final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ?? context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
''; '';
@ -28,7 +38,9 @@ class MobileFavoriteSpace extends StatelessWidget {
providers: [ providers: [
BlocProvider( BlocProvider(
create: (_) => SidebarSectionsBloc() create: (_) => SidebarSectionsBloc()
..add(SidebarSectionsEvent.initial(userProfile, workspaceId)), ..add(
SidebarSectionsEvent.initial(widget.userProfile, workspaceId),
),
), ),
BlocProvider( BlocProvider(
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),

View File

@ -5,13 +5,23 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class MobileHomeSpace extends StatelessWidget { class MobileHomeSpace extends StatefulWidget {
const MobileHomeSpace({super.key, required this.userProfile}); const MobileHomeSpace({super.key, required this.userProfile});
final UserProfilePB userProfile; final UserProfilePB userProfile;
@override
State<MobileHomeSpace> createState() => _MobileHomeSpaceState();
}
class _MobileHomeSpaceState extends State<MobileHomeSpace>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
final workspaceId = final workspaceId =
context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ?? context.read<UserWorkspaceBloc>().state.currentWorkspace?.workspaceId ??
''; '';
@ -23,7 +33,7 @@ class MobileHomeSpace extends StatelessWidget {
vertical: HomeSpaceViewSizes.mVerticalPadding, vertical: HomeSpaceViewSizes.mVerticalPadding,
), ),
child: MobileFolders( child: MobileFolders(
user: userProfile, user: widget.userProfile,
workspaceId: workspaceId, workspaceId: workspaceId,
showFavorite: false, showFavorite: false,
), ),

View File

@ -7,11 +7,21 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class MobileRecentSpace extends StatelessWidget { class MobileRecentSpace extends StatefulWidget {
const MobileRecentSpace({super.key}); const MobileRecentSpace({super.key});
@override
State<MobileRecentSpace> createState() => _MobileRecentSpaceState();
}
class _MobileRecentSpaceState extends State<MobileRecentSpace>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return BlocProvider( return BlocProvider(
create: (context) => create: (context) =>
RecentViewsBloc()..add(const RecentViewsEvent.initial()), RecentViewsBloc()..add(const RecentViewsEvent.initial()),

View File

@ -53,8 +53,8 @@ class MobileViewCard extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<ViewBloc>( BlocProvider<ViewBloc>(
create: (context) => create: (context) => ViewBloc(view: view, shouldLoadChildViews: false)
ViewBloc(view: view)..add(const ViewEvent.initial()), ..add(const ViewEvent.initial()),
), ),
BlocProvider( BlocProvider(
create: (context) => create: (context) =>

View File

@ -39,6 +39,7 @@ class MobileSpaceTabBar extends StatelessWidget {
indicatorColor: Theme.of(context).primaryColor, indicatorColor: Theme.of(context).primaryColor,
isScrollable: true, isScrollable: true,
labelStyle: labelStyle, labelStyle: labelStyle,
labelColor: baseStyle?.color,
labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), labelPadding: const EdgeInsets.symmetric(horizontal: 12.0),
unselectedLabelStyle: unselectedLabelStyle, unselectedLabelStyle: unselectedLabelStyle,
overlayColor: WidgetStateProperty.all(Colors.transparent), overlayColor: WidgetStateProperty.all(Colors.transparent),

View File

@ -21,7 +21,7 @@ class MobileBottomNavigationBar extends StatelessWidget {
return Scaffold( return Scaffold(
body: navigationShell, body: navigationShell,
bottomNavigationBar: Theme( bottomNavigationBar: Theme(
data: ThemeData( data: Theme.of(context).copyWith(
splashColor: Colors.transparent, splashColor: Colors.transparent,
highlightColor: Colors.transparent, highlightColor: Colors.transparent,
), ),

View File

@ -257,7 +257,6 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
final children = [ final children = [
// expand icon // expand icon
_buildLeftIcon(), _buildLeftIcon(),
const HSpace(6),
// icon // icon
_buildViewIcon(), _buildViewIcon(),
const HSpace(8), const HSpace(8),
@ -274,11 +273,11 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
// hover action // hover action
// ··· more action button // ··· more action button
// children.add(_buildViewMoreActionButton(context)); children.add(_buildViewMoreButton(context));
// only support add button for document layout // only support add button for document layout
if (!widget.isFeedback && widget.view.layout == ViewLayoutPB.Document) { if (!widget.isFeedback && widget.view.layout == ViewLayoutPB.Document) {
// + button // + button
children.add(_buildViewMoreButton(context));
children.add(_buildViewAddButton(context)); children.add(_buildViewAddButton(context));
} }
@ -326,22 +325,20 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
// show > if the view is expandable. // show > if the view is expandable.
// show · if the view can't contain child views. // show · if the view can't contain child views.
Widget _buildLeftIcon() { Widget _buildLeftIcon() {
if (isReferencedDatabaseView(widget.view, widget.parentView)) {
return const _DotIconWidget();
}
if (context.read<ViewBloc>().state.view.childViews.isEmpty) { if (context.read<ViewBloc>().state.view.childViews.isEmpty) {
return HSpace(widget.leftPadding); return HSpace(widget.leftPadding);
} }
return GestureDetector( return GestureDetector(
child: AnimatedRotation( behavior: HitTestBehavior.opaque,
duration: const Duration(milliseconds: 250), child: Padding(
turns: widget.isExpanded ? 0 : -0.25, padding: const EdgeInsets.only(right: 6.0, top: 6.0, bottom: 6.0),
child: const FlowySvg( child: FlowySvg(
FlowySvgs.m_expand_s, widget.isExpanded
blendMode: null, ? FlowySvgs.m_expand_s
), : FlowySvgs.m_collapse_s,
blendMode: null,
),
), ),
onTap: () { onTap: () {
context context
@ -431,25 +428,6 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
} }
} }
class _DotIconWidget extends StatelessWidget {
const _DotIconWidget();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(6.0),
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).iconTheme.color,
borderRadius: BorderRadius.circular(2),
),
),
);
}
}
// workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field. // workaround: we should use view.isEndPoint or something to check if the view can contain child views. But currently, we don't have that field.
bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) { bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {
if (parentView == null) { if (parentView == null) {

View File

@ -1,12 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/icon/icon_selector.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart';
import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/appflowy_network_image.dart';
import 'package:appflowy/shared/flowy_gradient_colors.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart';
import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy/shared/google_fonts_extension.dart';
@ -17,11 +19,12 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:auto_size_text_field/auto_size_text_field.dart'; import 'package:auto_size_text_field/auto_size_text_field.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
double kDocumentCoverHeight = 98.0; double kDocumentCoverHeight = 98.0;
double kDocumentTitlePadding = 20.0; double kDocumentTitlePadding = 20.0;
@ -171,12 +174,40 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
), ),
), ),
onTap: () async { onTap: () async {
final result = await context.push<EmojiPickerResult>( final pageStyleIconBloc = PageStyleIconBloc(view: widget.view)
MobileEmojiPickerScreen.routeName, ..add(const PageStyleIconEvent.initial());
await showMobileBottomSheet(
context,
showDragHandle: true,
showDivider: false,
showDoneButton: true,
showHeader: true,
title: LocaleKeys.titleBar_pageIcon.tr(),
backgroundColor: AFThemeExtension.of(context).background,
enableDraggableScrollable: true,
minChildSize: 0.6,
initialChildSize: 0.61,
showRemoveButton: true,
onRemove: () {
pageStyleIconBloc.add(
const PageStyleIconEvent.updateIcon('', true),
);
},
scrollableWidgetBuilder: (_, controller) {
return BlocProvider.value(
value: pageStyleIconBloc,
child: Expanded(
child: Scrollbar(
controller: controller,
child: IconSelector(
scrollController: controller,
),
),
),
);
},
builder: (_) => const SizedBox.shrink(),
); );
if (result != null && context.mounted) {
context.read<ViewBloc>().add(ViewEvent.updateIcon(result.emoji));
}
}, },
); );
} }

View File

@ -0,0 +1,159 @@
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.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_emoji_mart/flutter_emoji_mart.dart';
class IconSelector extends StatefulWidget {
const IconSelector({
super.key,
required this.scrollController,
});
final ScrollController scrollController;
@override
State<IconSelector> createState() => _IconSelectorState();
}
class _IconSelectorState extends State<IconSelector> {
EmojiData? emojiData;
List<String> availableEmojis = [];
PageStyleIconBloc? pageStyleIconBloc;
@override
void initState() {
super.initState();
// load the emoji data from cache if it's available
if (kCachedEmojiData != null) {
emojiData = kCachedEmojiData;
availableEmojis = _setupAvailableEmojis(emojiData!);
} else {
EmojiData.builtIn().then(
(value) {
kCachedEmojiData = value;
setState(() {
emojiData = value;
availableEmojis = _setupAvailableEmojis(value);
});
},
);
}
pageStyleIconBloc = context.read<PageStyleIconBloc>();
}
@override
void dispose() {
pageStyleIconBloc?.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (emojiData == null) {
return const Center(child: CircularProgressIndicator());
}
return RepaintBoundary(
child: BlocBuilder<PageStyleIconBloc, PageStyleIconState>(
builder: (_, state) => Column(
children: [
_buildSearchBar(context),
Expanded(
child: GridView.count(
crossAxisCount: 7,
controller: widget.scrollController,
children: [
for (final emoji in availableEmojis)
_buildEmoji(context, emoji, state.icon),
],
),
),
],
),
),
);
}
Widget _buildEmoji(
BuildContext context,
String emoji,
String? selectedEmoji,
) {
Widget child = SizedBox.square(
dimension: 24.0,
child: Center(
child: FlowyText.emoji(
emoji,
fontSize: 24,
),
),
);
if (emoji == selectedEmoji) {
child = Center(
child: Container(
width: 40,
height: 40,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.40,
strokeAlign: BorderSide.strokeAlignOutside,
color: Color(0xFF00BCF0),
),
borderRadius: BorderRadius.circular(10),
),
),
child: child,
),
);
}
return GestureDetector(
onTap: () {
context.read<PageStyleIconBloc>().add(
PageStyleIconEvent.updateIcon(emoji, true),
);
},
child: child,
);
}
List<String> _setupAvailableEmojis(EmojiData emojiData) {
final categories = emojiData.categories;
availableEmojis = categories
.map((e) => e.emojiIds.map((e) => emojiData.getEmojiById(e)))
.expand((e) => e)
.toList();
return availableEmojis;
}
Widget _buildSearchBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: FlowyMobileSearchTextField(
onChanged: (keyword) {
if (emojiData == null) {
return;
}
final filtered = emojiData!.filterByKeyword(keyword);
final availableEmojis = _setupAvailableEmojis(filtered);
setState(() {
this.availableEmojis = availableEmojis;
});
},
),
);
}
}

View File

@ -1,8 +1,7 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/icon/icon_selector.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@ -11,7 +10,6 @@ import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class PageStyleIcon extends StatefulWidget { class PageStyleIcon extends StatefulWidget {
@ -94,7 +92,7 @@ class _PageStyleIconState extends State<PageStyleIcon> {
child: Expanded( child: Expanded(
child: Scrollbar( child: Scrollbar(
controller: controller, controller: controller,
child: _IconSelector( child: IconSelector(
scrollController: controller, scrollController: controller,
), ),
), ),
@ -105,154 +103,3 @@ class _PageStyleIconState extends State<PageStyleIcon> {
); );
} }
} }
class _IconSelector extends StatefulWidget {
const _IconSelector({
required this.scrollController,
});
final ScrollController scrollController;
@override
State<_IconSelector> createState() => _IconSelectorState();
}
class _IconSelectorState extends State<_IconSelector> {
EmojiData? emojiData;
List<String> availableEmojis = [];
PageStyleIconBloc? pageStyleIconBloc;
@override
void initState() {
super.initState();
// load the emoji data from cache if it's available
if (kCachedEmojiData != null) {
emojiData = kCachedEmojiData;
availableEmojis = _setupAvailableEmojis(emojiData!);
} else {
EmojiData.builtIn().then(
(value) {
kCachedEmojiData = value;
setState(() {
emojiData = value;
availableEmojis = _setupAvailableEmojis(value);
});
},
);
}
pageStyleIconBloc = context.read<PageStyleIconBloc>();
}
@override
void dispose() {
pageStyleIconBloc?.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (emojiData == null) {
return const Center(child: CircularProgressIndicator());
}
return RepaintBoundary(
child: BlocBuilder<PageStyleIconBloc, PageStyleIconState>(
builder: (_, state) => Column(
children: [
_buildSearchBar(context),
Expanded(
child: GridView.count(
crossAxisCount: 7,
controller: widget.scrollController,
children: [
for (final emoji in availableEmojis)
_buildEmoji(context, emoji, state.icon),
],
),
),
],
),
),
);
}
Widget _buildEmoji(
BuildContext context,
String emoji,
String? selectedEmoji,
) {
Widget child = SizedBox.square(
dimension: 24.0,
child: Center(
child: FlowyText.emoji(
emoji,
fontSize: 24,
),
),
);
if (emoji == selectedEmoji) {
child = Center(
child: Container(
width: 40,
height: 40,
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 1.40,
strokeAlign: BorderSide.strokeAlignOutside,
color: Color(0xFF00BCF0),
),
borderRadius: BorderRadius.circular(10),
),
),
child: child,
),
);
}
return GestureDetector(
onTap: () {
context.read<PageStyleIconBloc>().add(
PageStyleIconEvent.updateIcon(emoji, true),
);
},
child: child,
);
}
List<String> _setupAvailableEmojis(EmojiData emojiData) {
final categories = emojiData.categories;
availableEmojis = categories
.map((e) => e.emojiIds.map((e) => emojiData.getEmojiById(e)))
.expand((e) => e)
.toList();
return availableEmojis;
}
Widget _buildSearchBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 12.0,
),
child: FlowyMobileSearchTextField(
onChanged: (keyword) {
if (emojiData == null) {
return;
}
final filtered = emojiData!.filterByKeyword(keyword);
final availableEmojis = _setupAvailableEmojis(filtered);
setState(() {
this.availableEmojis = availableEmojis;
});
},
),
);
}
}

View File

@ -19,7 +19,7 @@ import 'package:protobuf/protobuf.dart';
part 'view_bloc.freezed.dart'; part 'view_bloc.freezed.dart';
class ViewBloc extends Bloc<ViewEvent, ViewState> { class ViewBloc extends Bloc<ViewEvent, ViewState> {
ViewBloc({required this.view}) ViewBloc({required this.view, this.shouldLoadChildViews = true})
: viewBackendSvc = ViewBackendService(), : viewBackendSvc = ViewBackendService(),
listener = ViewListener(viewId: view.id), listener = ViewListener(viewId: view.id),
favoriteListener = FavoriteListener(), favoriteListener = FavoriteListener(),
@ -31,6 +31,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
final ViewBackendService viewBackendSvc; final ViewBackendService viewBackendSvc;
final ViewListener listener; final ViewListener listener;
final FavoriteListener favoriteListener; final FavoriteListener favoriteListener;
final bool shouldLoadChildViews;
@override @override
Future<void> close() async { Future<void> close() async {
@ -74,8 +75,10 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
}, },
); );
final isExpanded = await _getViewIsExpanded(view); final isExpanded = await _getViewIsExpanded(view);
emit(state.copyWith(isExpanded: isExpanded)); emit(state.copyWith(isExpanded: isExpanded, view: view));
await _loadChildViews(emit); if (shouldLoadChildViews) {
await _loadChildViews(emit);
}
}, },
setIsEditing: (e) { setIsEditing: (e) {
emit(state.copyWith(isEditing: e.isEditing)); emit(state.copyWith(isEditing: e.isEditing));

View File

@ -109,6 +109,7 @@ class _FavoriteFolderState extends State<FavoriteFolder> {
const HSpace(4.0), const HSpace(4.0),
], ],
shouldRenderChildren: false, shouldRenderChildren: false,
shouldLoadChildViews: false,
onTertiarySelected: (_, view) => onTertiarySelected: (_, view) =>
context.read<TabsBloc>().openTab(view), context.read<TabsBloc>().openTab(view),
onSelected: (_, view) { onSelected: (_, view) {
@ -152,6 +153,7 @@ class FavoriteMoreButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final favoriteBloc = context.watch<FavoriteBloc>(); final favoriteBloc = context.watch<FavoriteBloc>();
final tabsBloc = context.read<TabsBloc>();
final unpinnedViews = favoriteBloc.state.unpinnedViews; final unpinnedViews = favoriteBloc.state.unpinnedViews;
// only show the more button if there are unpinned views // only show the more button if there are unpinned views
if (unpinnedViews.isEmpty) { if (unpinnedViews.isEmpty) {
@ -169,8 +171,11 @@ class FavoriteMoreButton extends StatelessWidget {
borderRadius: 10.0, borderRadius: 10.0,
), ),
popupBuilder: (_) { popupBuilder: (_) {
return BlocProvider.value( return MultiBlocProvider(
value: favoriteBloc, providers: [
BlocProvider.value(value: favoriteBloc),
BlocProvider.value(value: tabsBloc),
],
child: const FavoriteMenu(minWidth: minWidth), child: const FavoriteMenu(minWidth: minWidth),
); );
}, },

View File

@ -1,11 +1,13 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
@ -125,7 +127,10 @@ class FavoriteMenu extends StatelessWidget {
view: e, view: e,
spaceType: FolderSpaceType.favorite, spaceType: FolderSpaceType.favorite,
level: 0, level: 0,
onSelected: (view, _) {}, onSelected: (_, view) {
context.read<TabsBloc>().openPlugin(view);
PopoverContainer.maybeOf(context)?.close();
},
isFeedback: false, isFeedback: false,
isDraggable: false, isDraggable: false,
shouldRenderChildren: false, shouldRenderChildren: false,
@ -143,7 +148,7 @@ class FavoriteMenu extends StatelessWidget {
} }
} }
class _FavoriteSearchField extends StatelessWidget { class _FavoriteSearchField extends StatefulWidget {
const _FavoriteSearchField({ const _FavoriteSearchField({
required this.width, required this.width,
required this.onSearch, required this.onSearch,
@ -152,11 +157,30 @@ class _FavoriteSearchField extends StatelessWidget {
final double width; final double width;
final void Function(BuildContext context, String text) onSearch; final void Function(BuildContext context, String text) onSearch;
@override
State<_FavoriteSearchField> createState() => _FavoriteSearchFieldState();
}
class _FavoriteSearchFieldState extends State<_FavoriteSearchField> {
final focusNode = FocusNode();
@override
void initState() {
super.initState();
focusNode.requestFocus();
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 30, height: 30,
width: width, width: widget.width,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: ShapeDecoration( decoration: ShapeDecoration(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -169,8 +193,9 @@ class _FavoriteSearchField extends StatelessWidget {
), ),
), ),
child: CupertinoSearchTextField( child: CupertinoSearchTextField(
onChanged: (text) => onSearch(context, text), onChanged: (text) => widget.onSearch(context, text),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
focusNode: focusNode,
placeholder: LocaleKeys.search_label.tr(), placeholder: LocaleKeys.search_label.tr(),
prefixIcon: const FlowySvg(FlowySvgs.m_search_m), prefixIcon: const FlowySvg(FlowySvgs.m_search_m),
prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0), prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0),

View File

@ -1,5 +1,4 @@
import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; import 'package:appflowy/workspace/application/favorite/favorite_service.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -43,9 +42,9 @@ class FavoriteMenuBloc extends Bloc<FavoriteMenuEvent, FavoriteMenuState> {
if (query.isNotEmpty) { if (query.isNotEmpty) {
queriedViews = _filter(views, query); queriedViews = _filter(views, query);
todayViews = _filter(state.todayViews, query); todayViews = _filter(todayViews, query);
thisWeekViews = _filter(state.thisWeekViews, query); thisWeekViews = _filter(thisWeekViews, query);
otherViews = _filter(state.otherViews, query); otherViews = _filter(otherViews, query);
} }
emit( emit(
@ -66,28 +65,20 @@ class FavoriteMenuBloc extends Bloc<FavoriteMenuEvent, FavoriteMenuState> {
final FavoriteService _service = FavoriteService(); final FavoriteService _service = FavoriteService();
RepeatedFavoriteViewPB? _source; RepeatedFavoriteViewPB? _source;
List<ViewPB> _filter(List<ViewPB> views, String query) { List<ViewPB> _filter(List<ViewPB> views, String query) => views
return views .where((view) => view.name.toLowerCase().contains(query.toLowerCase()))
.where( .toList();
(view) => view.name.toLowerCase().contains(query.toLowerCase()),
)
.toList();
}
// all, today, last week, other // all, today, last week, other
(List<ViewPB>, List<ViewPB>, List<ViewPB>, List<ViewPB>) _getViews( (List<ViewPB>, List<ViewPB>, List<ViewPB>, List<ViewPB>) _getViews(
RepeatedFavoriteViewPB source, RepeatedFavoriteViewPB source,
) { ) {
final List<ViewPB> views = final List<ViewPB> views = source.items.map((v) => v.item).toList();
source.items.map((v) => v.item).where((e) => !e.isPinned).toList();
final List<ViewPB> todayViews = []; final List<ViewPB> todayViews = [];
final List<ViewPB> thisWeekViews = []; final List<ViewPB> thisWeekViews = [];
final List<ViewPB> otherViews = []; final List<ViewPB> otherViews = [];
for (final favoriteView in source.items) { for (final favoriteView in source.items) {
final view = favoriteView.item; final view = favoriteView.item;
if (view.isPinned) {
continue;
}
final date = DateTime.fromMillisecondsSinceEpoch( final date = DateTime.fromMillisecondsSinceEpoch(
favoriteView.timestamp.toInt() * 1000, favoriteView.timestamp.toInt() * 1000,
); );

View File

@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
@ -31,8 +30,6 @@ class FavoritePinAction extends StatelessWidget {
width: 24, width: 24,
icon: icon, icon: icon,
onPressed: () { onPressed: () {
PopoverContainer.maybeOf(context)?.closeAll();
view.isPinned view.isPinned
? context.read<FavoriteBloc>().add(FavoriteEvent.unpin(view)) ? context.read<FavoriteBloc>().add(FavoriteEvent.unpin(view))
: context.read<FavoriteBloc>().add(FavoriteEvent.pin(view)); : context.read<FavoriteBloc>().add(FavoriteEvent.pin(view));

View File

@ -13,11 +13,12 @@ class SidebarFooter extends StatelessWidget {
return const Row( return const Row(
children: [ children: [
Expanded(child: SidebarTrashButton()), Expanded(child: SidebarTrashButton()),
SizedBox( // Enable it when the widget button is ready
height: 16, // SizedBox(
child: VerticalDivider(width: 1, color: Color(0x141F2329)), // height: 16,
), // child: VerticalDivider(width: 1, color: Color(0x141F2329)),
Expanded(child: SidebarWidgetButton()), // ),
// Expanded(child: SidebarWidgetButton()),
], ],
); );
} }

View File

@ -57,6 +57,7 @@ class ViewItem extends StatelessWidget {
this.shouldRenderChildren = true, this.shouldRenderChildren = true,
this.leftIconBuilder, this.leftIconBuilder,
this.rightIconsBuilder, this.rightIconsBuilder,
this.shouldLoadChildViews = true,
}); });
final ViewPB view; final ViewPB view;
@ -107,10 +108,14 @@ class ViewItem extends StatelessWidget {
// custom the right icon widget, if it's null, the default ... and + button will be used // custom the right icon widget, if it's null, the default ... and + button will be used
final ViewItemRightIconsBuilder? rightIconsBuilder; final ViewItemRightIconsBuilder? rightIconsBuilder;
final bool shouldLoadChildViews;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), create: (_) =>
ViewBloc(view: view, shouldLoadChildViews: shouldLoadChildViews)
..add(const ViewEvent.initial()),
child: BlocConsumer<ViewBloc, ViewState>( child: BlocConsumer<ViewBloc, ViewState>(
listenWhen: (p, c) => listenWhen: (p, c) =>
c.lastCreatedView != null && c.lastCreatedView != null &&

View File

@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" 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"/> <path d="M10.9667 7.59993C11.2333 7.79993 11.2333 8.19993 10.9667 8.39993L6.3 11.8999C5.97038 12.1471 5.5 11.912 5.5 11.4999L5.5 4.49993C5.5 4.08791 5.97038 3.85272 6.3 4.09993L10.9667 7.59993Z" fill="#8F959E"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 315 B

View File

@ -201,8 +201,8 @@
"addBlockBelow": "Add a block below" "addBlockBelow": "Add a block below"
}, },
"sideBar": { "sideBar": {
"closeSidebar": "Close side bar", "closeSidebar": "Close sidebar",
"openSidebar": "Open side bar", "openSidebar": "Open sidebar",
"personal": "Personal", "personal": "Personal",
"favorites": "Favorites", "favorites": "Favorites",
"clickToHidePersonal": "Click to hide personal section", "clickToHidePersonal": "Click to hide personal section",

View File

@ -139,7 +139,7 @@
"addToFavorites": "Add to Favorites", "addToFavorites": "Add to Favorites",
"copyLink": "Copy Link", "copyLink": "Copy Link",
"changeIcon": "Change icon", "changeIcon": "Change icon",
"collapseAllPages": "Collapse all pages" "collapseAllPages": "Collapse all subpages"
}, },
"blankPageTitle": "Blank page", "blankPageTitle": "Blank page",
"newPageText": "New page", "newPageText": "New page",
@ -229,8 +229,8 @@
"genSummary": "Generate summary" "genSummary": "Generate summary"
}, },
"sideBar": { "sideBar": {
"closeSidebar": "Close side bar", "closeSidebar": "Close sidebar",
"openSidebar": "Open side bar", "openSidebar": "Open sidebar",
"personal": "Personal", "personal": "Personal",
"private": "Private", "private": "Private",
"workspace": "Workspace", "workspace": "Workspace",