mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: open apps in tabs (#2962)
* feat: open apps in tabs Closes: #2942 Relates: #2312 * fix: resolve comments * fix: unfocus editor to close toolbar on open/change tab * test: abstract open in a new tab helper
This commit is contained in:
69
frontend/appflowy_flutter/integration_test/tabs_test.dart
Normal file
69
frontend/appflowy_flutter/integration_test/tabs_test.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
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_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
|
||||||
|
import 'util/base.dart';
|
||||||
|
import 'util/common_operations.dart';
|
||||||
|
|
||||||
|
const _readmeName = 'Read me';
|
||||||
|
const _documentName = 'Document';
|
||||||
|
const _calendarName = 'Calendar';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('Tabs', () {
|
||||||
|
testWidgets('Open AppFlowy and open/navigate multiple tabs',
|
||||||
|
(tester) async {
|
||||||
|
await tester.initializeAppFlowy();
|
||||||
|
await tester.tapGoButton();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(TabsManager),
|
||||||
|
matching: find.byType(TabBar),
|
||||||
|
),
|
||||||
|
findsNothing,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.createNewPageWithName(ViewLayoutPB.Calendar, _calendarName);
|
||||||
|
await tester.createNewPageWithName(ViewLayoutPB.Document, _documentName);
|
||||||
|
|
||||||
|
// Navigate current view to "Read me" document again
|
||||||
|
await tester.tapButtonWithName(_readmeName);
|
||||||
|
|
||||||
|
/// Open second menu item in a new tab
|
||||||
|
await tester.openAppInNewTab(_calendarName);
|
||||||
|
|
||||||
|
/// Open third menu item in a new tab
|
||||||
|
await tester.openAppInNewTab(_documentName);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(TabsManager),
|
||||||
|
matching: find.byType(TabBar),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(TabBar),
|
||||||
|
matching: find.byType(FlowyTab),
|
||||||
|
),
|
||||||
|
findsNWidgets(3),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Navigate to the first tab
|
||||||
|
await tester.tap(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(FlowyTab),
|
||||||
|
matching: find.text(_readmeName),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -276,6 +276,14 @@ extension CommonOperations on WidgetTester {
|
|||||||
}
|
}
|
||||||
await pumpAndSettle();
|
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()));
|
||||||
|
await pumpAndSettle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ViewLayoutPBTest on ViewLayoutPB {
|
extension ViewLayoutPBTest on ViewLayoutPB {
|
||||||
|
@ -42,6 +42,9 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
@override
|
@override
|
||||||
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
|
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget tabBarItem(String pluginId) => leftBarItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildWidget({PluginContext? context}) => const BlankPage();
|
Widget buildWidget({PluginContext? context}) => const BlankPage();
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -210,6 +211,9 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
|||||||
@override
|
@override
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
|
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildWidget({PluginContext? context}) {
|
Widget buildWidget({PluginContext? context}) {
|
||||||
notifier.isDeleted.addListener(() {
|
notifier.isDeleted.addListener(() {
|
||||||
|
@ -9,6 +9,7 @@ import 'package:appflowy/plugins/util.dart';
|
|||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
import 'package:appflowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.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';
|
||||||
@ -104,6 +105,9 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
@override
|
@override
|
||||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? get rightBarItem {
|
Widget? get rightBarItem {
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
|
||||||
@ -13,7 +14,6 @@ import 'package:appflowy/generated/locale_keys.g.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:appflowy/workspace/application/view/view_ext.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/workspace/presentation/home/menu/menu.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
@ -156,7 +156,11 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
|||||||
case _ActionType.viewDatabase:
|
case _ActionType.viewDatabase:
|
||||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
getIt<MenuSharedState>().latestOpenView = viewPB;
|
||||||
|
|
||||||
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
getIt<TabsBloc>().add(
|
||||||
|
TabsEvent.openPlugin(
|
||||||
|
plugin: viewPB.plugin(),
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case _ActionType.delete:
|
case _ActionType.delete:
|
||||||
final transaction = widget.editorState.transaction;
|
final transaction = widget.editorState.transaction;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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/presentation/home/home_stack.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/menu.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
@ -31,8 +31,11 @@ class MenuTrash extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
getIt<MenuSharedState>().latestOpenView = null;
|
getIt<MenuSharedState>().latestOpenView = null;
|
||||||
getIt<HomeStackManager>()
|
getIt<TabsBloc>().add(
|
||||||
.setPlugin(makePlugin(pluginType: PluginType.trash));
|
TabsEvent.openPlugin(
|
||||||
|
plugin: makePlugin(pluginType: PluginType.trash),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
child: _render(context),
|
child: _render(context),
|
||||||
),
|
),
|
||||||
|
@ -51,6 +51,9 @@ class TrashPluginDisplay extends PluginWidgetBuilder {
|
|||||||
@override
|
@override
|
||||||
Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr());
|
Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr());
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget tabBarItem(String pluginId) => leftBarItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget? get rightBarItem => null;
|
Widget? get rightBarItem => null;
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import 'package:appflowy/user/application/auth/auth_service.dart';
|
|||||||
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
||||||
import 'package:appflowy/user/application/user_listener.dart';
|
import 'package:appflowy/user/application/user_listener.dart';
|
||||||
import 'package:appflowy/user/application/user_service.dart';
|
import 'package:appflowy/user/application/user_service.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
@ -23,7 +24,6 @@ import 'package:appflowy/workspace/application/settings/prelude.dart';
|
|||||||
import 'package:appflowy/user/application/prelude.dart';
|
import 'package:appflowy/user/application/prelude.dart';
|
||||||
import 'package:appflowy/user/presentation/router.dart';
|
import 'package:appflowy/user/presentation/router.dart';
|
||||||
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
import 'package:appflowy/plugins/trash/application/prelude.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';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||||
@ -108,9 +108,6 @@ void _resolveHomeDeps(GetIt getIt) {
|
|||||||
(user, _) => UserListener(userProfile: user),
|
(user, _) => UserListener(userProfile: user),
|
||||||
);
|
);
|
||||||
|
|
||||||
//
|
|
||||||
getIt.registerLazySingleton<HomeStackManager>(() => HomeStackManager());
|
|
||||||
|
|
||||||
getIt.registerFactoryParam<WelcomeBloc, UserProfilePB, void>(
|
getIt.registerFactoryParam<WelcomeBloc, UserProfilePB, void>(
|
||||||
(user, _) => WelcomeBloc(
|
(user, _) => WelcomeBloc(
|
||||||
userService: UserBackendService(userId: user.id),
|
userService: UserBackendService(userId: user.id),
|
||||||
@ -122,6 +119,8 @@ void _resolveHomeDeps(GetIt getIt) {
|
|||||||
getIt.registerFactoryParam<DocShareBloc, ViewPB, void>(
|
getIt.registerFactoryParam<DocShareBloc, ViewPB, void>(
|
||||||
(view, _) => DocShareBloc(view: view),
|
(view, _) => DocShareBloc(view: view),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
getIt.registerLazySingleton<TabsBloc>(() => TabsBloc());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resolveFolderDeps(GetIt getIt) {
|
void _resolveFolderDeps(GetIt getIt) {
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:appflowy/plugins/util.dart';
|
||||||
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
|
import 'package:appflowy/startup/startup.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';
|
||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'tabs_event.dart';
|
||||||
|
part 'tabs_state.dart';
|
||||||
|
part 'tabs_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class TabsBloc extends Bloc<TabsEvent, TabsState> {
|
||||||
|
late final MenuSharedState menuSharedState;
|
||||||
|
|
||||||
|
TabsBloc() : super(TabsState()) {
|
||||||
|
menuSharedState = getIt<MenuSharedState>();
|
||||||
|
|
||||||
|
on<TabsEvent>((event, emit) async {
|
||||||
|
event.when(
|
||||||
|
selectTab: (int index) {
|
||||||
|
if (index != state.currentIndex) {
|
||||||
|
emit(state.copyWith(newIndex: index));
|
||||||
|
_setLatestOpenView();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moveTab: () {},
|
||||||
|
closeTab: (String pluginId) {
|
||||||
|
emit(state.closeView(pluginId));
|
||||||
|
_setLatestOpenView();
|
||||||
|
},
|
||||||
|
openTab: (Plugin plugin, ViewPB view) {
|
||||||
|
emit(state.openView(plugin, view));
|
||||||
|
_setLatestOpenView(view);
|
||||||
|
},
|
||||||
|
openPlugin: (Plugin plugin, ViewPB? view) {
|
||||||
|
emit(state.openPlugin(plugin: plugin));
|
||||||
|
_setLatestOpenView(view);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setLatestOpenView([ViewPB? view]) {
|
||||||
|
if (view != null) {
|
||||||
|
menuSharedState.latestOpenView = view;
|
||||||
|
} else {
|
||||||
|
final pageManager = state.currentPageManager;
|
||||||
|
final notifier = pageManager.plugin.notifier;
|
||||||
|
if (notifier is ViewPluginNotifier) {
|
||||||
|
menuSharedState.latestOpenView = notifier.view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
part of 'tabs_bloc.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class TabsEvent with _$TabsEvent {
|
||||||
|
const factory TabsEvent.moveTab() = _MoveTab;
|
||||||
|
const factory TabsEvent.closeTab(String pluginId) = _CloseTab;
|
||||||
|
const factory TabsEvent.selectTab(int index) = _SelectTab;
|
||||||
|
const factory TabsEvent.openTab({
|
||||||
|
required Plugin plugin,
|
||||||
|
required ViewPB view,
|
||||||
|
}) = _OpenTab;
|
||||||
|
const factory TabsEvent.openPlugin({required Plugin plugin, ViewPB? view}) =
|
||||||
|
_OpenPlugin;
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
part of 'tabs_bloc.dart';
|
||||||
|
|
||||||
|
class TabsState {
|
||||||
|
final int currentIndex;
|
||||||
|
|
||||||
|
final List<PageManager> _pageManagers;
|
||||||
|
int get pages => _pageManagers.length;
|
||||||
|
PageManager get currentPageManager => _pageManagers[currentIndex];
|
||||||
|
List<PageManager> get pageManagers => _pageManagers;
|
||||||
|
|
||||||
|
TabsState({
|
||||||
|
this.currentIndex = 0,
|
||||||
|
List<PageManager>? pageManagers,
|
||||||
|
}) : _pageManagers = pageManagers ?? [PageManager()];
|
||||||
|
|
||||||
|
/// This opens a new tab given a [Plugin] and a [View].
|
||||||
|
///
|
||||||
|
/// If the [Plugin.id] is already associated with an open tab,
|
||||||
|
/// then it selects that tab.
|
||||||
|
///
|
||||||
|
TabsState openView(Plugin plugin, ViewPB view) {
|
||||||
|
final selectExistingPlugin = _selectPluginIfOpen(plugin.id);
|
||||||
|
|
||||||
|
if (selectExistingPlugin == null) {
|
||||||
|
_pageManagers.add(PageManager()..setPlugin(plugin));
|
||||||
|
|
||||||
|
return copyWith(newIndex: pages - 1, pageManagers: [..._pageManagers]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectExistingPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
TabsState closeView(String pluginId) {
|
||||||
|
_pageManagers.removeWhere((pm) => pm.plugin.id == pluginId);
|
||||||
|
|
||||||
|
/// If currentIndex is greater than the amount of allowed indices
|
||||||
|
/// And the current selected tab isn't the first (index 0)
|
||||||
|
/// as currentIndex cannot be -1
|
||||||
|
/// Then decrease currentIndex by 1
|
||||||
|
final newIndex = currentIndex > pages - 1 && currentIndex > 0
|
||||||
|
? currentIndex - 1
|
||||||
|
: currentIndex;
|
||||||
|
|
||||||
|
return copyWith(
|
||||||
|
newIndex: newIndex,
|
||||||
|
pageManagers: [..._pageManagers],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This opens a plugin in the current selected tab,
|
||||||
|
/// due to how Document currently works, only one tab
|
||||||
|
/// per plugin can currently be active.
|
||||||
|
///
|
||||||
|
/// If the plugin is already open in a tab, then that tab
|
||||||
|
/// will become selected.
|
||||||
|
///
|
||||||
|
TabsState openPlugin({required Plugin plugin}) {
|
||||||
|
final selectExistingPlugin = _selectPluginIfOpen(plugin.id);
|
||||||
|
|
||||||
|
if (selectExistingPlugin == null) {
|
||||||
|
final pageManagers = [..._pageManagers];
|
||||||
|
pageManagers[currentIndex].setPlugin(plugin);
|
||||||
|
|
||||||
|
return copyWith(pageManagers: pageManagers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectExistingPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a [Plugin.id] is already associated with an open tab.
|
||||||
|
/// Returns a [TabState] with new index if there is a match.
|
||||||
|
///
|
||||||
|
/// If no match it returns null
|
||||||
|
///
|
||||||
|
TabsState? _selectPluginIfOpen(String id) {
|
||||||
|
final index = _pageManagers.indexWhere((pm) => pm.plugin.id == id);
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyWith(newIndex: index);
|
||||||
|
}
|
||||||
|
|
||||||
|
TabsState copyWith({
|
||||||
|
int? newIndex,
|
||||||
|
List<PageManager>? pageManagers,
|
||||||
|
}) =>
|
||||||
|
TabsState(
|
||||||
|
currentIndex: newIndex ?? currentIndex,
|
||||||
|
pageManagers: pageManagers ?? _pageManagers,
|
||||||
|
);
|
||||||
|
}
|
@ -5,6 +5,7 @@ import 'package:appflowy/workspace/application/appearance.dart';
|
|||||||
import 'package:appflowy/workspace/application/home/home_bloc.dart';
|
import 'package:appflowy/workspace/application/home/home_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/home/home_service.dart';
|
import 'package:appflowy/workspace/application/home/home_service.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:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';
|
import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart';
|
||||||
@ -39,6 +40,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),
|
||||||
BlocProvider<HomeBloc>(
|
BlocProvider<HomeBloc>(
|
||||||
create: (context) {
|
create: (context) {
|
||||||
return HomeBloc(widget.user, widget.workspaceSetting)
|
return HomeBloc(widget.user, widget.workspaceSetting)
|
||||||
@ -74,14 +76,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
final view = state.latestView;
|
final view = state.latestView;
|
||||||
if (view != null) {
|
if (view != null) {
|
||||||
// Only open the last opened view if the [HomeStackManager] current opened plugin is blank and the last opened view is not null.
|
// Only open the last opened view if the [TabsState.currentPageManager] current opened plugin is blank and the last opened view is not null.
|
||||||
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
|
// All opened widgets that display on the home screen are in the form of plugins. There is a list of built-in plugins defined in the [PluginType] enum, including board, grid and trash.
|
||||||
if (getIt<HomeStackManager>().plugin.pluginType ==
|
final currentPageManager =
|
||||||
|
context.read<TabsBloc>().state.currentPageManager;
|
||||||
|
|
||||||
|
if (currentPageManager.plugin.pluginType ==
|
||||||
PluginType.blank) {
|
PluginType.blank) {
|
||||||
getIt<HomeStackManager>().setPlugin(
|
getIt<TabsBloc>().add(
|
||||||
view.plugin(listenOnViewChanged: true),
|
TabsEvent.openPlugin(
|
||||||
|
plugin: view.plugin(listenOnViewChanged: true),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
getIt<MenuSharedState>().latestOpenView = view;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -275,18 +281,22 @@ class HomeScreenStackAdaptor extends HomeStackDelegate {
|
|||||||
(parentView) {
|
(parentView) {
|
||||||
final List<ViewPB> views = parentView.childViews;
|
final List<ViewPB> views = parentView.childViews;
|
||||||
if (views.isNotEmpty) {
|
if (views.isNotEmpty) {
|
||||||
var lastView = views.last;
|
ViewPB lastView = views.last;
|
||||||
if (index != null && index != 0 && views.length > index - 1) {
|
if (index != null && index != 0 && views.length > index - 1) {
|
||||||
lastView = views[index - 1];
|
lastView = views[index - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
getIt<MenuSharedState>().latestOpenView = lastView;
|
getIt<TabsBloc>().add(
|
||||||
getIt<HomeStackManager>().setPlugin(
|
TabsEvent.openPlugin(
|
||||||
lastView.plugin(listenOnViewChanged: true),
|
plugin: lastView.plugin(listenOnViewChanged: true),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
getIt<MenuSharedState>().latestOpenView = null;
|
getIt<TabsBloc>().add(
|
||||||
getIt<HomeStackManager>().setPlugin(BlankPagePlugin());
|
TabsEvent.openPlugin(
|
||||||
|
plugin: BlankPagePlugin(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
|
@ -3,6 +3,8 @@ class HomeSizes {
|
|||||||
static const double topBarHeight = 60;
|
static const double topBarHeight = 60;
|
||||||
static const double editPanelTopBarHeight = 60;
|
static const double editPanelTopBarHeight = 60;
|
||||||
static const double editPanelWidth = 400;
|
static const double editPanelWidth = 400;
|
||||||
|
static const double tabBarHeigth = 40;
|
||||||
|
static const double tabBarWidth = 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeInsets {
|
class HomeInsets {
|
||||||
|
@ -2,14 +2,17 @@ import 'package:appflowy/core/frameless_window.dart';
|
|||||||
import 'package:appflowy/plugins/blank/blank.dart';
|
import 'package:appflowy/plugins/blank/blank.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/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/workspace/presentation/home/navigation.dart';
|
import 'package:appflowy/workspace/presentation/home/navigation.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
import 'package:flowy_infra_ui/style_widget/extension.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:time/time.dart';
|
import 'package:time/time.dart';
|
||||||
|
|
||||||
@ -32,27 +35,73 @@ class HomeStack extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
final pageController = PageController();
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
return BlocProvider<TabsBloc>.value(
|
||||||
getIt<HomeStackManager>().stackTopBar(layout: layout),
|
value: getIt<TabsBloc>(),
|
||||||
Expanded(
|
child: BlocBuilder<TabsBloc, TabsState>(
|
||||||
child: Container(
|
builder: (context, state) {
|
||||||
color: Theme.of(context).colorScheme.surface,
|
return Column(
|
||||||
child: FocusTraversalGroup(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
child: getIt<HomeStackManager>().stackWidget(
|
children: [
|
||||||
onDeleted: (view, index) {
|
TabsManager(pageController: pageController),
|
||||||
delegate.didDeleteStackWidget(view, index);
|
state.currentPageManager.stackTopBar(layout: layout),
|
||||||
},
|
Expanded(
|
||||||
|
child: PageView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
controller: pageController,
|
||||||
|
children: state.pageManagers
|
||||||
|
.map(
|
||||||
|
(pm) => PageStack(pageManager: pm, delegate: delegate),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PageStack extends StatefulWidget {
|
||||||
|
const PageStack({
|
||||||
|
super.key,
|
||||||
|
required this.pageManager,
|
||||||
|
required this.delegate,
|
||||||
|
});
|
||||||
|
|
||||||
|
final PageManager pageManager;
|
||||||
|
|
||||||
|
final HomeStackDelegate delegate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PageStack> createState() => _PageStackState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PageStackState extends State<PageStack>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: FocusTraversalGroup(
|
||||||
|
child: widget.pageManager.stackWidget(
|
||||||
|
onDeleted: (view, index) {
|
||||||
|
widget.delegate.didDeleteStackWidget(view, index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
||||||
|
|
||||||
class FadingIndexedStack extends StatefulWidget {
|
class FadingIndexedStack extends StatefulWidget {
|
||||||
final int index;
|
final int index;
|
||||||
final List<Widget> children;
|
final List<Widget> children;
|
||||||
@ -104,18 +153,20 @@ class FadingIndexedStackState extends State<FadingIndexedStack> {
|
|||||||
abstract mixin class NavigationItem {
|
abstract mixin class NavigationItem {
|
||||||
Widget get leftBarItem;
|
Widget get leftBarItem;
|
||||||
Widget? get rightBarItem => null;
|
Widget? get rightBarItem => null;
|
||||||
|
Widget tabBarItem(String pluginId);
|
||||||
|
|
||||||
NavigationCallback get action => (id) {
|
NavigationCallback get action => (id) => throw UnimplementedError();
|
||||||
getIt<HomeStackManager>().setStackWithId(id);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeStackNotifier extends ChangeNotifier {
|
class PageNotifier extends ChangeNotifier {
|
||||||
Plugin _plugin;
|
Plugin _plugin;
|
||||||
|
|
||||||
Widget get titleWidget => _plugin.widgetBuilder.leftBarItem;
|
Widget get titleWidget => _plugin.widgetBuilder.leftBarItem;
|
||||||
|
|
||||||
HomeStackNotifier({Plugin? plugin})
|
Widget tabBarWidget(String pluginId) =>
|
||||||
|
_plugin.widgetBuilder.tabBarItem(pluginId);
|
||||||
|
|
||||||
|
PageNotifier({Plugin? plugin})
|
||||||
: _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);
|
: _plugin = plugin ?? makePlugin(pluginType: PluginType.blank);
|
||||||
|
|
||||||
/// This is the only place where the plugin is set.
|
/// This is the only place where the plugin is set.
|
||||||
@ -133,10 +184,13 @@ class HomeStackNotifier extends ChangeNotifier {
|
|||||||
Plugin get plugin => _plugin;
|
Plugin get plugin => _plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HomeStack is initialized as singleton to control the page stack.
|
// PageManager manages the view for one Tab
|
||||||
class HomeStackManager {
|
class PageManager {
|
||||||
final HomeStackNotifier _notifier = HomeStackNotifier();
|
final PageNotifier _notifier = PageNotifier();
|
||||||
HomeStackManager();
|
|
||||||
|
PageNotifier get notifier => _notifier;
|
||||||
|
|
||||||
|
PageManager();
|
||||||
|
|
||||||
Widget title() {
|
Widget title() {
|
||||||
return _notifier.plugin.widgetBuilder.leftBarItem;
|
return _notifier.plugin.widgetBuilder.leftBarItem;
|
||||||
@ -157,7 +211,7 @@ class HomeStackManager {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider.value(value: _notifier),
|
ChangeNotifierProvider.value(value: _notifier),
|
||||||
],
|
],
|
||||||
child: Selector<HomeStackNotifier, Widget>(
|
child: Selector<PageNotifier, Widget>(
|
||||||
selector: (context, notifier) => notifier.titleWidget,
|
selector: (context, notifier) => notifier.titleWidget,
|
||||||
builder: (context, widget, child) {
|
builder: (context, widget, child) {
|
||||||
return MoveWindowDetector(child: HomeTopBar(layout: layout));
|
return MoveWindowDetector(child: HomeTopBar(layout: layout));
|
||||||
@ -170,7 +224,7 @@ class HomeStackManager {
|
|||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [ChangeNotifierProvider.value(value: _notifier)],
|
providers: [ChangeNotifierProvider.value(value: _notifier)],
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (_, HomeStackNotifier notifier, __) {
|
builder: (_, PageNotifier notifier, __) {
|
||||||
return FadingIndexedStack(
|
return FadingIndexedStack(
|
||||||
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
|
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
|
||||||
children: getIt<PluginSandbox>().supportPluginTypes.map(
|
children: getIt<PluginSandbox>().supportPluginTypes.map(
|
||||||
@ -185,9 +239,9 @@ class HomeStackManager {
|
|||||||
padding: builder.contentPadding,
|
padding: builder.contentPadding,
|
||||||
child: pluginWidget,
|
child: pluginWidget,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return const BlankPage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return const BlankPage();
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
);
|
);
|
||||||
@ -218,9 +272,9 @@ class HomeTopBar extends StatelessWidget {
|
|||||||
const FlowyNavigation(),
|
const FlowyNavigation(),
|
||||||
const HSpace(16),
|
const HSpace(16),
|
||||||
ChangeNotifierProvider.value(
|
ChangeNotifierProvider.value(
|
||||||
value: Provider.of<HomeStackNotifier>(context, listen: false),
|
value: Provider.of<PageNotifier>(context, listen: false),
|
||||||
child: Consumer(
|
child: Consumer(
|
||||||
builder: (_, HomeStackNotifier notifier, __) =>
|
builder: (_, PageNotifier notifier, __) =>
|
||||||
notifier.plugin.widgetBuilder.rightBarItem ??
|
notifier.plugin.widgetBuilder.rightBarItem ??
|
||||||
const SizedBox.shrink(),
|
const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||||
@ -18,7 +19,6 @@ import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
|||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
|
||||||
class ViewSectionItem extends StatelessWidget {
|
class ViewSectionItem extends StatelessWidget {
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
@ -115,6 +115,14 @@ class ViewSectionItem extends StatelessWidget {
|
|||||||
case ViewDisclosureAction.duplicate:
|
case ViewDisclosureAction.duplicate:
|
||||||
blocContext.read<ViewBloc>().add(const ViewEvent.duplicate());
|
blocContext.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||||
break;
|
break;
|
||||||
|
case ViewDisclosureAction.openInNewTab:
|
||||||
|
blocContext.read<TabsBloc>().add(
|
||||||
|
TabsEvent.openTab(
|
||||||
|
plugin: state.view.plugin(),
|
||||||
|
view: blocContext.read<ViewBloc>().state.view,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -135,6 +143,7 @@ enum ViewDisclosureAction {
|
|||||||
rename,
|
rename,
|
||||||
delete,
|
delete,
|
||||||
duplicate,
|
duplicate,
|
||||||
|
openInNewTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ViewDisclosureExtension on ViewDisclosureAction {
|
extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||||
@ -146,6 +155,8 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
|
|||||||
return LocaleKeys.disclosureAction_delete.tr();
|
return LocaleKeys.disclosureAction_delete.tr();
|
||||||
case ViewDisclosureAction.duplicate:
|
case ViewDisclosureAction.duplicate:
|
||||||
return LocaleKeys.disclosureAction_duplicate.tr();
|
return LocaleKeys.disclosureAction_duplicate.tr();
|
||||||
|
case ViewDisclosureAction.openInNewTab:
|
||||||
|
return LocaleKeys.disclosureAction_openNewTab.tr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +168,8 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
|
|||||||
return const FlowySvg(name: 'editor/delete');
|
return const FlowySvg(name: 'editor/delete');
|
||||||
case ViewDisclosureAction.duplicate:
|
case ViewDisclosureAction.duplicate:
|
||||||
return const FlowySvg(name: 'editor/copy');
|
return const FlowySvg(name: 'editor/copy');
|
||||||
|
case ViewDisclosureAction.openInNewTab:
|
||||||
|
return const FlowySvg(name: 'grid/expander');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
import 'package:appflowy/workspace/application/app/app_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart';
|
import 'package:appflowy/workspace/application/menu/menu_view_section_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.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/workspace/presentation/home/menu/menu.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';
|
||||||
@ -27,8 +27,10 @@ class ViewSection extends StatelessWidget {
|
|||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.selectedView != null) {
|
if (state.selectedView != null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
getIt<HomeStackManager>().setPlugin(
|
getIt<TabsBloc>().add(
|
||||||
state.selectedView!.plugin(listenOnViewChanged: true),
|
TabsEvent.openPlugin(
|
||||||
|
plugin: state.selectedView!.plugin(listenOnViewChanged: true),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -73,10 +75,6 @@ class ViewSection extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _isViewSelected(ViewSectionState state, String viewId) {
|
bool _isViewSelected(ViewSectionState state, String viewId) {
|
||||||
final view = state.selectedView;
|
return state.selectedView?.id == viewId;
|
||||||
if (view == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return view.id == viewId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,14 @@ import 'package:appflowy/plugins/trash/menu.dart';
|
|||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.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/workspace/presentation/home/home_stack.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';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||||
show UserProfilePB;
|
show UserProfilePB;
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:expandable/expandable.dart';
|
import 'package:expandable/expandable.dart';
|
||||||
// import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
|
||||||
import 'package:flowy_infra/image.dart';
|
import 'package:flowy_infra/image.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
import 'package:flowy_infra/size.dart';
|
||||||
import 'package:flowy_infra/time/duration.dart';
|
import 'package:flowy_infra/time/duration.dart';
|
||||||
@ -63,7 +62,9 @@ class HomeMenu extends StatelessWidget {
|
|||||||
BlocListener<MenuBloc, MenuState>(
|
BlocListener<MenuBloc, MenuState>(
|
||||||
listenWhen: (p, c) => p.plugin.id != c.plugin.id,
|
listenWhen: (p, c) => p.plugin.id != c.plugin.id,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
getIt<HomeStackManager>().setPlugin(state.plugin);
|
getIt<TabsBloc>().add(
|
||||||
|
TabsEvent.openPlugin(plugin: state.plugin),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -131,7 +132,8 @@ class HomeMenu extends StatelessWidget {
|
|||||||
// expect: oldIndex: 0, newIndex: 1
|
// expect: oldIndex: 0, newIndex: 1
|
||||||
// receive: oldIndex: 0, newIndex: 2
|
// receive: oldIndex: 0, newIndex: 2
|
||||||
// Workaround: if newIndex > oldIndex, we just minus one
|
// Workaround: if newIndex > oldIndex, we just minus one
|
||||||
final int index = newIndex > oldIndex ? newIndex - 1 : newIndex;
|
final int index =
|
||||||
|
newIndex > oldIndex ? newIndex - 1 : newIndex;
|
||||||
context
|
context
|
||||||
.read<MenuBloc>()
|
.read<MenuBloc>()
|
||||||
.add(MenuEvent.moveApp(oldIndex, index));
|
.add(MenuEvent.moveApp(oldIndex, index));
|
||||||
|
@ -19,14 +19,9 @@ class NavigationNotifier with ChangeNotifier {
|
|||||||
List<NavigationItem> navigationItems;
|
List<NavigationItem> navigationItems;
|
||||||
NavigationNotifier({required this.navigationItems});
|
NavigationNotifier({required this.navigationItems});
|
||||||
|
|
||||||
void update(HomeStackNotifier notifier) {
|
void update(PageNotifier notifier) {
|
||||||
bool shouldNotify = false;
|
|
||||||
if (navigationItems != notifier.plugin.widgetBuilder.navigationItems) {
|
if (navigationItems != notifier.plugin.widgetBuilder.navigationItems) {
|
||||||
navigationItems = notifier.plugin.widgetBuilder.navigationItems;
|
navigationItems = notifier.plugin.widgetBuilder.navigationItems;
|
||||||
shouldNotify = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldNotify) {
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,9 +32,9 @@ class FlowyNavigation extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProxyProvider<HomeStackNotifier, NavigationNotifier>(
|
return ChangeNotifierProxyProvider<PageNotifier, NavigationNotifier>(
|
||||||
create: (_) {
|
create: (_) {
|
||||||
final notifier = Provider.of<HomeStackNotifier>(context, listen: false);
|
final notifier = Provider.of<PageNotifier>(context, listen: false);
|
||||||
return NavigationNotifier(
|
return NavigationNotifier(
|
||||||
navigationItems: notifier.plugin.widgetBuilder.navigationItems,
|
navigationItems: notifier.plugin.widgetBuilder.navigationItems,
|
||||||
);
|
);
|
||||||
@ -54,7 +49,6 @@ class FlowyNavigation extends StatelessWidget {
|
|||||||
builder: (ctx, items, child) => Expanded(
|
builder: (ctx, items, child) => Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: _renderNavigationItems(items),
|
children: _renderNavigationItems(items),
|
||||||
// crossAxisAlignment: WrapCrossAlignment.start,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -173,6 +167,9 @@ class EllipsisNaviItem extends NavigationItem {
|
|||||||
fontSize: FontSizes.s16,
|
fontSize: FontSizes.s16,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget tabBarItem(String pluginId) => leftBarItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
NavigationCallback get action => (id) {};
|
NavigationCallback get action => (id) {};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
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:flowy_infra/image.dart';
|
||||||
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class FlowyTab extends StatefulWidget {
|
||||||
|
final PageManager pageManager;
|
||||||
|
final bool isCurrent;
|
||||||
|
|
||||||
|
const FlowyTab({
|
||||||
|
super.key,
|
||||||
|
required this.pageManager,
|
||||||
|
required this.isCurrent,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FlowyTab> createState() => _FlowyTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlowyTabState extends State<FlowyTab> {
|
||||||
|
bool _isHovering = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTertiaryTapUp: _closeTab,
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (_) => _setHovering(true),
|
||||||
|
onExit: (_) => _setHovering(),
|
||||||
|
child: Container(
|
||||||
|
width: HomeSizes.tabBarWidth,
|
||||||
|
height: HomeSizes.tabBarHeigth,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getBackgroundColor(),
|
||||||
|
),
|
||||||
|
child: ChangeNotifierProvider.value(
|
||||||
|
value: widget.pageManager.notifier,
|
||||||
|
child: Consumer<PageNotifier>(
|
||||||
|
builder: (context, value, child) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: widget.pageManager.notifier
|
||||||
|
.tabBarWidget(widget.pageManager.plugin.id),
|
||||||
|
),
|
||||||
|
Visibility(
|
||||||
|
visible: _isHovering,
|
||||||
|
child: FlowyIconButton(
|
||||||
|
onPressed: _closeTab,
|
||||||
|
icon: const FlowySvg(
|
||||||
|
name: 'editor/close',
|
||||||
|
size: Size.fromWidth(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setHovering([bool isHovering = false]) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isHovering = isHovering);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getBackgroundColor() {
|
||||||
|
if (widget.isCurrent) {
|
||||||
|
return Theme.of(context).colorScheme.onSecondaryContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isHovering) {
|
||||||
|
return AFThemeExtension.of(context).lightGreyHover;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Theme.of(context).colorScheme.surfaceVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _closeTab([TapUpDetails? details]) => context
|
||||||
|
.read<TabsBloc>()
|
||||||
|
.add(TabsEvent.closeTab(widget.pageManager.plugin.id));
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
class TabsManager extends StatefulWidget {
|
||||||
|
final PageController pageController;
|
||||||
|
|
||||||
|
const TabsManager({
|
||||||
|
super.key,
|
||||||
|
required this.pageController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider<TabsBloc>.value(
|
||||||
|
value: BlocProvider.of<TabsBloc>(context),
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: BlocBuilder<TabsBloc, TabsState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (_controller.length == 1) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
height: HomeSizes.tabBarHeigth,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
|
||||||
|
/// 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) =>
|
||||||
|
context.read<TabsBloc>().add(TabsEvent.selectTab(newIndex)),
|
||||||
|
tabs: state.pageManagers
|
||||||
|
.map(
|
||||||
|
(pm) => FlowyTab(
|
||||||
|
key: UniqueKey(),
|
||||||
|
pageManager: pm,
|
||||||
|
isCurrent: state.currentPageManager == pm,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ViewTabBarItem extends StatefulWidget {
|
||||||
|
final ViewPB view;
|
||||||
|
|
||||||
|
const ViewTabBarItem({
|
||||||
|
super.key,
|
||||||
|
required this.view,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ViewTabBarItem> createState() => _ViewTabBarItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ViewTabBarItemState extends State<ViewTabBarItem> {
|
||||||
|
late final ViewListener _viewListener;
|
||||||
|
late ViewPB view;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
view = widget.view;
|
||||||
|
_viewListener = ViewListener(viewId: widget.view.id);
|
||||||
|
_viewListener.start(
|
||||||
|
onViewUpdated: (updatedView) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => view = updatedView);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_viewListener.stop();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FlowyText.medium(view.name);
|
||||||
|
}
|
||||||
|
}
|
@ -61,7 +61,8 @@
|
|||||||
"disclosureAction": {
|
"disclosureAction": {
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"duplicate": "Duplicate"
|
"duplicate": "Duplicate",
|
||||||
|
"openNewTab": "Open in a new tab"
|
||||||
},
|
},
|
||||||
"blankPageTitle": "Blank page",
|
"blankPageTitle": "Blank page",
|
||||||
"newPageText": "New page",
|
"newPageText": "New page",
|
||||||
|
Reference in New Issue
Block a user