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:
parent
d0747afbf2
commit
5b1afeb85d
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -42,6 +42,9 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
@override
|
||||
Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr());
|
||||
|
||||
@override
|
||||
Widget tabBarItem(String pluginId) => leftBarItem;
|
||||
|
||||
@override
|
||||
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/left_bar_item.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_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -210,6 +211,9 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: notifier.view);
|
||||
|
||||
@override
|
||||
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||
|
||||
@override
|
||||
Widget buildWidget({PluginContext? context}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
|
@ -9,6 +9,7 @@ import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.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/tab_bar_item.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -104,6 +105,9 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
@override
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||
|
||||
@override
|
||||
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
|
||||
|
||||
@override
|
||||
Widget? get rightBarItem {
|
||||
return Row(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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/presentation/widgets/pop_up_action.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:flowy_infra/image.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_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
@ -156,7 +156,11 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
case _ActionType.viewDatabase:
|
||||
getIt<MenuSharedState>().latestOpenView = viewPB;
|
||||
|
||||
getIt<HomeStackManager>().setPlugin(viewPB.plugin());
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: viewPB.plugin(),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case _ActionType.delete:
|
||||
final transaction = widget.editorState.transaction;
|
||||
|
@ -1,6 +1,6 @@
|
||||
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/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@ -31,8 +31,11 @@ class MenuTrash extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
getIt<MenuSharedState>().latestOpenView = null;
|
||||
getIt<HomeStackManager>()
|
||||
.setPlugin(makePlugin(pluginType: PluginType.trash));
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: makePlugin(pluginType: PluginType.trash),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: _render(context),
|
||||
),
|
||||
|
@ -51,6 +51,9 @@ class TrashPluginDisplay extends PluginWidgetBuilder {
|
||||
@override
|
||||
Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr());
|
||||
|
||||
@override
|
||||
Widget tabBarItem(String pluginId) => leftBarItem;
|
||||
|
||||
@override
|
||||
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/user_listener.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_service.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/presentation/router.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_backend/protobuf/flowy-folder2/view.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),
|
||||
);
|
||||
|
||||
//
|
||||
getIt.registerLazySingleton<HomeStackManager>(() => HomeStackManager());
|
||||
|
||||
getIt.registerFactoryParam<WelcomeBloc, UserProfilePB, void>(
|
||||
(user, _) => WelcomeBloc(
|
||||
userService: UserBackendService(userId: user.id),
|
||||
@ -122,6 +119,8 @@ void _resolveHomeDeps(GetIt getIt) {
|
||||
getIt.registerFactoryParam<DocShareBloc, ViewPB, void>(
|
||||
(view, _) => DocShareBloc(view: view),
|
||||
);
|
||||
|
||||
getIt.registerLazySingleton<TabsBloc>(() => TabsBloc());
|
||||
}
|
||||
|
||||
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_service.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/presentation/home/hotkeys.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) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<TabsBloc>.value(value: getIt<TabsBloc>()),
|
||||
BlocProvider<HomeBloc>(
|
||||
create: (context) {
|
||||
return HomeBloc(widget.user, widget.workspaceSetting)
|
||||
@ -74,14 +76,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
listener: (context, state) {
|
||||
final view = state.latestView;
|
||||
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.
|
||||
if (getIt<HomeStackManager>().plugin.pluginType ==
|
||||
final currentPageManager =
|
||||
context.read<TabsBloc>().state.currentPageManager;
|
||||
|
||||
if (currentPageManager.plugin.pluginType ==
|
||||
PluginType.blank) {
|
||||
getIt<HomeStackManager>().setPlugin(
|
||||
view.plugin(listenOnViewChanged: true),
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: view.plugin(listenOnViewChanged: true),
|
||||
),
|
||||
);
|
||||
getIt<MenuSharedState>().latestOpenView = view;
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -275,18 +281,22 @@ class HomeScreenStackAdaptor extends HomeStackDelegate {
|
||||
(parentView) {
|
||||
final List<ViewPB> views = parentView.childViews;
|
||||
if (views.isNotEmpty) {
|
||||
var lastView = views.last;
|
||||
ViewPB lastView = views.last;
|
||||
if (index != null && index != 0 && views.length > index - 1) {
|
||||
lastView = views[index - 1];
|
||||
}
|
||||
|
||||
getIt<MenuSharedState>().latestOpenView = lastView;
|
||||
getIt<HomeStackManager>().setPlugin(
|
||||
lastView.plugin(listenOnViewChanged: true),
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: lastView.plugin(listenOnViewChanged: true),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
getIt<MenuSharedState>().latestOpenView = null;
|
||||
getIt<HomeStackManager>().setPlugin(BlankPagePlugin());
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: BlankPagePlugin(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
|
@ -3,6 +3,8 @@ class HomeSizes {
|
||||
static const double topBarHeight = 60;
|
||||
static const double editPanelTopBarHeight = 60;
|
||||
static const double editPanelWidth = 400;
|
||||
static const double tabBarHeigth = 40;
|
||||
static const double tabBarWidth = 200;
|
||||
}
|
||||
|
||||
class HomeInsets {
|
||||
|
@ -2,14 +2,17 @@ import 'package:appflowy/core/frameless_window.dart';
|
||||
import 'package:appflowy/plugins/blank/blank.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.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/navigation.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.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/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:time/time.dart';
|
||||
|
||||
@ -32,27 +35,73 @@ class HomeStack extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
getIt<HomeStackManager>().stackTopBar(layout: layout),
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: FocusTraversalGroup(
|
||||
child: getIt<HomeStackManager>().stackWidget(
|
||||
onDeleted: (view, index) {
|
||||
delegate.didDeleteStackWidget(view, index);
|
||||
},
|
||||
final pageController = PageController();
|
||||
|
||||
return BlocProvider<TabsBloc>.value(
|
||||
value: getIt<TabsBloc>(),
|
||||
child: BlocBuilder<TabsBloc, TabsState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
TabsManager(pageController: pageController),
|
||||
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 {
|
||||
final int index;
|
||||
final List<Widget> children;
|
||||
@ -104,18 +153,20 @@ class FadingIndexedStackState extends State<FadingIndexedStack> {
|
||||
abstract mixin class NavigationItem {
|
||||
Widget get leftBarItem;
|
||||
Widget? get rightBarItem => null;
|
||||
Widget tabBarItem(String pluginId);
|
||||
|
||||
NavigationCallback get action => (id) {
|
||||
getIt<HomeStackManager>().setStackWithId(id);
|
||||
};
|
||||
NavigationCallback get action => (id) => throw UnimplementedError();
|
||||
}
|
||||
|
||||
class HomeStackNotifier extends ChangeNotifier {
|
||||
class PageNotifier extends ChangeNotifier {
|
||||
Plugin _plugin;
|
||||
|
||||
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);
|
||||
|
||||
/// This is the only place where the plugin is set.
|
||||
@ -133,10 +184,13 @@ class HomeStackNotifier extends ChangeNotifier {
|
||||
Plugin get plugin => _plugin;
|
||||
}
|
||||
|
||||
// HomeStack is initialized as singleton to control the page stack.
|
||||
class HomeStackManager {
|
||||
final HomeStackNotifier _notifier = HomeStackNotifier();
|
||||
HomeStackManager();
|
||||
// PageManager manages the view for one Tab
|
||||
class PageManager {
|
||||
final PageNotifier _notifier = PageNotifier();
|
||||
|
||||
PageNotifier get notifier => _notifier;
|
||||
|
||||
PageManager();
|
||||
|
||||
Widget title() {
|
||||
return _notifier.plugin.widgetBuilder.leftBarItem;
|
||||
@ -157,7 +211,7 @@ class HomeStackManager {
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: _notifier),
|
||||
],
|
||||
child: Selector<HomeStackNotifier, Widget>(
|
||||
child: Selector<PageNotifier, Widget>(
|
||||
selector: (context, notifier) => notifier.titleWidget,
|
||||
builder: (context, widget, child) {
|
||||
return MoveWindowDetector(child: HomeTopBar(layout: layout));
|
||||
@ -170,7 +224,7 @@ class HomeStackManager {
|
||||
return MultiProvider(
|
||||
providers: [ChangeNotifierProvider.value(value: _notifier)],
|
||||
child: Consumer(
|
||||
builder: (_, HomeStackNotifier notifier, __) {
|
||||
builder: (_, PageNotifier notifier, __) {
|
||||
return FadingIndexedStack(
|
||||
index: getIt<PluginSandbox>().indexOf(notifier.plugin.pluginType),
|
||||
children: getIt<PluginSandbox>().supportPluginTypes.map(
|
||||
@ -185,9 +239,9 @@ class HomeStackManager {
|
||||
padding: builder.contentPadding,
|
||||
child: pluginWidget,
|
||||
);
|
||||
} else {
|
||||
return const BlankPage();
|
||||
}
|
||||
|
||||
return const BlankPage();
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
@ -218,9 +272,9 @@ class HomeTopBar extends StatelessWidget {
|
||||
const FlowyNavigation(),
|
||||
const HSpace(16),
|
||||
ChangeNotifierProvider.value(
|
||||
value: Provider.of<HomeStackNotifier>(context, listen: false),
|
||||
value: Provider.of<PageNotifier>(context, listen: false),
|
||||
child: Consumer(
|
||||
builder: (_, HomeStackNotifier notifier, __) =>
|
||||
builder: (_, PageNotifier notifier, __) =>
|
||||
notifier.plugin.widgetBuilder.rightBarItem ??
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
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_ext.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:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class ViewSectionItem extends StatelessWidget {
|
||||
final bool isSelected;
|
||||
final ViewPB view;
|
||||
@ -115,6 +115,14 @@ class ViewSectionItem extends StatelessWidget {
|
||||
case ViewDisclosureAction.duplicate:
|
||||
blocContext.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
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,
|
||||
delete,
|
||||
duplicate,
|
||||
openInNewTab,
|
||||
}
|
||||
|
||||
extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||
@ -146,6 +155,8 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||
return LocaleKeys.disclosureAction_delete.tr();
|
||||
case ViewDisclosureAction.duplicate:
|
||||
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');
|
||||
case ViewDisclosureAction.duplicate:
|
||||
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/workspace/application/app/app_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/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -27,8 +27,10 @@ class ViewSection extends StatelessWidget {
|
||||
listener: (context, state) {
|
||||
if (state.selectedView != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
getIt<HomeStackManager>().setPlugin(
|
||||
state.selectedView!.plugin(listenOnViewChanged: true),
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openPlugin(
|
||||
plugin: state.selectedView!.plugin(listenOnViewChanged: true),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -73,10 +75,6 @@ class ViewSection extends StatelessWidget {
|
||||
}
|
||||
|
||||
bool _isViewSelected(ViewSectionState state, String viewId) {
|
||||
final view = state.selectedView;
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
return view.id == viewId;
|
||||
return state.selectedView?.id == viewId;
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,14 @@ import 'package:appflowy/plugins/trash/menu.dart';
|
||||
import 'package:appflowy/startup/startup.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/workspace/presentation/home/home_stack.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;
|
||||
import 'package:easy_localization/easy_localization.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/size.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
@ -63,7 +62,9 @@ class HomeMenu extends StatelessWidget {
|
||||
BlocListener<MenuBloc, MenuState>(
|
||||
listenWhen: (p, c) => p.plugin.id != c.plugin.id,
|
||||
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
|
||||
// receive: oldIndex: 0, newIndex: 2
|
||||
// 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
|
||||
.read<MenuBloc>()
|
||||
.add(MenuEvent.moveApp(oldIndex, index));
|
||||
|
@ -19,14 +19,9 @@ class NavigationNotifier with ChangeNotifier {
|
||||
List<NavigationItem> navigationItems;
|
||||
NavigationNotifier({required this.navigationItems});
|
||||
|
||||
void update(HomeStackNotifier notifier) {
|
||||
bool shouldNotify = false;
|
||||
void update(PageNotifier notifier) {
|
||||
if (navigationItems != notifier.plugin.widgetBuilder.navigationItems) {
|
||||
navigationItems = notifier.plugin.widgetBuilder.navigationItems;
|
||||
shouldNotify = true;
|
||||
}
|
||||
|
||||
if (shouldNotify) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -37,9 +32,9 @@ class FlowyNavigation extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProxyProvider<HomeStackNotifier, NavigationNotifier>(
|
||||
return ChangeNotifierProxyProvider<PageNotifier, NavigationNotifier>(
|
||||
create: (_) {
|
||||
final notifier = Provider.of<HomeStackNotifier>(context, listen: false);
|
||||
final notifier = Provider.of<PageNotifier>(context, listen: false);
|
||||
return NavigationNotifier(
|
||||
navigationItems: notifier.plugin.widgetBuilder.navigationItems,
|
||||
);
|
||||
@ -54,7 +49,6 @@ class FlowyNavigation extends StatelessWidget {
|
||||
builder: (ctx, items, child) => Expanded(
|
||||
child: Row(
|
||||
children: _renderNavigationItems(items),
|
||||
// crossAxisAlignment: WrapCrossAlignment.start,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -173,6 +167,9 @@ class EllipsisNaviItem extends NavigationItem {
|
||||
fontSize: FontSizes.s16,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget tabBarItem(String pluginId) => leftBarItem;
|
||||
|
||||
@override
|
||||
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": {
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
"duplicate": "Duplicate"
|
||||
"duplicate": "Duplicate",
|
||||
"openNewTab": "Open in a new tab"
|
||||
},
|
||||
"blankPageTitle": "Blank page",
|
||||
"newPageText": "New page",
|
||||
|
Loading…
Reference in New Issue
Block a user