fix: tab menu and tabbar improvements (#6785)

* fix: tab menu and tabbar improvements

* chore: update appflowy_editor

* test: tab menu test

* test: fix test after refactor
This commit is contained in:
Mathias Mogensen 2024-11-14 06:55:09 +01:00 committed by GitHub
parent d9f2d14e99
commit 1952ef0853
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 469 additions and 170 deletions

View File

@ -346,7 +346,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
test_number: [1, 2, 3, 4, 5, 6, 7, 8]
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
include:
- os: ubuntu-latest
target: "x86_64-unknown-linux-gnu"

View File

@ -1,17 +1,18 @@
import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../../shared/base.dart';
import '../../shared/common_operations.dart';
import '../../shared/expectation.dart';
import '../../shared/keyboard.dart';
import '../../shared/util.dart';
const _documentName = 'First Doc';
const _documentTwoName = 'Second Doc';
@ -20,17 +21,12 @@ void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tabs', () {
testWidgets('Open AppFlowy and open/navigate/close tabs', (tester) async {
testWidgets('open/navigate/close tabs', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsNothing,
);
// No tabs rendered yet
expect(find.byType(FlowyTab), findsNothing);
await tester.createNewPageWithNameUnderParent(name: _documentName);
@ -44,7 +40,7 @@ void main() {
expect(
find.descendant(
of: find.byType(TabBar),
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(3),
@ -71,11 +67,83 @@ void main() {
expect(
find.descendant(
of: find.byType(TabBar),
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(2),
);
});
testWidgets('right click show tab menu, close others', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsNothing,
);
await tester.createNewPageWithNameUnderParent(name: _documentName);
await tester.createNewPageWithNameUnderParent(name: _documentTwoName);
/// Open second menu item in a new tab
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
/// Open third menu item in a new tab
await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(FlowyTab),
),
findsNWidgets(3),
);
/// Right click on second tab
await tester.tap(
buttons: kSecondaryButton,
find.descendant(
of: find.byType(FlowyTab),
matching: find.text(gettingStarted),
),
);
await tester.pumpAndSettle();
expect(find.byType(TabMenu), findsOneWidget);
final firstTabFinder = find.descendant(
of: find.byType(FlowyTab),
matching: find.text(_documentTwoName),
);
final secondTabFinder = find.descendant(
of: find.byType(FlowyTab),
matching: find.text(gettingStarted),
);
final thirdTabFinder = find.descendant(
of: find.byType(FlowyTab),
matching: find.text(_documentName),
);
expect(firstTabFinder, findsOneWidget);
expect(secondTabFinder, findsOneWidget);
expect(thirdTabFinder, findsOneWidget);
// Close other tabs than the second item
await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr()));
await tester.pumpAndSettle();
// We expect to not find any tabs
expect(firstTabFinder, findsNothing);
expect(secondTabFinder, findsNothing);
expect(thirdTabFinder, findsNothing);
// Expect second tab to be current page (current page has breadcrumb, cover title,
// and in this case view name in sidebar)
expect(find.text(gettingStarted), findsNWidgets(3));
});
});
}

View File

@ -4,7 +4,6 @@ import 'emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'hotkeys_test.dart' as hotkeys_test;
import 'import_files_test.dart' as import_files_test;
import 'share_markdown_test.dart' as share_markdown_test;
import 'tabs_test.dart' as tabs_test;
import 'zoom_in_out_test.dart' as zoom_in_out_test;
void main() {
@ -17,7 +16,6 @@ void main() {
emoji_shortcut_test.main();
share_markdown_test.main();
import_files_test.main();
tabs_test.main();
zoom_in_out_test.main();
// DON'T add more tests here.
}

View File

@ -0,0 +1,16 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
import 'desktop/first_test/first_test.dart' as first_test;
Future<void> main() async {
await runIntegration9OnDesktop();
}
Future<void> runIntegration9OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
first_test.main();
tabs_test.main();
}

View File

