feat: tabs shortcuts (#3112)

This commit is contained in:
Mathias Mogensen 2023-08-08 07:09:17 +02:00 committed by GitHub
parent 923285bfcf
commit 35a47bfe5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 283 additions and 174 deletions

View File

@ -50,7 +50,7 @@ void main() {
await tester.tapFirstDayOfWeekStartFromMonday(); await tester.tapFirstDayOfWeekStartFromMonday();
// Open the other page and open the new calendar page again // Open the other page and open the new calendar page again
await tester.openPage(gettingStated); await tester.openPage(gettingStarted);
await tester.pumpAndSettle(const Duration(milliseconds: 300)); await tester.pumpAndSettle(const Duration(milliseconds: 300));
await tester.openPage(name, layout: ViewLayoutPB.Calendar); await tester.openPage(name, layout: ViewLayoutPB.Calendar);

View File

@ -34,20 +34,20 @@ void main() {
// delete the readme page // delete the readme page
await tester.hoverOnPageName( await tester.hoverOnPageName(
gettingStated, gettingStarted,
onHover: () async => await tester.tapDeletePageButton(), onHover: () async => await tester.tapDeletePageButton(),
); );
// the banner should show up and the readme page should be gone // the banner should show up and the readme page should be gone
tester.expectToSeeDocumentBanner(); tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStated); tester.expectNotToSeePageName(gettingStarted);
// restore the readme page // restore the readme page
await tester.tapRestoreButton(); await tester.tapRestoreButton();
// the banner should be gone and the readme page should be back // the banner should be gone and the readme page should be back
tester.expectNotToSeeDocumentBanner(); tester.expectNotToSeeDocumentBanner();
tester.expectToSeePageName(gettingStated); tester.expectToSeePageName(gettingStarted);
}); });
testWidgets('delete the readme page and delete it permanently', testWidgets('delete the readme page and delete it permanently',
@ -58,20 +58,20 @@ void main() {
// delete the readme page // delete the readme page
await tester.hoverOnPageName( await tester.hoverOnPageName(
gettingStated, gettingStarted,
onHover: () async => await tester.tapDeletePageButton(), onHover: () async => await tester.tapDeletePageButton(),
); );
// the banner should show up and the readme page should be gone // the banner should show up and the readme page should be gone
tester.expectToSeeDocumentBanner(); tester.expectToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStated); tester.expectNotToSeePageName(gettingStarted);
// delete the page permanently // delete the page permanently
await tester.tapDeletePermanentlyButton(); await tester.tapDeletePermanentlyButton();
// the banner should be gone and the readme page should be gone // the banner should be gone and the readme page should be gone
tester.expectNotToSeeDocumentBanner(); tester.expectNotToSeeDocumentBanner();
tester.expectNotToSeePageName(gettingStated); tester.expectNotToSeePageName(gettingStarted);
}); });
}); });
} }

View File

@ -59,7 +59,7 @@ void main() {
); );
// switch to other page and switch back // switch to other page and switch back
await tester.openPage(gettingStated); await tester.openPage(gettingStarted);
await tester.openPage(pageName); await tester.openPage(pageName);
// the numbered list should be kept // the numbered list should be kept
@ -91,7 +91,7 @@ void main() {
} }
// switch to other page and switch back // switch to other page and switch back
await tester.openPage(gettingStated); await tester.openPage(gettingStarted);
await tester.openPage(pageName); await tester.openPage(pageName);
// this screenshots are different on different platform, so comment it out temporarily. // this screenshots are different on different platform, so comment it out temporarily.

View File

@ -17,7 +17,7 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
// expect to see a getting started page // expect to see a getting started page
tester.expectToSeePageName(gettingStated); tester.expectToSeePageName(gettingStarted);
await tester.tapAddViewButton(); await tester.tapAddViewButton();
await tester.tapImportButton(); await tester.tapImportButton();

View File

