From 72b03dd01b4671472247701ce5ecadb43eeecea1 Mon Sep 17 00:00:00 2001 From: squidrye Date: Fri, 22 Sep 2023 20:12:55 +0530 Subject: [PATCH] feat: added absorbpointer for duplicated views --- .../lib/core/config/kv_keys.dart | 6 + .../appflowy_flutter/lib/startup/startup.dart | 5 + .../application/tabs/tabs_controller.dart | 160 ++++++++++++++++++ .../application/tabs/tabs_service.dart | 45 +++++ .../presentation/home/home_stack.dart | 68 +++++++- .../presentation/home/tabs/tabs_manager.dart | 4 +- 6 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_controller.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_service.dart diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index 72b0988d5b0b..058c7308bcd2 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -49,4 +49,10 @@ class KVKeys { /// The value is a boolean string. static const String showRenameDialogWhenCreatingNewFile = 'showRenameDialogWhenCreatingNewFile'; + + ///The key for saving list of open views when using multi-pane feature + /// + ///The value is a json string with following format: + /// openedPlugins: {'pluginId': true, 'pluginId':false} + static const String openedPlugins = 'openedPlugins'; } diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index ef64c30e699b..119742dea99e 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:flutter/foundation.dart'; @@ -50,6 +52,9 @@ class FlowyRunner { (value) => Directory(value), ); + // remove panes shared preference + await getIt().remove(KVKeys.openedPlugins); + // add task final launcher = getIt(); launcher.addTasks( diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_controller.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_controller.dart new file mode 100644 index 000000000000..92fb6604b23b --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_controller.dart @@ -0,0 +1,160 @@ +import 'package:appflowy/plugins/util.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_service.dart'; +import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; +import 'package:appflowy/workspace/presentation/home/tabs/draggable_tab_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:flutter/material.dart'; + +class TabsController extends ChangeNotifier { + int currentIndex; + List pageManagers; + int get pages => pageManagers.length; + PageManager get currentPageManager => pageManagers[currentIndex]; + final MenuSharedState menuSharedState; + + TabsController({int? currentIndex, List? pageManagers}) + : pageManagers = pageManagers ?? [PageManager()], + menuSharedState = getIt(), + currentIndex = currentIndex ?? 0; + + Future closeAllViews() async { + for (final page in pageManagers) { + closeView(page.plugin.id, closePaneSubRoutine: true); + } + } + + void openView(Plugin plugin) async { + final selectExistingPlugin = _selectPluginIfOpen(plugin.id); + + if (!selectExistingPlugin) { + final readOnly = await TabsService.setPluginOpenedInCache(plugin); + pageManagers.add(PageManager() + ..setPlugin(plugin) + ..setReadOnlyStatus(readOnly)); + } + currentIndex = pageManagers.length - 1; + + setLatestOpenView(); + notifyListeners(); + } + + void closeView(String pluginId, {bool? closePaneSubRoutine}) async { + // Avoid closing the only open tab + if (pageManagers.length == 1) { + if (closePaneSubRoutine ?? false) { + await TabsService.setPluginClosedInCache(pluginId); + } + return; + } + await TabsService.setPluginClosedInCache(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 + currentIndex = currentIndex > pageManagers.length - 1 && currentIndex > 0 + ? currentIndex - 1 + : currentIndex; + + setLatestOpenView(); + notifyListeners(); + } + + /// 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 + /// + bool _selectPluginIfOpen(String id) { + final index = pageManagers.indexWhere((pm) => pm.plugin.id == id); + if (index == -1) { + return false; + } + currentIndex = index; + notifyListeners(); + return true; + } + + /// 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. + /// + void openPlugin({required Plugin plugin}) async { + final selectExistingPlugin = _selectPluginIfOpen(plugin.id); + + if (!selectExistingPlugin) { + await TabsService.setPluginClosedInCache(currentPageManager.plugin.id); + final readOnly = await TabsService.setPluginOpenedInCache(plugin); + pageManagers[currentIndex] + ..setPlugin(plugin) + ..setReadOnlyStatus(readOnly); + } + setLatestOpenView(); + notifyListeners(); + } + + void selectTab({required int index}) { + if (index != currentIndex && index >= 0 && index < pages) { + currentIndex = index; + setLatestOpenView(); + notifyListeners(); + } + } + + void move({ + required PageManager from, + required PageManager to, + required TabDraggableHoverPosition position, + }) async { + final selectExistingPlugin = _selectPluginIfOpen(from.plugin.id); + + if (!selectExistingPlugin) { + final readOnly = await TabsService.setPluginOpenedInCache(from.plugin); + final newPm = PageManager() + ..setPlugin(from.plugin) + ..setReadOnlyStatus(readOnly); + switch (position) { + case TabDraggableHoverPosition.none: + return; + case TabDraggableHoverPosition.left: + { + final index = pageManagers.indexOf(to); + pageManagers.insert(index, newPm); + currentIndex = index; + break; + } + case TabDraggableHoverPosition.right: + { + final index = pageManagers.indexOf(to); + if (index + 1 == pageManagers.length) { + pageManagers.add(newPm); + } else { + pageManagers.insert(index + 1, newPm); + } + currentIndex = index + 1; + break; + } + } + } + setLatestOpenView(); + notifyListeners(); + } + + void setLatestOpenView([ViewPB? view]) { + if (view != null) { + menuSharedState.latestOpenView = view; + } else { + final notifier = currentPageManager.plugin.notifier; + if (notifier is ViewPluginNotifier) { + menuSharedState.latestOpenView = notifier.view; + } + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_service.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_service.dart new file mode 100644 index 000000000000..ae99d0d5006f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_service.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/log.dart'; + +class TabsService { + const TabsService(); + + static Future setPluginClosedInCache(String pluginId) async { + final result = await getIt().get(KVKeys.openedPlugins); + final map = result.fold( + (l) => {}, + (r) => jsonDecode(r), + ); + if (map[pluginId] != null) { + map[pluginId] -= 1; + + if (map[pluginId] <= 0) { + map.remove(pluginId); + } + } + await getIt().set(KVKeys.openedPlugins, jsonEncode(map)); + } + + static Future setPluginOpenedInCache(Plugin plugin) async { + final result = await getIt().get(KVKeys.openedPlugins); + final map = result.fold( + (l) => {}, + (r) => jsonDecode(r), + ); + // Log.warn("Result Map $map ${map[plugin.id]} ${plugin.id}"); + if (map[plugin.id] != null) { + map[plugin.id] += 1; + return true; + } + + map[plugin.id] = 1; + Log.warn(map); + await getIt().set(KVKeys.openedPlugins, jsonEncode(map)); + return false; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 2681820a0ebc..a437f04316be 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -1,4 +1,5 @@ import 'package:appflowy/core/frameless_window.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; @@ -11,6 +12,7 @@ import 'package:appflowy/workspace/presentation/home/panes/flowy_pane_group.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:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/extension.dart'; import 'package:flutter/material.dart'; @@ -41,6 +43,7 @@ class HomeStack extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + _printTree(state.root); return BlocBuilder( builder: (context, homeState) { return FlowyPaneGroup( @@ -55,6 +58,13 @@ class HomeStack extends StatelessWidget { }, ); } + + void _printTree(PaneNode node, [String prefix = '']) { + print('$prefix${node.tabs.hashCode}'); + for (var child in node.children) { + _printTree(child, '$prefix └─ '); + } + } } class PageStack extends StatefulWidget { @@ -77,7 +87,23 @@ class _PageStackState extends State @override Widget build(BuildContext context) { super.build(context); + if (widget.pageManager.readOnly) { + return Stack( + children: [ + AbsorbPointer( + child: Opacity( + opacity: 0.5, + child: _buildWidgetStack(context), + ), + ), + Positioned(child: _buildReadOnlyBanner()) + ], + ); + } + return _buildWidgetStack(context); + } + Widget _buildWidgetStack(BuildContext context) { return Container( color: Theme.of(context).colorScheme.surface, child: FocusTraversalGroup( @@ -90,6 +116,29 @@ class _PageStackState extends State ); } + Widget _buildReadOnlyBanner() { + final colorScheme = Theme.of(context).colorScheme; + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 20), + child: Container( + width: double.infinity, + color: colorScheme.primary, + child: FittedBox( + alignment: Alignment.center, + fit: BoxFit.scaleDown, + child: Row( + children: [ + FlowyText.medium( + LocaleKeys.readOnlyViewText.tr(), + fontSize: 14, + ), + ], + ), + ), + ), + ); + } + @override bool get wantKeepAlive => true; } @@ -152,14 +201,16 @@ abstract mixin class NavigationItem { class PageNotifier extends ChangeNotifier { Plugin _plugin; + bool _readOnly; Widget get titleWidget => _plugin.widgetBuilder.leftBarItem; Widget tabBarWidget(String pluginId) => _plugin.widgetBuilder.tabBarItem(pluginId); - PageNotifier({Plugin? plugin}) - : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank); + PageNotifier({Plugin? plugin, bool? readOnly}) + : _plugin = plugin ?? makePlugin(pluginType: PluginType.blank), + _readOnly = readOnly ?? false; /// This is the only place where the plugin is set. /// No need compare the old plugin with the new plugin. Just set it. @@ -173,7 +224,14 @@ class PageNotifier extends ChangeNotifier { notifyListeners(); } + set readOnlyStatus(bool status) { + _readOnly = status; + notifyListeners(); + } + Plugin get plugin => _plugin; + + bool get readOnly => _readOnly; } // PageManager manages the view for one Tab @@ -190,10 +248,16 @@ class PageManager { Plugin get plugin => _notifier.plugin; + bool get readOnly => _notifier.readOnly; + void setPlugin(Plugin newPlugin) { _notifier.plugin = newPlugin; } + void setReadOnlyStatus(bool status) { + _notifier.readOnlyStatus = status; + } + void setStackWithId(String id) { // Navigate to the page with id } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart index f65e917e363a..fce3ff69c9cb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart @@ -1,6 +1,6 @@ import 'package:appflowy/workspace/application/panes/panes.dart'; import 'package:appflowy/workspace/application/panes/panes_cubit/panes_cubit.dart'; -import 'package:appflowy/workspace/application/tabs/tabs.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_controller.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/tabs/draggable_tab_item.dart'; import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart'; @@ -11,7 +11,7 @@ import 'package:provider/provider.dart'; class TabsManager extends StatefulWidget { final PageController pageController; - final Tabs tabs; + final TabsController tabs; final PaneNode pane; const TabsManager({ super.key,