@ -8,6 +8,7 @@ import 'desktop_runner_5.dart';
import 'desktop_runner_6.dart';
import 'desktop_runner_7.dart';
import 'desktop_runner_8.dart';
import 'desktop_runner_9.dart';
import 'mobile_runner_1.dart';
/// The main task runner for all integration tests in AppFlowy.
@ -27,6 +28,7 @@ Future<void> main() async {
await runIntegration6OnDesktop();
await runIntegration7OnDesktop();
await runIntegration8OnDesktop();
await runIntegration9OnDesktop();
} else if (Platform.isIOS || Platform.isAndroid) {
await runIntegration1OnMobile();
} else {

View File

@ -230,9 +230,7 @@ class DatabaseTabBarViewPlugin extends Plugin {
const kDatabasePluginWidgetBuilderHorizontalPadding = 'horizontal_padding';
class DatabasePluginWidgetBuilderSize {
const DatabasePluginWidgetBuilderSize({
required this.horizontalPadding,
});
const DatabasePluginWidgetBuilderSize({required this.horizontalPadding});
final double horizontalPadding;
}

View File

@ -0,0 +1,79 @@
import 'dart:async';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'tab_menu_bloc.freezed.dart';
class TabMenuBloc extends Bloc<TabMenuEvent, TabMenuState> {
TabMenuBloc({required this.viewId}) : super(const TabMenuState.isLoading()) {
_fetchView();
_dispatch();
}
final String viewId;
ViewPB? view;
void _dispatch() {
on<TabMenuEvent>(
(event, emit) async {
await event.when(
error: (error) async => emit(const TabMenuState.isError()),
fetchedView: (view) async =>
emit(TabMenuState.isReady(isFavorite: view.isFavorite)),
toggleFavorite: () async {
final didToggle = await ViewBackendService.favorite(viewId: viewId);
if (didToggle.isSuccess) {
final isFavorite = state.maybeMap(
isReady: (s) => s.isFavorite,
orElse: () => null,
);
if (isFavorite != null) {
emit(TabMenuState.isReady(isFavorite: !isFavorite));
}
}
},
);
},
);
}
Future<void> _fetchView() async {
final viewOrFailure = await ViewBackendService.getView(viewId);
viewOrFailure.fold(
(view) {
this.view = view;
add(TabMenuEvent.fetchedView(view));
},
(error) {
Log.error(error);
add(TabMenuEvent.error(error));
},
);
}
}
@freezed
class TabMenuEvent with _$TabMenuEvent {
const factory TabMenuEvent.error(FlowyError error) = _Error;
const factory TabMenuEvent.fetchedView(ViewPB view) = _FetchedView;
const factory TabMenuEvent.toggleFavorite() = _ToggleFavorite;
}
@freezed
class TabMenuState with _$TabMenuState {
const factory TabMenuState.isLoading() = _IsLoading;
/// This will only be the state in case fetching the view failed.
///
/// One such case can be from when a View is in the trash, as such we can disable
/// certain options in the TabMenu such as the favorite option.
///
const factory TabMenuState.isError() = _IsError;
const factory TabMenuState.isReady({required bool isFavorite}) = _IsReady;
}

View File

@ -58,6 +58,18 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
_setLatestOpenView(view);
}
},
closeOtherTabs: (String pluginId) {
final pagesToClose = [
...state._pageManagers.where((pm) => pm.plugin.id != pluginId),
];
final newstate = state;
for (final pm in pagesToClose) {
newstate.closeView(pm.plugin.id);
}
emit(newstate.copyWith(newIndex: 0));
_setLatestOpenView();
},
);
},
);
@ -69,7 +81,8 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
} else {
final pageManager = state.currentPageManager;
final notifier = pageManager.plugin.notifier;
if (notifier is ViewPluginNotifier) {
if (notifier is ViewPluginNotifier &&
menuSharedState.latestOpenView?.id != notifier.view.id) {
menuSharedState.latestOpenView = notifier.view;
}
}

View File

@ -4,6 +4,7 @@ part of 'tabs_bloc.dart';
class TabsEvent with _$TabsEvent {
const factory TabsEvent.moveTab() = _MoveTab;
const factory TabsEvent.closeTab(String pluginId) = _CloseTab;
const factory TabsEvent.closeOtherTabs(String pluginId) = _CloseOtherTabs;
const factory TabsEvent.closeCurrentTab() = _CloseCurrentTab;
const factory TabsEvent.selectTab(int index) = _SelectTab;
const factory TabsEvent.openTab({

View File

@ -34,7 +34,7 @@ abstract class HomeStackDelegate {
void didDeleteStackWidget(ViewPB view, int? index);
}
class HomeStack extends StatelessWidget {
class HomeStack extends StatefulWidget {
const HomeStack({
super.key,
required this.delegate,
@ -47,49 +47,67 @@ class HomeStack extends StatelessWidget {
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
final pageController = PageController();
State<HomeStack> createState() => _HomeStackState();
}
class _HomeStackState extends State<HomeStack> {
int selectedIndex = 0;
@override
Widget build(BuildContext context) {
return BlocProvider<TabsBloc>.value(
value: getIt<TabsBloc>(),
child: BlocBuilder<TabsBloc, TabsState>(
builder: (context, state) {
return Column(
children: [
if (Platform.isWindows)
Column(
mainAxisSize: MainAxisSize.min,
children: [
WindowTitleBar(
leftChildren: [
_buildToggleMenuButton(context),
],
),
],
),
Padding(
padding: EdgeInsets.only(left: layout.menuSpacing),
child: TabsManager(pageController: pageController),
buildWhen: (prev, curr) => prev.currentIndex != curr.currentIndex,
builder: (context, state) => Column(
children: [
if (Platform.isWindows)
Column(
mainAxisSize: MainAxisSize.min,
children: [
WindowTitleBar(
leftChildren: [_buildToggleMenuButton(context)],
),
],
),
state.currentPageManager.stackTopBar(layout: layout),
Expanded(
child: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: pageController,
children: state.pageManagers
.map(
(pm) => PageStack(
pageManager: pm,
delegate: delegate,
userProfile: userProfile,
),
)
.toList(),
),
Padding(
padding: EdgeInsets.only(left: widget.layout.menuSpacing),
child: TabsManager(
onIndexChanged: (index) {
if (selectedIndex != index) {
// Unfocus editor to hide selection toolbar
FocusScope.of(context).unfocus();
context.read<TabsBloc>().add(TabsEvent.selectTab(index));
setState(() => selectedIndex = index);
}
},
),
],
);
},
),
Expanded(
child: FadingIndexedStack(
index: selectedIndex,
duration: const Duration(milliseconds: 350),
children: state.pageManagers
.map(
(pm) => Column(
children: [
pm.stackTopBar(layout: widget.layout),
Expanded(
child: PageStack(
pageManager: pm,
delegate: widget.delegate,
userProfile: widget.userProfile,
),
),
],
),
)
.toList(),
),
),
],
),
),
);
}
@ -145,7 +163,6 @@ class PageStack extends StatefulWidget {
});
final PageManager pageManager;
final HomeStackDelegate delegate;
final UserProfilePB userProfile;
@ -216,9 +233,7 @@ class FadingIndexedStackState extends State<FadingIndexedStack> {
return TweenAnimationBuilder<double>(
duration: _targetOpacity > 0 ? widget.duration : 0.milliseconds,
tween: Tween(begin: 0, end: _targetOpacity),
builder: (_, value, child) {
return Opacity(opacity: value, child: child);
},
builder: (_, value, child) => Opacity(opacity: value, child: child),
child: IndexedStack(index: widget.index, children: widget.children),
);
}
@ -279,15 +294,9 @@ class PageManager {
_notifier.setPlugin(newPlugin, setLatest);
}
void setStackWithId(String id) {
// Navigate to the page with id
}
Widget stackTopBar({required HomeLayout layout}) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: _notifier),
],
providers: [ChangeNotifierProvider.value(value: _notifier)],
child: Selector<PageNotifier, Widget>(
selector: (context, notifier) => notifier.titleWidget,
builder: (_, __, child) => MoveWindowDetector(
@ -319,7 +328,6 @@ class PageManager {
shrinkWrap: false,
);
// TODO(Xazin): Board should fill up full width
return Padding(
padding: builder.contentPadding,
child: pluginWidget,
@ -340,13 +348,21 @@ class PageManager {
}
}
class HomeTopBar extends StatelessWidget {
class HomeTopBar extends StatefulWidget {
const HomeTopBar({super.key, required this.layout});
final HomeLayout layout;
@override
State<HomeTopBar> createState() => _HomeTopBarState();
}
class _HomeTopBarState extends State<HomeTopBar>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
@ -359,7 +375,7 @@ class HomeTopBar extends StatelessWidget {
),
child: Row(
children: [
HSpace(layout.menuSpacing),
HSpace(widget.layout.menuSpacing),
const FlowyNavigation(),
const HSpace(16),
ChangeNotifierProvider.value(
@ -375,4 +391,7 @@ class HomeTopBar extends StatelessWidget {
),
);
}
@override
bool get wantKeepAlive => true;
}

View File

@ -1,10 +1,16 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/tabs/tab_menu_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
class FlowyTab extends StatefulWidget {
@ -12,63 +18,187 @@ class FlowyTab extends StatefulWidget {
super.key,
required this.pageManager,
required this.isCurrent,
required this.onTap,
});
final PageManager pageManager;
final bool isCurrent;
final VoidCallback onTap;
@override
State<FlowyTab> createState() => _FlowyTabState();
}
class _FlowyTabState extends State<FlowyTab> {
final controller = PopoverController();
@override
Widget build(BuildContext context) {
return FlowyHover(
isSelected: () => widget.isCurrent,
style: const HoverStyle(
resetHoverOnRebuild: false,
style: HoverStyle(
borderRadius: BorderRadius.zero,
backgroundColor: widget.isCurrent
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.surfaceContainerHighest,
hoverColor:
widget.isCurrent ? Theme.of(context).colorScheme.surface : null,
),
builder: (context, onHover) {
return ChangeNotifierProvider.value(
builder: (context, isHovering) => AppFlowyPopover(
controller: controller,
offset: const Offset(4, 4),
triggerActions: PopoverTriggerFlags.secondaryClick,
showAtCursor: true,
popupBuilder: (_) => BlocProvider.value(
value: context.read<TabsBloc>(),
child: BlocProvider<TabMenuBloc>(
create: (_) => TabMenuBloc(
viewId: widget.pageManager.plugin.id,
),
child: TabMenu(pageId: widget.pageManager.plugin.id),
),
),
child: ChangeNotifierProvider.value(
value: widget.pageManager.notifier,
child: Consumer<PageNotifier>(
builder: (context, value, child) => Padding(
builder: (context, value, _) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: HomeSizes.tabBarWidth,
height: HomeSizes.tabBarHeight,
child: Row(
children: [
Expanded(
child: widget.pageManager.notifier
.tabBarWidget(widget.pageManager.plugin.id),
// We use a Listener to avoid gesture detector onPanStart debounce
child: Listener(
onPointerDown: (event) {
if (event.buttons == kPrimaryButton) {
widget.onTap();
}
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
// Stop move window detector
onPanStart: (_) {},
child: Container(
constraints: const BoxConstraints(
maxWidth: HomeSizes.tabBarWidth,
minWidth: 100,
),
Visibility(
visible: onHover,
child: SizedBox(
width: 26,
height: 26,
child: FlowyIconButton(
onPressed: _closeTab,
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(22),
height: HomeSizes.tabBarHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: widget.pageManager.notifier
.tabBarWidget(widget.pageManager.plugin.id),
),
Visibility(
visible: isHovering,
child: SizedBox(
width: 26,
height: 26,
child: FlowyIconButton(
onPressed: () => _closeTab(context),
icon: const FlowySvg(
FlowySvgs.close_s,
size: Size.square(22),
),
),
),
),
),
],
),
],
),
),
),
),
),
),
),
);
}
void _closeTab(BuildContext context) => context
.read<TabsBloc>()
.add(TabsEvent.closeTab(widget.pageManager.plugin.id));
}
@visibleForTesting
class TabMenu extends StatelessWidget {
const TabMenu({super.key, required this.pageId});
final String pageId;
@override
Widget build(BuildContext context) {
return BlocBuilder<TabMenuBloc, TabMenuState>(
builder: (context, state) {
if (state.maybeMap(
isLoading: (_) => true,
orElse: () => false,
)) {
return const SizedBox.shrink();
}
final disableFavoriteOption = state.maybeWhen(
isReady: (_) => false,
orElse: () => true,
);
return SeparatedColumn(
separatorBuilder: () => const VSpace(4),
mainAxisSize: MainAxisSize.min,
children: [
FlowyButton(
text: FlowyText.regular(LocaleKeys.tabMenu_close.tr()),
onTap: () => _closeTab(context),
),
FlowyButton(
text: FlowyText.regular(
LocaleKeys.tabMenu_closeOthers.tr(),
),
onTap: () => _closeOtherTabs(context),
),
const Divider(height: 1),
_favoriteDisabledTooltip(
showTooltip: disableFavoriteOption,
child: FlowyButton(
disable: disableFavoriteOption,
text: FlowyText.regular(
state.maybeWhen(
isReady: (isFavorite) => isFavorite
? LocaleKeys.tabMenu_unfavorite.tr()
: LocaleKeys.tabMenu_favorite.tr(),
orElse: () => LocaleKeys.tabMenu_favorite.tr(),
),
color: disableFavoriteOption
? Theme.of(context).hintColor
: null,
),
onTap: () => _toggleFavorite(context),
),
),
],
);
},
);
}
void _closeTab([TapUpDetails? details]) => context
.read<TabsBloc>()
.add(TabsEvent.closeTab(widget.pageManager.plugin.id));
void _closeTab(BuildContext context) =>
context.read<TabsBloc>().add(TabsEvent.closeTab(pageId));
void _closeOtherTabs(BuildContext context) =>
context.read<TabsBloc>().add(TabsEvent.closeOtherTabs(pageId));
void _toggleFavorite(BuildContext context) =>
context.read<TabMenuBloc>().add(const TabMenuEvent.toggleFavorite());
Widget _favoriteDisabledTooltip({
required bool showTooltip,
required Widget child,
}) {
if (showTooltip) {
return FlowyTooltip(
message: LocaleKeys.tabMenu_favoriteDisabledHint.tr(),
child: child,
);
}
return child;
}
}

View File

@ -1,59 +1,35 @@
import 'package:appflowy/core/frameless_window.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class TabsManager extends StatefulWidget {
const TabsManager({super.key, required this.pageController});
const TabsManager({
super.key,
required this.onIndexChanged,
});
final PageController pageController;
final void Function(int) onIndexChanged;
@override
State<TabsManager> createState() => _TabsManagerState();
}
class _TabsManagerState extends State<TabsManager>
with TickerProviderStateMixin {
late TabController _controller;
@override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 1);
}
class _TabsManagerState extends State<TabsManager> {
@override
Widget build(BuildContext context) {
return BlocProvider<TabsBloc>.value(
value: BlocProvider.of<TabsBloc>(context),
value: context.read<TabsBloc>(),
child: BlocListener<TabsBloc, TabsState>(
listener: (context, state) {
if (_controller.length != state.pages) {
_controller.dispose();
_controller = TabController(
vsync: this,
initialIndex: state.currentIndex,
length: state.pages,
);
}
if (state.currentIndex != widget.pageController.page) {
// Unfocus editor to hide selection toolbar
FocusScope.of(context).unfocus();
widget.pageController.animateToPage(
state.currentIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
listenWhen: (prev, curr) =>
prev.currentIndex != curr.currentIndex || prev.pages != curr.pages,
listener: (context, state) => widget.onIndexChanged(state.currentIndex),
child: BlocBuilder<TabsBloc, TabsState>(
builder: (context, state) {
if (_controller.length == 1) {
if (state.pages == 1) {
return const SizedBox.shrink();
}
@ -63,31 +39,29 @@ class _TabsManagerState extends State<TabsManager>
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
/// TODO(Xazin): Custom Reorderable TabBar
child: TabBar(
padding: EdgeInsets.zero,
labelPadding: EdgeInsets.zero,
indicator: BoxDecoration(
border: Border.all(width: 0, color: Colors.transparent),
),
indicatorWeight: 0,
dividerColor: Colors.transparent,
isScrollable: true,
controller: _controller,
onTap: (newIndex) {
AFFocusManager.of(context).notifyLoseFocus();
context.read<TabsBloc>().add(TabsEvent.selectTab(newIndex));
},
tabs: state.pageManagers
.map(
(pm) => FlowyTab(
key: UniqueKey(),
pageManager: pm,
isCurrent: state.currentPageManager == pm,
child: MoveWindowDetector(
child: Row(
children: state.pageManagers.map<Widget>((pm) {
return Flexible(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: HomeSizes.tabBarWidth,
),
child: FlowyTab(
key: ValueKey('tab-${pm.plugin.id}'),
pageManager: pm,
isCurrent: state.currentPageManager == pm,
onTap: () {
if (state.currentPageManager != pm) {
final index = state.pageManagers.indexOf(pm);
widget.onIndexChanged(index);
}
},
),
),
)
.toList(),
);
}).toList(),
),
),
);
},
@ -95,10 +69,4 @@ class _TabsManagerState extends State<TabsManager>
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

View File

@ -61,8 +61,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "76daa96"
resolved-ref: "76daa96af51f0ad4e881c10426a91780977544e5"
ref: ea81e3c
resolved-ref: ea81e3c1647344aff45970c39556902ffad4373d
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "4.0.0"

View File

@ -172,7 +172,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "76daa96"
ref: "ea81e3c"
appflowy_editor_plugins:
git:

View File

@ -2845,5 +2845,12 @@
"one": "1 member",
"many": "{count} members",
"other": "{count} members"
},
"tabMenu": {
"close": "Close",
"closeOthers": "Close other tabs",
"favorite": "Favorite",
"unfavorite": "Unfavorite",
"favoriteDisabledHint": "Cannot favorite this view"
}
}