@ -16,6 +16,7 @@ import 'share_markdown_test.dart' as share_markdown_test;
import 'switch_folder_test.dart' as switch_folder_test; import 'switch_folder_test.dart' as switch_folder_test;
import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner; import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'board/board_test_runner.dart' as board_test_runner; import 'board/board_test_runner.dart' as board_test_runner;
import 'tabs_test.dart' as tabs_test;
/// The main task runner for all integration tests in AppFlowy. /// The main task runner for all integration tests in AppFlowy.
/// ///
@ -51,6 +52,9 @@ void main() {
database_view_test.main(); database_view_test.main();
database_calendar_test.main(); database_calendar_test.main();
// Tabs
tabs_test.main();
// board_test.main(); // board_test.main();
// empty_document_test.main(); // empty_document_test.main();
// smart_menu_test.main(); // smart_menu_test.main();

View File

@ -16,7 +16,7 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
// expect to see a readme page // expect to see a readme page
tester.expectToSeePageName(gettingStated); tester.expectToSeePageName(gettingStarted);
// mock the file picker // mock the file picker
final path = await mockSaveFilePath( final path = await mockSaveFilePath(
@ -43,11 +43,11 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
// expect to see a getting started page // expect to see a getting started page
tester.expectToSeePageName(gettingStated); tester.expectToSeePageName(gettingStarted);
// rename the document // rename the document
await tester.hoverOnPageName( await tester.hoverOnPageName(
gettingStated, gettingStarted,
onHover: () async { onHover: () async {
await tester.renamePage('example'); await tester.renamePage('example');
}, },

View File

@ -30,7 +30,7 @@ void main() {
2, 2,
].map((e) => 'document_$e').toList(); ].map((e) => 'document_$e').toList();
for (var i = 0; i < names.length; i++) { for (var i = 0; i < names.length; i++) {
final parentName = i == 0 ? gettingStated : names[i - 1]; final parentName = i == 0 ? gettingStarted : names[i - 1];
await tester.createNewPageWithName( await tester.createNewPageWithName(
name: names[i], name: names[i],
parentName: parentName, parentName: parentName,
@ -44,9 +44,9 @@ void main() {
); );
} }
await tester.favoriteViewByName(gettingStated); await tester.favoriteViewByName(gettingStarted);
expect( expect(
tester.findFavoritePageName(gettingStated), tester.findFavoritePageName(gettingStarted),
findsOneWidget, findsOneWidget,
); );
@ -56,9 +56,9 @@ void main() {
findsNWidgets(2), findsNWidgets(2),
); );
await tester.unfavoriteViewByName(gettingStated); await tester.unfavoriteViewByName(gettingStarted);
expect( expect(
tester.findFavoritePageName(gettingStated), tester.findFavoritePageName(gettingStarted),
findsNothing, findsNothing,
); );
expect( expect(
@ -84,9 +84,9 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
const name = 'test'; const name = 'test';
await tester.favoriteViewByName(gettingStated); await tester.favoriteViewByName(gettingStarted);
await tester.hoverOnPageName( await tester.hoverOnPageName(
gettingStated, gettingStarted,
layout: ViewLayoutPB.Document, layout: ViewLayoutPB.Document,
onHover: () async { onHover: () async {
await tester.renamePage(name); await tester.renamePage(name);
@ -112,7 +112,7 @@ void main() {
final names = [1, 2].map((e) => 'document_$e').toList(); final names = [1, 2].map((e) => 'document_$e').toList();
for (var i = 0; i < names.length; i++) { for (var i = 0; i < names.length; i++) {
final parentName = i == 0 ? gettingStated : names[i - 1]; final parentName = i == 0 ? gettingStarted : names[i - 1];
await tester.createNewPageWithName( await tester.createNewPageWithName(
name: names[i], name: names[i],
parentName: parentName, parentName: parentName,
@ -120,7 +120,7 @@ void main() {
); );
tester.expectToSeePageName(names[i], parentName: parentName); tester.expectToSeePageName(names[i], parentName: parentName);
} }
await tester.favoriteViewByName(gettingStated); await tester.favoriteViewByName(gettingStarted);
await tester.favoriteViewByName(names[0]); await tester.favoriteViewByName(names[0]);
await tester.favoriteViewByName(names[1]); await tester.favoriteViewByName(names[1]);
@ -154,7 +154,7 @@ void main() {
); );
await tester.hoverOnPageName( await tester.hoverOnPageName(
gettingStated, gettingStarted,
layout: ViewLayoutPB.Document, layout: ViewLayoutPB.Document,
onHover: () async { onHover: () async {
await tester.tapDeletePageButton(); await tester.tapDeletePageButton();
@ -181,7 +181,7 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
await tester.createNewPageWithName(); await tester.createNewPageWithName();
await tester.favoriteViewByName(gettingStated); await tester.favoriteViewByName(gettingStarted);
expect( expect(
find.byWidgetPredicate( find.byWidgetPredicate(
(widget) => (widget) =>
@ -201,9 +201,9 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
await tester.createNewPageWithName(); await tester.createNewPageWithName();
await tester.favoriteViewByName(gettingStated); await tester.favoriteViewByName(gettingStarted);
await tester.hoverOnPageName( await tester.hoverOnPageName(
gettingStated, gettingStarted,
layout: ViewLayoutPB.Document, layout: ViewLayoutPB.Document,
useLast: false, useLast: false,
onHover: () async { onHover: () async {

View File

@ -70,7 +70,7 @@ void main() {
break; break;
} }
await tester.openPage(gettingStated); await tester.openPage(gettingStarted);
} }
}); });
@ -80,7 +80,7 @@ void main() {
final names = [1, 2, 3, 4].map((e) => 'document_$e').toList(); final names = [1, 2, 3, 4].map((e) => 'document_$e').toList();
for (var i = 0; i < names.length; i++) { for (var i = 0; i < names.length; i++) {
final parentName = i == 0 ? gettingStated : names[i - 1]; final parentName = i == 0 ? gettingStarted : names[i - 1];
await tester.createNewPageWithName( await tester.createNewPageWithName(
name: names[i], name: names[i],
parentName: parentName, parentName: parentName,
@ -92,7 +92,7 @@ void main() {
// move the document_3 to the getting started page // move the document_3 to the getting started page
await tester.movePageToOtherPage( await tester.movePageToOtherPage(
name: names[3], name: names[3],
parentName: gettingStated, parentName: gettingStarted,
layout: ViewLayoutPB.Document, layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document, parentLayout: ViewLayoutPB.Document,
); );
@ -101,7 +101,7 @@ void main() {
.view .view
.parentViewId; .parentViewId;
final toId = tester final toId = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated)) .widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view .view
.id; .id;
expect(fromId, toId); expect(fromId, toId);
@ -109,13 +109,13 @@ void main() {
// move the document_2 before document_1 // move the document_2 before document_1
await tester.movePageToOtherPage( await tester.movePageToOtherPage(
name: names[2], name: names[2],
parentName: gettingStated, parentName: gettingStarted,
layout: ViewLayoutPB.Document, layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document, parentLayout: ViewLayoutPB.Document,
position: DraggableHoverPosition.bottom, position: DraggableHoverPosition.bottom,
); );
final childViews = tester final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated)) .widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view .view
.childViews; .childViews;
expect( expect(
@ -170,7 +170,7 @@ void main() {
// it should not be moved // it should not be moved
final childViews = tester final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated)) .widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view .view
.childViews; .childViews;
expect( expect(

View File

@ -1,23 +1,26 @@
import 'dart:io';
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart'; import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart'; import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'util/base.dart'; import 'util/base.dart';
import 'util/common_operations.dart'; import 'util/common_operations.dart';
import 'util/expectation.dart';
import 'util/keyboard.dart';
const _readmeName = 'Read me'; const _documentName = 'First Doc';
const _documentName = 'Document'; const _documentTwoName = 'Second Doc';
const _calendarName = 'Calendar';
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tabs', () { group('Tabs', () {
testWidgets('Open AppFlowy and open/navigate multiple tabs', testWidgets('Open AppFlowy and open/navigate/close tabs', (tester) async {
(tester) async {
await tester.initializeAppFlowy(); await tester.initializeAppFlowy();
await tester.tapGoButton(); await tester.tapGoButton();
@ -29,31 +32,21 @@ void main() {
findsNothing, findsNothing,
); );
await tester.createNewPageWithName(
name: _calendarName,
layout: ViewLayoutPB.Calendar,
);
await tester.createNewPageWithName( await tester.createNewPageWithName(
name: _documentName, name: _documentName,
layout: ViewLayoutPB.Document, layout: ViewLayoutPB.Document,
); );
// Navigate current view to "Read me" document again await tester.createNewPageWithName(
await tester.tapButtonWithName(_readmeName); name: _documentTwoName,
layout: ViewLayoutPB.Document,
);
/// Open second menu item in a new tab /// Open second menu item in a new tab
await tester.openAppInNewTab(_calendarName); await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
/// Open third menu item in a new tab /// Open third menu item in a new tab
await tester.openAppInNewTab(_documentName); await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsOneWidget,
);
expect( expect(
find.descendant( find.descendant(
@ -63,13 +56,32 @@ void main() {
findsNWidgets(3), findsNWidgets(3),
); );
/// Navigate to the first tab /// Navigate to the second tab
await tester.tap( await tester.tap(
find.descendant( find.descendant(
of: find.byType(FlowyTab), of: find.byType(FlowyTab),
matching: find.text(_readmeName), matching: find.text(gettingStarted),
), ),
); );
/// Close tab by shortcut
await FlowyTestKeyboard.simulateKeyDownEvent(
[
Platform.isMacOS
? LogicalKeyboardKey.meta
: LogicalKeyboardKey.control,
LogicalKeyboardKey.keyW,
],
tester: tester,
);
expect(
find.descendant(
of: find.byType(TabBar),
matching: find.byType(FlowyTab),
),
findsNWidgets(2),
);
}); });
}); });
} }

View File

@ -8,7 +8,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
import 'package:appflowy/user/presentation/skip_log_in_screen.dart'; import 'package:appflowy/user/presentation/skip_log_in_screen.dart';
import 'package:appflowy/workspace/presentation/home/menu/app/section/item.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -29,7 +28,7 @@ extension CommonOperations on WidgetTester {
/// Tap the + button on the home page. /// Tap the + button on the home page.
Future<void> tapAddViewButton({ Future<void> tapAddViewButton({
String name = gettingStated, String name = gettingStarted,
}) async { }) async {
await hoverOnPageName( await hoverOnPageName(
name, name,
@ -211,6 +210,12 @@ extension CommonOperations on WidgetTester {
await tapButtonWithName(ViewMoreActionType.unFavorite.name); await tapButtonWithName(ViewMoreActionType.unFavorite.name);
} }
/// Tap the Open in a new tab button
Future<void> tapOpenInTabButton() async {
await tapPageOptionButton();
await tapButtonWithName(ViewMoreActionType.openInNewTab.name);
}
/// Rename the page. /// Rename the page.
Future<void> renamePage(String name) async { Future<void> renamePage(String name) async {
await tapRenamePageButton(); await tapRenamePageButton();
@ -272,7 +277,7 @@ extension CommonOperations on WidgetTester {
bool openAfterCreated = true, bool openAfterCreated = true,
}) async { }) async {
// create a new page // create a new page
await tapAddViewButton(name: parentName ?? gettingStated); await tapAddViewButton(name: parentName ?? gettingStarted);
await tapButtonWithName(layout.menuName); await tapButtonWithName(layout.menuName);
await pumpAndSettle(); await pumpAndSettle();
@ -336,11 +341,14 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle(); await pumpAndSettle();
} }
Future<void> openAppInNewTab(String name) async { Future<void> openAppInNewTab(String name, ViewLayoutPB layout) async {
await hoverOnPageName(name); await hoverOnPageName(
await tap(find.byType(ViewDisclosureButton)); name,
await pumpAndSettle(); onHover: () async {
await tap(find.text(LocaleKeys.disclosureAction_openNewTab.tr())); await tapOpenInTabButton();
await pumpAndSettle();
},
);
await pumpAndSettle(); await pumpAndSettle();
} }

View File

@ -76,7 +76,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapGoButton(); await tapGoButton();
// expect to see a readme page // expect to see a readme page
expectToSeePageName(gettingStated); expectToSeePageName(gettingStarted);
await tapAddViewButton(); await tapAddViewButton();
await tapImportButton(); await tapImportButton();

View File

@ -11,13 +11,13 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
// const String readme = 'Read me'; // const String readme = 'Read me';
const String gettingStated = '⭐️ Getting started'; const String gettingStarted = '⭐️ Getting started';
extension Expectation on WidgetTester { extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page. /// Expect to see the home page and with a default read me page.
void expectToSeeHomePage() { void expectToSeeHomePage() {
expect(find.byType(HomeStack), findsOneWidget); expect(find.byType(HomeStack), findsOneWidget);
expect(find.textContaining(gettingStated), findsWidgets); expect(find.textContaining(gettingStarted), findsWidgets);
} }
/// Expect to see the page name on the home page. /// Expect to see the page name on the home page.

View File

@ -8,4 +8,12 @@ extension RawKeyboardExtension on RawKeyboard {
LogicalKeyboardKey.altRight, LogicalKeyboardKey.altRight,
].contains(key), ].contains(key),
); );
bool get isControlPressed => keysPressed.any(
(key) => [
LogicalKeyboardKey.control,
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
].contains(key),
);
} }

View File

@ -35,9 +35,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
_listener.start(appsChanged: _handleAppsOrFail); _listener.start(appsChanged: _handleAppsOrFail);
await _fetchApps(emit); await _fetchApps(emit);
}, },
openPage: (e) async {
emit(state.copyWith(plugin: e.plugin));
},
createApp: (_CreateApp event) async { createApp: (_CreateApp event) async {
final result = await _workspaceService.createApp( final result = await _workspaceService.createApp(
name: event.name, name: event.name,
@ -110,7 +107,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
@freezed @freezed
class MenuEvent with _$MenuEvent { class MenuEvent with _$MenuEvent {
const factory MenuEvent.initial() = _Initial; const factory MenuEvent.initial() = _Initial;
const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
const factory MenuEvent.createApp(String name, {String? desc, int? index}) = const factory MenuEvent.createApp(String name, {String? desc, int? index}) =
_CreateApp; _CreateApp;
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp; const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;

View File

@ -1,6 +1,7 @@
import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
@ -21,7 +22,9 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
on<TabsEvent>((event, emit) async { on<TabsEvent>((event, emit) async {
event.when( event.when(
selectTab: (int index) { selectTab: (int index) {
if (index != state.currentIndex) { if (index != state.currentIndex &&
index >= 0 &&
index < state.pages) {
emit(state.copyWith(newIndex: index)); emit(state.copyWith(newIndex: index));
_setLatestOpenView(); _setLatestOpenView();
} }
@ -31,6 +34,10 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
emit(state.closeView(pluginId)); emit(state.closeView(pluginId));
_setLatestOpenView(); _setLatestOpenView();
}, },
closeCurrentTab: () {
emit(state.closeView(state.currentPageManager.plugin.id));
_setLatestOpenView();
},
openTab: (Plugin plugin, ViewPB view) { openTab: (Plugin plugin, ViewPB view) {
emit(state.openView(plugin, view)); emit(state.openView(plugin, view));
_setLatestOpenView(view); _setLatestOpenView(view);
@ -54,4 +61,12 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
} }
} }
} }
/// Adds a [TabsEvent.openTab] event for the provided [ViewPB]
void openTab(ViewPB view) =>
add(TabsEvent.openTab(plugin: view.plugin(), view: view));
/// Adds a [TabsEvent.openPlugin] event for the provided [ViewPB]
void openPlugin(ViewPB view) =>
add(TabsEvent.openPlugin(plugin: view.plugin(), view: view));
} }

View File

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

View File

@ -31,6 +31,11 @@ class TabsState {
} }
TabsState closeView(String pluginId) { TabsState closeView(String pluginId) {
// Avoid closing the only open tab
if (_pageManagers.length == 1) {
return this;
}
_pageManagers.removeWhere((pm) => pm.plugin.id == pluginId); _pageManagers.removeWhere((pm) => pm.plugin.id == pluginId);
/// If currentIndex is greater than the amount of allowed indices /// If currentIndex is greater than the amount of allowed indices
@ -79,6 +84,10 @@ class TabsState {
return null; return null;
} }
if (index == currentIndex) {
return this;
}
return copyWith(newIndex: index); return copyWith(newIndex: index);
} }

View File

@ -112,9 +112,13 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
ext: {}, ext: {},
openAfterCreate: e.openAfterCreated, openAfterCreate: e.openAfterCreated,
); );
emit( emit(
result.fold( result.fold(
(l) => state.copyWith(successOrFailure: left(unit)), (view) => state.copyWith(
lastCreatedView: view,
successOrFailure: left(unit),
),
(error) => state.copyWith(successOrFailure: right(error)), (error) => state.copyWith(successOrFailure: right(error)),
), ),
); );
@ -218,6 +222,7 @@ class ViewState with _$ViewState {
required bool isEditing, required bool isEditing,
required bool isExpanded, required bool isExpanded,
required Either<Unit, FlowyError> successOrFailure, required Either<Unit, FlowyError> successOrFailure,
@Default(null) ViewPB? lastCreatedView,
}) = _ViewState; }) = _ViewState;
factory ViewState.init(ViewPB view) => ViewState( factory ViewState.init(ViewPB view) => ViewState(
@ -226,5 +231,6 @@ class ViewState with _$ViewState {
isExpanded: false, isExpanded: false,
isEditing: false, isEditing: false,
successOrFailure: left(unit), successOrFailure: left(unit),
lastCreatedView: null,
); );
} }

View File

@ -2,45 +2,102 @@ import 'dart:io';
import 'package:appflowy/workspace/application/appearance.dart'; import 'package:appflowy/workspace/application/appearance.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
typedef KeyDownHandler = void Function(HotKey hotKey);
/// Helper class that utilizes the global [HotKeyManager] to easily
/// add a [HotKey] with different handlers.
///
/// Makes registration of a [HotKey] simple and easy to read, and makes
/// sure the [KeyDownHandler], and other handlers, are grouped with the
/// relevant [HotKey].
///
class HotKeyItem {
final HotKey hotKey;
final KeyDownHandler? keyDownHandler;
HotKeyItem({
required this.hotKey,
this.keyDownHandler,
});
void register() =>
hotKeyManager.register(hotKey, keyDownHandler: keyDownHandler);
}
class HomeHotKeys extends StatelessWidget { class HomeHotKeys extends StatelessWidget {
final Widget child; final Widget child;
const HomeHotKeys({required this.child, Key? key}) : super(key: key); const HomeHotKeys({required this.child, Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final HotKey hotKey = HotKey( // Collapse sidebar menu
KeyCode.backslash, HotKeyItem(
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], hotKey: HotKey(
// Set hotkey scope (default is HotKeyScope.system) KeyCode.backslash,
scope: HotKeyScope.inapp, // Set as inapp-wide hotkey. modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
); // Set hotkey scope (default is HotKeyScope.system)
hotKeyManager.register( scope: HotKeyScope.inapp, // Set as inapp-wide hotkey.
hotKey, ),
keyDownHandler: (hotKey) { keyDownHandler: (_) => context
context .read<HomeSettingBloc>()
.read<HomeSettingBloc>() .add(const HomeSettingEvent.collapseMenu()),
.add(const HomeSettingEvent.collapseMenu()); ).register();
},
); // Toggle theme mode light/dark
HotKeyItem(
hotKey: HotKey(
KeyCode.keyL,
modifiers: [
Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,
KeyModifier.shift,
],
scope: HotKeyScope.inapp,
),
keyDownHandler: (_) =>
context.read<AppearanceSettingsCubit>().toggleThemeMode(),
).register();
// Close current tab
HotKeyItem(
hotKey: HotKey(
KeyCode.keyW,
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
scope: HotKeyScope.inapp,
),
keyDownHandler: (_) =>
context.read<TabsBloc>().add(const TabsEvent.closeCurrentTab()),
).register();
// Go to previous tab
HotKeyItem(
hotKey: HotKey(
KeyCode.pageUp,
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
scope: HotKeyScope.inapp,
),
keyDownHandler: (_) => _selectTab(context, -1),
).register();
// Go to next tab
HotKeyItem(
hotKey: HotKey(
KeyCode.pageDown,
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
scope: HotKeyScope.inapp,
),
keyDownHandler: (_) => _selectTab(context, 1),
).register();
final HotKey hotKeyForToggleThemeMode = HotKey(
KeyCode.keyL,
modifiers: [
Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,
KeyModifier.shift,
],
scope: HotKeyScope.inapp,
);
hotKeyManager.register(
hotKeyForToggleThemeMode,
keyDownHandler: (_) {
context.read<AppearanceSettingsCubit>().toggleThemeMode();
},
);
return child; return child;
} }
void _selectTab(BuildContext context, int change) {
final bloc = context.read<TabsBloc>();
bloc.add(TabsEvent.selectTab(bloc.state.currentIndex + change));
}
} }

View File

@ -123,12 +123,7 @@ class ViewSectionItem extends StatelessWidget {
.add(FavoriteEvent.toggle(view)); .add(FavoriteEvent.toggle(view));
break; break;
case ViewDisclosureAction.openInNewTab: case ViewDisclosureAction.openInNewTab:
blocContext.read<TabsBloc>().add( blocContext.read<TabsBloc>().openTab(state.view);
TabsEvent.openTab(
plugin: state.view.plugin(),
view: blocContext.read<ViewBloc>().state.view,
),
);
break; break;
} }
}, },

View File

@ -7,7 +7,6 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/menu/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_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
@ -49,34 +48,18 @@ class HomeMenu extends StatelessWidget {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider<MenuBloc>( BlocProvider<MenuBloc>(
create: (context) { create: (context) => MenuBloc(
final menuBloc = MenuBloc( user: user,
user: user, workspace: workspaceSetting.workspace,
workspace: workspaceSetting.workspace, )..add(const MenuEvent.initial()),
);
menuBloc.add(const MenuEvent.initial());
return menuBloc;
},
), ),
BlocProvider( BlocProvider(
create: (ctx) => create: (context) =>
getIt<FavoriteBloc>()..add(const FavoriteEvent.initial()), getIt<FavoriteBloc>()..add(const FavoriteEvent.initial()),
) )
], ],
child: MultiBlocListener( child: BlocBuilder<MenuBloc, MenuState>(
listeners: [ builder: (context, state) => _renderBody(context),
BlocListener<MenuBloc, MenuState>(
listenWhen: (p, c) => p.plugin.id != c.plugin.id,
listener: (context, state) {
getIt<TabsBloc>().add(
TabsEvent.openPlugin(plugin: state.plugin),
);
},
),
],
child: BlocBuilder<MenuBloc, MenuState>(
builder: (context, state) => _renderBody(context),
),
), ),
); );
} }

View File

@ -1,14 +1,13 @@
import 'package:appflowy/core/raw_keyboard_extension.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.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-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class FavoriteFolder extends StatelessWidget { class FavoriteFolder extends StatelessWidget {
@ -24,6 +23,7 @@ class FavoriteFolder extends StatelessWidget {
if (views.isEmpty) { if (views.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return BlocProvider<FolderBloc>( return BlocProvider<FolderBloc>(
create: (context) => FolderBloc(type: FolderCategoryType.favorite) create: (context) => FolderBloc(type: FolderCategoryType.favorite)
..add( ..add(
@ -54,11 +54,14 @@ class FavoriteFolder extends StatelessWidget {
view: view, view: view,
level: 0, level: 0,
onSelected: (view) { onSelected: (view) {
getIt<MenuSharedState>().latestOpenView = view; if (RawKeyboard.instance.isControlPressed) {
context context.read<TabsBloc>().openTab(view);
.read<MenuBloc>() }
.add(MenuEvent.openPage(view.plugin()));
context.read<TabsBloc>().openPlugin(view);
}, },
onTertiarySelected: (view) =>
context.read<TabsBloc>().openTab(view),
), ),
) )
], ],

View File

@ -1,15 +1,15 @@
import 'package:appflowy/core/raw_keyboard_extension.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.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-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.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/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalFolder extends StatelessWidget { class PersonalFolder extends StatelessWidget {
@ -52,13 +52,14 @@ class PersonalFolder extends StatelessWidget {
leftPadding: 16, leftPadding: 16,
isFeedback: false, isFeedback: false,
onSelected: (view) { onSelected: (view) {
getIt<TabsBloc>().add( if (RawKeyboard.instance.isControlPressed) {
TabsEvent.openPlugin( context.read<TabsBloc>().openTab(view);
plugin: view.plugin(), }
view: view,
), context.read<TabsBloc>().openPlugin(view);
);
}, },
onTertiarySelected: (view) =>
context.read<TabsBloc>().openTab(view),
), ),
) )
], ],

