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();
// 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.openPage(name, layout: ViewLayoutPB.Calendar);

View File

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

View File

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

View File

@ -17,7 +17,7 @@ void main() {
await tester.tapGoButton();
// expect to see a getting started page
tester.expectToSeePageName(gettingStated);
tester.expectToSeePageName(gettingStarted);
await tester.tapAddViewButton();
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 'sidebar/sidebar_test_runner.dart' as sidebar_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.
///
@ -51,6 +52,9 @@ void main() {
database_view_test.main();
database_calendar_test.main();
// Tabs
tabs_test.main();
// board_test.main();
// empty_document_test.main();
// smart_menu_test.main();

View File

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

View File

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

View File

@ -70,7 +70,7 @@ void main() {
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();
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(
name: names[i],
parentName: parentName,
@ -92,7 +92,7 @@ void main() {
// move the document_3 to the getting started page
await tester.movePageToOtherPage(
name: names[3],
parentName: gettingStated,
parentName: gettingStarted,
layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document,
);
@ -101,7 +101,7 @@ void main() {
.view
.parentViewId;
final toId = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view
.id;
expect(fromId, toId);
@ -109,13 +109,13 @@ void main() {
// move the document_2 before document_1
await tester.movePageToOtherPage(
name: names[2],
parentName: gettingStated,
parentName: gettingStarted,
layout: ViewLayoutPB.Document,
parentLayout: ViewLayoutPB.Document,
position: DraggableHoverPosition.bottom,
);
final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view
.childViews;
expect(
@ -170,7 +170,7 @@ void main() {
// it should not be moved
final childViews = tester
.widget<SingleInnerViewItem>(tester.findPageName(gettingStated))
.widget<SingleInnerViewItem>(tester.findPageName(gettingStarted))
.view
.childViews;
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/tabs_manager.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.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 'util/base.dart';
import 'util/common_operations.dart';
import 'util/expectation.dart';
import 'util/keyboard.dart';
const _readmeName = 'Read me';
const _documentName = 'Document';
const _calendarName = 'Calendar';
const _documentName = 'First Doc';
const _documentTwoName = 'Second Doc';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Tabs', () {
testWidgets('Open AppFlowy and open/navigate multiple tabs',
(tester) async {
testWidgets('Open AppFlowy and open/navigate/close tabs', (tester) async {
await tester.initializeAppFlowy();
await tester.tapGoButton();
@ -29,31 +32,21 @@ void main() {
findsNothing,
);
await tester.createNewPageWithName(
name: _calendarName,
layout: ViewLayoutPB.Calendar,
);
await tester.createNewPageWithName(
name: _documentName,
layout: ViewLayoutPB.Document,
);
// Navigate current view to "Read me" document again
await tester.tapButtonWithName(_readmeName);
await tester.createNewPageWithName(
name: _documentTwoName,
layout: ViewLayoutPB.Document,
);
/// 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
await tester.openAppInNewTab(_documentName);
expect(
find.descendant(
of: find.byType(TabsManager),
matching: find.byType(TabBar),
),
findsOneWidget,
);
await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document);
expect(
find.descendant(
@ -63,13 +56,32 @@ void main() {
findsNWidgets(3),
);
/// Navigate to the first tab
/// Navigate to the second tab
await tester.tap(
find.descendant(
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/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_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
@ -29,7 +28,7 @@ extension CommonOperations on WidgetTester {
/// Tap the + button on the home page.
Future<void> tapAddViewButton({
String name = gettingStated,
String name = gettingStarted,
}) async {
await hoverOnPageName(
name,
@ -211,6 +210,12 @@ extension CommonOperations on WidgetTester {
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.
Future<void> renamePage(String name) async {
await tapRenamePageButton();
@ -272,7 +277,7 @@ extension CommonOperations on WidgetTester {
bool openAfterCreated = true,
}) async {
// create a new page
await tapAddViewButton(name: parentName ?? gettingStated);
await tapAddViewButton(name: parentName ?? gettingStarted);
await tapButtonWithName(layout.menuName);
await pumpAndSettle();
@ -336,11 +341,14 @@ extension CommonOperations on WidgetTester {
await pumpAndSettle();
}
Future<void> openAppInNewTab(String name) async {
await hoverOnPageName(name);
await tap(find.byType(ViewDisclosureButton));
await pumpAndSettle();
await tap(find.text(LocaleKeys.disclosureAction_openNewTab.tr()));
Future<void> openAppInNewTab(String name, ViewLayoutPB layout) async {
await hoverOnPageName(
name,
onHover: () async {
await tapOpenInTabButton();
await pumpAndSettle();
},
);
await pumpAndSettle();
}

View File

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

View File

@ -11,13 +11,13 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter_test/flutter_test.dart';
// const String readme = 'Read me';
const String gettingStated = '⭐️ Getting started';
const String gettingStarted = '⭐️ Getting started';
extension Expectation on WidgetTester {
/// Expect to see the home page and with a default read me page.
void expectToSeeHomePage() {
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.

View File

@ -8,4 +8,12 @@ extension RawKeyboardExtension on RawKeyboard {
LogicalKeyboardKey.altRight,
].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);
await _fetchApps(emit);
},
openPage: (e) async {
emit(state.copyWith(plugin: e.plugin));
},
createApp: (_CreateApp event) async {
final result = await _workspaceService.createApp(
name: event.name,
@ -110,7 +107,6 @@ class MenuBloc extends Bloc<MenuEvent, MenuState> {
@freezed
class MenuEvent with _$MenuEvent {
const factory MenuEvent.initial() = _Initial;
const factory MenuEvent.openPage(Plugin plugin) = _OpenPage;
const factory MenuEvent.createApp(String name, {String? desc, int? index}) =
_CreateApp;
const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp;

View File

@ -1,6 +1,7 @@
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.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/menu/menu.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 {
event.when(
selectTab: (int index) {
if (index != state.currentIndex) {
if (index != state.currentIndex &&
index >= 0 &&
index < state.pages) {
emit(state.copyWith(newIndex: index));
_setLatestOpenView();
}
@ -31,6 +34,10 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
emit(state.closeView(pluginId));
_setLatestOpenView();
},
closeCurrentTab: () {
emit(state.closeView(state.currentPageManager.plugin.id));
_setLatestOpenView();
},
openTab: (Plugin plugin, ViewPB view) {
emit(state.openView(plugin, 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 {
const factory TabsEvent.moveTab() = _MoveTab;
const factory TabsEvent.closeTab(String pluginId) = _CloseTab;
const factory TabsEvent.closeCurrentTab() = _CloseCurrentTab;
const factory TabsEvent.selectTab(int index) = _SelectTab;
const factory TabsEvent.openTab({
required Plugin plugin,

View File

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

View File

@ -112,9 +112,13 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
ext: {},
openAfterCreate: e.openAfterCreated,
);
emit(
result.fold(
(l) => state.copyWith(successOrFailure: left(unit)),
(view) => state.copyWith(
lastCreatedView: view,
successOrFailure: left(unit),
),
(error) => state.copyWith(successOrFailure: right(error)),
),
);
@ -218,6 +222,7 @@ class ViewState with _$ViewState {
required bool isEditing,
required bool isExpanded,
required Either<Unit, FlowyError> successOrFailure,
@Default(null) ViewPB? lastCreatedView,
}) = _ViewState;
factory ViewState.init(ViewPB view) => ViewState(
@ -226,5 +231,6 @@ class ViewState with _$ViewState {
isExpanded: false,
isEditing: false,
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/home/home_setting_bloc.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.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 {
final Widget child;
const HomeHotKeys({required this.child, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final HotKey hotKey = HotKey(
KeyCode.backslash,
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
// Set hotkey scope (default is HotKeyScope.system)
scope: HotKeyScope.inapp, // Set as inapp-wide hotkey.
);
hotKeyManager.register(
hotKey,
keyDownHandler: (hotKey) {
context
.read<HomeSettingBloc>()
.add(const HomeSettingEvent.collapseMenu());
},
);
// Collapse sidebar menu
HotKeyItem(
hotKey: HotKey(
KeyCode.backslash,
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
// Set hotkey scope (default is HotKeyScope.system)
scope: HotKeyScope.inapp, // Set as inapp-wide hotkey.
),
keyDownHandler: (_) => context
.read<HomeSettingBloc>()
.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;
}
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));
break;
case ViewDisclosureAction.openInNewTab:
blocContext.read<TabsBloc>().add(
TabsEvent.openTab(
plugin: state.view.plugin(),
view: blocContext.read<ViewBloc>().state.view,
),
);
blocContext.read<TabsBloc>().openTab(state.view);
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/home/home_setting_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_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/workspace.pb.dart';
@ -49,34 +48,18 @@ class HomeMenu extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider<MenuBloc>(
create: (context) {
final menuBloc = MenuBloc(
user: user,
workspace: workspaceSetting.workspace,
);
menuBloc.add(const MenuEvent.initial());
return menuBloc;
},
create: (context) => MenuBloc(
user: user,
workspace: workspaceSetting.workspace,
)..add(const MenuEvent.initial()),
),
BlocProvider(
create: (ctx) =>
create: (context) =>
getIt<FavoriteBloc>()..add(const FavoriteEvent.initial()),
)
],
child: MultiBlocListener(
listeners: [
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),
),
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/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/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class FavoriteFolder extends StatelessWidget {
@ -24,6 +23,7 @@ class FavoriteFolder extends StatelessWidget {
if (views.isEmpty) {
return const SizedBox.shrink();
}
return BlocProvider<FolderBloc>(
create: (context) => FolderBloc(type: FolderCategoryType.favorite)
..add(
@ -54,11 +54,14 @@ class FavoriteFolder extends StatelessWidget {
view: view,
level: 0,
onSelected: (view) {
getIt<MenuSharedState>().latestOpenView = view;
context
.read<MenuBloc>()
.add(MenuEvent.openPage(view.plugin()));
if (RawKeyboard.instance.isControlPressed) {
context.read<TabsBloc>().openTab(view);
}
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/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/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_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class PersonalFolder extends StatelessWidget {
@ -52,13 +52,14 @@ class PersonalFolder extends StatelessWidget {
leftPadding: 16,
isFeedback: false,
onSelected: (view) {
getIt<TabsBloc>().add(
TabsEvent.openPlugin(
plugin: view.plugin(),
view: view,
),
);
if (RawKeyboard.instance.isControlPressed) {
context.read<TabsBloc>().openTab(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/menu/menu_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_trash.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-user/protobuf.dart'
show UserProfilePB;
@ -48,17 +48,18 @@ class HomeSideBar extends StatelessWidget {
],
child: BlocListener<MenuBloc, MenuState>(
listenWhen: (p, c) => p.plugin.id != c.plugin.id,
listener: (context, state) => getIt<TabsBloc>().add(
TabsEvent.openPlugin(plugin: state.plugin),
),
listener: (context, state) => context
.read<TabsBloc>()
.add(TabsEvent.openPlugin(plugin: state.plugin)),
child: Builder(
builder: (context) {
final menuState = context.watch<MenuBloc>().state;
final favoriteState = context.watch<FavoriteBloc>().state;
return _buildSidebar(
context,
menuState,
favoriteState,
menuState.views,
favoriteState.views,
);
},
),
@ -68,11 +69,10 @@ class HomeSideBar extends StatelessWidget {
Widget _buildSidebar(
BuildContext context,
MenuState state,
FavoriteState favoriteState,
List<ViewPB> views,
List<ViewPB> favoriteViews,
) {
final views = state.views;
return Container(
return DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
border: Border(
@ -95,7 +95,7 @@ class HomeSideBar extends StatelessWidget {
child: SingleChildScrollView(
child: SidebarFolder(
views: views,
favoriteViews: favoriteState.views,
favoriteViews: favoriteViews,
),
),
),

View File

@ -48,12 +48,7 @@ class SidebarNewPageButton extends StatelessWidget {
value: '',
confirm: (value) {
if (value.isNotEmpty) {
context.read<MenuBloc>().add(
MenuEvent.createApp(
value,
desc: '',
),
);
context.read<MenuBloc>().add(MenuEvent.createApp(value));
}
},
).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_bloc/flutter_bloc.dart';
typedef ViewItemOnSelected = void Function(ViewPB);
class ViewItem extends StatelessWidget {
const ViewItem({
super.key,
@ -28,6 +30,7 @@ class ViewItem extends StatelessWidget {
required this.level,
this.leftPadding = 10,
required this.onSelected,
this.onTertiarySelected,
this.isFirstChild = false,
this.isDraggable = true,
required this.isFeedback,
@ -46,7 +49,11 @@ class ViewItem extends StatelessWidget {
// the left padding of the each level = level * 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
// add top border to the first child
@ -62,7 +69,12 @@ class ViewItem extends StatelessWidget {
Widget build(BuildContext context) {
return BlocProvider(
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) {
// don't remove this code. it's related to the backend service.
view.childViews
@ -78,6 +90,7 @@ class ViewItem extends StatelessWidget {
showActions: state.isEditing,
isExpanded: state.isExpanded,
onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isFirstChild: isFirstChild,
isDraggable: isDraggable,
isFeedback: isFeedback,
@ -101,6 +114,7 @@ class InnerViewItem extends StatelessWidget {
required this.leftPadding,
required this.showActions,
required this.onSelected,
this.onTertiarySelected,
this.isFirstChild = false,
required this.isFeedback,
});
@ -120,7 +134,8 @@ class InnerViewItem extends StatelessWidget {
final double leftPadding;
final bool showActions;
final void Function(ViewPB) onSelected;
final ViewItemOnSelected onSelected;
final ViewItemOnSelected? onTertiarySelected;
@override
Widget build(BuildContext context) {
@ -131,6 +146,7 @@ class InnerViewItem extends StatelessWidget {
showActions: showActions,
categoryType: categoryType,
onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isExpanded: isExpanded,
isDraggable: isDraggable,
leftPadding: leftPadding,
@ -148,6 +164,7 @@ class InnerViewItem extends StatelessWidget {
view: childView,
level: level + 1,
onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isDraggable: isDraggable,
leftPadding: leftPadding,
isFeedback: isFeedback,
@ -176,6 +193,7 @@ class InnerViewItem extends StatelessWidget {
categoryType: categoryType,
level: level,
onSelected: onSelected,
onTertiarySelected: onTertiarySelected,
isDraggable: false,
leftPadding: leftPadding,
isFeedback: true,
@ -206,6 +224,7 @@ class SingleInnerViewItem extends StatefulWidget {
required this.categoryType,
required this.showActions,
required this.onSelected,
this.onTertiarySelected,
required this.isFeedback,
});
@ -220,7 +239,8 @@ class SingleInnerViewItem extends StatefulWidget {
final bool isDraggable;
final bool showActions;
final void Function(ViewPB) onSelected;
final ViewItemOnSelected onSelected;
final ViewItemOnSelected? onTertiarySelected;
final FolderCategoryType categoryType;
@override
@ -279,6 +299,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => widget.onSelected(widget.view),
onTertiaryTapDown: (_) => widget.onTertiarySelected?.call(widget.view),
child: SizedBox(
height: 26,
child: Padding(
@ -377,12 +398,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
context.read<ViewBloc>().add(const ViewEvent.duplicate());
break;
case ViewMoreActionType.openInNewTab:
context.read<TabsBloc>().add(
TabsEvent.openTab(
plugin: widget.view.plugin(),
view: widget.view,
),
);
context.read<TabsBloc>().openTab(widget.view);
break;
default:
throw UnsupportedError('$action is not supported');