View File

@ -1,4 +1,3 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
@ -7,6 +6,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_pa
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB; show UserProfilePB;
@ -48,17 +48,18 @@ class HomeSideBar extends StatelessWidget {
], ],
child: BlocListener<MenuBloc, MenuState>( child: BlocListener<MenuBloc, MenuState>(
listenWhen: (p, c) => p.plugin.id != c.plugin.id, listenWhen: (p, c) => p.plugin.id != c.plugin.id,
listener: (context, state) => getIt<TabsBloc>().add( listener: (context, state) => context
TabsEvent.openPlugin(plugin: state.plugin), .read<TabsBloc>()
), .add(TabsEvent.openPlugin(plugin: state.plugin)),
child: Builder( child: Builder(
builder: (context) { builder: (context) {
final menuState = context.watch<MenuBloc>().state; final menuState = context.watch<MenuBloc>().state;
final favoriteState = context.watch<FavoriteBloc>().state; final favoriteState = context.watch<FavoriteBloc>().state;
return _buildSidebar( return _buildSidebar(
context, context,
menuState, menuState.views,
favoriteState, favoriteState.views,
); );
}, },
), ),
@ -68,11 +69,10 @@ class HomeSideBar extends StatelessWidget {
Widget _buildSidebar( Widget _buildSidebar(
BuildContext context, BuildContext context,
MenuState state, List<ViewPB> views,
FavoriteState favoriteState, List<ViewPB> favoriteViews,
) { ) {
final views = state.views; return DecoratedBox(
return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant, color: Theme.of(context).colorScheme.surfaceVariant,
border: Border( border: Border(
@ -95,7 +95,7 @@ class HomeSideBar extends StatelessWidget {
child: SingleChildScrollView( child: SingleChildScrollView(
child: SidebarFolder( child: SidebarFolder(
views: views, views: views,
favoriteViews: favoriteState.views, favoriteViews: favoriteViews,
), ),
), ),
), ),

View File

@ -48,12 +48,7 @@ class SidebarNewPageButton extends StatelessWidget {
value: '', value: '',
confirm: (value) { confirm: (value) {
if (value.isNotEmpty) { if (value.isNotEmpty) {
context.read<MenuBloc>().add( context.read<MenuBloc>().add(MenuEvent.createApp(value));
MenuEvent.createApp(
value,
desc: '',
),
);
} }
}, },
).show(context); ).show(context);

View File

@ -19,6 +19,8 @@ import 'package:flowy_infra_ui/style_widget/hover.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';
typedef ViewItemOnSelected = void Function(ViewPB);
class ViewItem extends StatelessWidget { class ViewItem extends StatelessWidget {
const ViewItem({ const ViewItem({
super.key, super.key,
@ -28,6 +30,7 @@ class ViewItem extends StatelessWidget {
required this.level, required this.level,
this.leftPadding = 10, this.leftPadding = 10,
required this.onSelected, required this.onSelected,
this.onTertiarySelected,
this.isFirstChild = false, this.isFirstChild = false,
this.isDraggable = true, this.isDraggable = true,
required this.isFeedback, required this.isFeedback,
@ -46,7 +49,11 @@ class ViewItem extends StatelessWidget {
// the left padding of the each level = level * leftPadding // the left padding of the each level = level * leftPadding
final double leftPadding; final double leftPadding;
final void Function(ViewPB) onSelected; // Selected by normal conventions
final ViewItemOnSelected onSelected;
// Selected by middle mouse button
final ViewItemOnSelected? onTertiarySelected;
// used for indicating the first child of the parent view, so that we can // used for indicating the first child of the parent view, so that we can
// add top border to the first child // add top border to the first child
@ -62,7 +69,12 @@ class ViewItem extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()),
child: BlocBuilder<ViewBloc, ViewState>( child: BlocConsumer<ViewBloc, ViewState>(
listenWhen: (p, c) =>
c.lastCreatedView != null &&
p.lastCreatedView?.id != c.lastCreatedView!.id,
listener: (context, state) =>
context.read<TabsBloc>().openPlugin(state.lastCreatedView!),
builder: (context, state) { builder: (context, state) {
// don't remove this code. it's related to the backend service. // don't remove this code. it's related to the backend service.
view.childViews view.childViews
@ -78,6 +90,7 @@ class ViewItem extends StatelessWidget {
showActions: state.isEditing, showActions: state.isEditing,
isExpanded: state.isExpanded, isExpanded: state.isExpanded,
onSelected: onSelected, onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isFirstChild: isFirstChild, isFirstChild: isFirstChild,
isDraggable: isDraggable, isDraggable: isDraggable,
isFeedback: isFeedback, isFeedback: isFeedback,
@ -101,6 +114,7 @@ class InnerViewItem extends StatelessWidget {
required this.leftPadding, required this.leftPadding,
required this.showActions, required this.showActions,
required this.onSelected, required this.onSelected,
this.onTertiarySelected,
this.isFirstChild = false, this.isFirstChild = false,
required this.isFeedback, required this.isFeedback,
}); });
@ -120,7 +134,8 @@ class InnerViewItem extends StatelessWidget {
final double leftPadding; final double leftPadding;
final bool showActions; final bool showActions;
final void Function(ViewPB) onSelected; final ViewItemOnSelected onSelected;
final ViewItemOnSelected? onTertiarySelected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -131,6 +146,7 @@ class InnerViewItem extends StatelessWidget {
showActions: showActions, showActions: showActions,
categoryType: categoryType, categoryType: categoryType,
onSelected: onSelected, onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isExpanded: isExpanded, isExpanded: isExpanded,
isDraggable: isDraggable, isDraggable: isDraggable,
leftPadding: leftPadding, leftPadding: leftPadding,
@ -148,6 +164,7 @@ class InnerViewItem extends StatelessWidget {
view: childView, view: childView,
level: level + 1, level: level + 1,
onSelected: onSelected, onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isDraggable: isDraggable, isDraggable: isDraggable,
leftPadding: leftPadding, leftPadding: leftPadding,
isFeedback: isFeedback, isFeedback: isFeedback,
@ -176,6 +193,7 @@ class InnerViewItem extends StatelessWidget {
categoryType: categoryType, categoryType: categoryType,
level: level, level: level,
onSelected: onSelected, onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isDraggable: false, isDraggable: false,
leftPadding: leftPadding, leftPadding: leftPadding,
isFeedback: true, isFeedback: true,
@ -206,6 +224,7 @@ class SingleInnerViewItem extends StatefulWidget {
required this.categoryType, required this.categoryType,
required this.showActions, required this.showActions,
required this.onSelected, required this.onSelected,
this.onTertiarySelected,
required this.isFeedback, required this.isFeedback,
}); });
@ -220,7 +239,8 @@ class SingleInnerViewItem extends StatefulWidget {
final bool isDraggable; final bool isDraggable;
final bool showActions; final bool showActions;
final void Function(ViewPB) onSelected; final ViewItemOnSelected onSelected;
final ViewItemOnSelected? onTertiarySelected;
final FolderCategoryType categoryType; final FolderCategoryType categoryType;
@override @override
@ -279,6 +299,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onTap: () => widget.onSelected(widget.view), onTap: () => widget.onSelected(widget.view),
onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view),
child: SizedBox( child: SizedBox(
height: 26, height: 26,
child: Padding( child: Padding(
@ -377,12 +398,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
context.read<ViewBloc>().add(const ViewEvent.duplicate()); context.read<ViewBloc>().add(const ViewEvent.duplicate());
break; break;
case ViewMoreActionType.openInNewTab: case ViewMoreActionType.openInNewTab:
context.read<TabsBloc>().add( context.read<TabsBloc>().openTab(widget.view);
TabsEvent.openTab(
plugin: widget.view.plugin(),
view: widget.view,
),
);
break; break;
default: default:
throw UnsupportedError('$action is not supported'); throw UnsupportedError('$action is not supported');