added list view; persist tab view ui settings;
This commit is contained in:
@@ -57,7 +57,7 @@ final class TabSessionProvider extends $NotifierProvider<TabSession, void> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabSessionHash() => r'f0a3aca70719afd37cad260c5d36621455252bfb';
|
||||
String _$tabSessionHash() => r'4b7206863e205dfeee6201ba6072e742c43ab6f3';
|
||||
|
||||
final class TabSessionFamily extends $Family
|
||||
with $ClassFamilyOverride<TabSession, void, void, void, String?> {
|
||||
|
||||
@@ -255,7 +255,7 @@ Future<bool> isTabTunneled(Ref ref, String? tabId) async {
|
||||
case TorRegularTabProxyMode.container:
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabState.id);
|
||||
.getTabContainerId(tabState.id);
|
||||
|
||||
final containerData = await containerId.mapNotNull(
|
||||
(containerId) => ref
|
||||
|
||||
@@ -188,7 +188,7 @@ final class IsTabTunneledProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$isTabTunneledHash() => r'fd8ca366b0835eef8aea7ad64453e6a6f2e69e82';
|
||||
String _$isTabTunneledHash() => r'2e1a911c26345951cf259ca4547d236a71607667';
|
||||
|
||||
final class IsTabTunneledFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
||||
|
||||
@@ -188,7 +188,7 @@ class TabRepository extends _$TabRepository {
|
||||
Future<bool> selectTab(String tabId) async {
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabId);
|
||||
.getTabContainerId(tabId);
|
||||
|
||||
if (!ref.mounted) return false;
|
||||
|
||||
@@ -234,7 +234,7 @@ class TabRepository extends _$TabRepository {
|
||||
|
||||
final currentContainerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabId);
|
||||
.getTabContainerId(tabId);
|
||||
|
||||
if (!ref.mounted) return;
|
||||
|
||||
@@ -419,7 +419,7 @@ class TabRepository extends _$TabRepository {
|
||||
} else {
|
||||
final tabContainerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabState.id);
|
||||
.getTabContainerId(tabState.id);
|
||||
|
||||
if (targetContainerId != tabContainerId) {
|
||||
if (originUri == null) {
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'ef1116cbf09f456deae7a7d3f5cee3ba843d7430';
|
||||
String _$tabRepositoryHash() => r'7af24e759044d1b1b471cdf90a3eb38eef06f61b';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -83,7 +83,7 @@ class SelectedBangData extends _$SelectedBangData {
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
EquatableValue<List<String>> availableTabIds(
|
||||
EquatableValue<List<DefaultTabEntity>> availableTabIds(
|
||||
Ref ref,
|
||||
ContainerFilter containerFilter,
|
||||
) {
|
||||
@@ -94,10 +94,37 @@ EquatableValue<List<String>> availableTabIds(
|
||||
);
|
||||
final tabList = ref.watch(tabListProvider);
|
||||
|
||||
return EquatableValue(
|
||||
containerTabs?.where((tabId) => tabList.value.contains(tabId)).toList() ??
|
||||
[],
|
||||
);
|
||||
final availableTabs =
|
||||
containerTabs?.where((tabId) => tabList.value.contains(tabId)).toList() ??
|
||||
[];
|
||||
|
||||
switch (containerFilter) {
|
||||
case ContainerFilterById():
|
||||
return EquatableValue(
|
||||
availableTabs
|
||||
.map(
|
||||
(t) => DefaultTabEntity(
|
||||
tabId: t,
|
||||
containerId: containerFilter.containerId,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
case ContainerFilterDisabled():
|
||||
return ref.watch(
|
||||
watchTabsContainerIdProvider(EquatableValue(availableTabs)).select(
|
||||
(value) => EquatableValue(
|
||||
value.value?.entries
|
||||
.map(
|
||||
(e) =>
|
||||
DefaultTabEntity(tabId: e.key, containerId: e.value),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
@@ -109,8 +136,9 @@ EquatableValue<Map<String, TabState>> availableTabStates(
|
||||
final tabStates = ref.watch(tabStatesProvider);
|
||||
|
||||
return EquatableValue({
|
||||
for (final tabId in availableTabs.value)
|
||||
if (tabStates.containsKey(tabId)) tabId: tabStates[tabId]!,
|
||||
for (final tabEntity in availableTabs.value)
|
||||
if (tabStates.containsKey(tabEntity.tabId))
|
||||
tabEntity.tabId: tabStates[tabEntity.tabId]!,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,7 +199,12 @@ EquatableValue<List<TabEntity>> suggestedTabEntities(
|
||||
.whereNot(
|
||||
(tabId) => excludedTabIds.value?.contains(tabId) ?? false,
|
||||
)
|
||||
.map((tabId) => DefaultTabEntity(tabId: tabId))
|
||||
.map(
|
||||
(tabId) => DefaultTabEntity(
|
||||
tabId: tabId,
|
||||
containerId: containerId,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
) ??
|
||||
const [],
|
||||
@@ -198,6 +231,7 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
||||
.map(
|
||||
(tab) => SearchResultTabEntity(
|
||||
tabId: tab.id,
|
||||
containerId: tab.containerId,
|
||||
searchQuery: result.query,
|
||||
),
|
||||
)
|
||||
@@ -208,13 +242,7 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
||||
)
|
||||
.value;
|
||||
|
||||
final availableTabs = ref.watch(
|
||||
availableTabIdsProvider(containerFilter).select(
|
||||
(value) => EquatableValue(
|
||||
value.value.map((tab) => DefaultTabEntity(tabId: tab)).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
final availableTabs = ref.watch(availableTabIdsProvider(containerFilter));
|
||||
|
||||
if (tabSearchResults == null) {
|
||||
if (groupTrees) {
|
||||
|
||||
@@ -212,11 +212,11 @@ const availableTabIdsProvider = AvailableTabIdsFamily._();
|
||||
final class AvailableTabIdsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
EquatableValue<List<String>>,
|
||||
EquatableValue<List<String>>,
|
||||
EquatableValue<List<String>>
|
||||
EquatableValue<List<DefaultTabEntity>>,
|
||||
EquatableValue<List<DefaultTabEntity>>,
|
||||
EquatableValue<List<DefaultTabEntity>>
|
||||
>
|
||||
with $Provider<EquatableValue<List<String>>> {
|
||||
with $Provider<EquatableValue<List<DefaultTabEntity>>> {
|
||||
const AvailableTabIdsProvider._({
|
||||
required AvailableTabIdsFamily super.from,
|
||||
required ContainerFilter super.argument,
|
||||
@@ -240,21 +240,22 @@ final class AvailableTabIdsProvider
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<EquatableValue<List<String>>> $createElement(
|
||||
$ProviderElement<EquatableValue<List<DefaultTabEntity>>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
EquatableValue<List<String>> create(Ref ref) {
|
||||
EquatableValue<List<DefaultTabEntity>> create(Ref ref) {
|
||||
final argument = this.argument as ContainerFilter;
|
||||
return availableTabIds(ref, argument);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(EquatableValue<List<String>> value) {
|
||||
Override overrideWithValue(EquatableValue<List<DefaultTabEntity>> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<EquatableValue<List<String>>>(value),
|
||||
providerOverride:
|
||||
$SyncValueProvider<EquatableValue<List<DefaultTabEntity>>>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -269,12 +270,12 @@ final class AvailableTabIdsProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$availableTabIdsHash() => r'aa166b471d45913df5166a3126abeb7c0d791412';
|
||||
String _$availableTabIdsHash() => r'fa7280a7a5f9c277faa71a30f5dd50afa30a20b1';
|
||||
|
||||
final class AvailableTabIdsFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
EquatableValue<List<String>>,
|
||||
EquatableValue<List<DefaultTabEntity>>,
|
||||
ContainerFilter
|
||||
> {
|
||||
const AvailableTabIdsFamily._()
|
||||
@@ -358,7 +359,7 @@ final class AvailableTabStatesProvider
|
||||
}
|
||||
|
||||
String _$availableTabStatesHash() =>
|
||||
r'f506b050e1dc4f50fbd2699d97af5a16a037bc4b';
|
||||
r'32f15b0f739495e11025bdc10d324764465b542d';
|
||||
|
||||
final class AvailableTabStatesFamily extends $Family
|
||||
with
|
||||
@@ -497,7 +498,7 @@ final class SuggestedTabEntitiesProvider
|
||||
}
|
||||
|
||||
String _$suggestedTabEntitiesHash() =>
|
||||
r'6fb58bf4b7377f826377c88110c8e4aca9f6d6f9';
|
||||
r'1c3d9a4f85db60301fe5c5b55fb55512d1b59696';
|
||||
|
||||
final class SuggestedTabEntitiesFamily extends $Family
|
||||
with $FunctionalFamilyOverride<EquatableValue<List<TabEntity>>, String?> {
|
||||
@@ -601,7 +602,7 @@ final class SeamlessFilteredTabEntitiesProvider
|
||||
}
|
||||
|
||||
String _$seamlessFilteredTabEntitiesHash() =>
|
||||
r'eb6573514f55b598fbc1a91370079b4b8372c55e';
|
||||
r'f2bf14737bc03f71fc61252359979b4e901e5df7';
|
||||
|
||||
final class SeamlessFilteredTabEntitiesFamily extends $Family
|
||||
with
|
||||
|
||||
+25
-2
@@ -17,9 +17,14 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:riverpod/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/user/data/providers.dart';
|
||||
|
||||
part 'tab_view_controllers.g.dart';
|
||||
|
||||
@@ -37,11 +42,19 @@ class TabSuggestionsController extends _$TabSuggestionsController {
|
||||
|
||||
@override
|
||||
bool build() {
|
||||
return false;
|
||||
persist(
|
||||
ref.watch(riverpodDatabaseStorageProvider),
|
||||
key: 'TabSuggestions',
|
||||
encode: (state) => jsonEncode([state]),
|
||||
decode: (encoded) => (jsonDecode(encoded) as List<dynamic>).first as bool,
|
||||
);
|
||||
|
||||
return stateOrNull ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
enum TabsViewMode {
|
||||
list(MdiIcons.folderTable, 'List'),
|
||||
grid(MdiIcons.table, 'Grid'),
|
||||
tree(MdiIcons.familyTree, 'Tree');
|
||||
|
||||
@@ -61,7 +74,17 @@ class TabsViewModeController extends _$TabsViewModeController {
|
||||
|
||||
@override
|
||||
TabsViewMode build() {
|
||||
return TabsViewMode.grid;
|
||||
persist(
|
||||
ref.watch(riverpodDatabaseStorageProvider),
|
||||
key: 'TabsViewMode',
|
||||
encode: (state) => jsonEncode([state.name]),
|
||||
decode: (encoded) {
|
||||
final name = (jsonDecode(encoded) as List<dynamic>).first as String;
|
||||
return TabsViewMode.values.firstWhere((e) => e.name == name);
|
||||
},
|
||||
);
|
||||
|
||||
return stateOrNull ?? TabsViewMode.list;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ final class TabSuggestionsControllerProvider
|
||||
}
|
||||
|
||||
String _$tabSuggestionsControllerHash() =>
|
||||
r'5ce7f385b8aba14d432912cf0acb06aad04433fc';
|
||||
r'c60912e474177f2a0f15167743db439d157cd852';
|
||||
|
||||
abstract class _$TabSuggestionsController extends $Notifier<bool> {
|
||||
bool build();
|
||||
@@ -96,7 +96,7 @@ final class TabsViewModeControllerProvider
|
||||
}
|
||||
|
||||
String _$tabsViewModeControllerHash() =>
|
||||
r'9c574458f6d5a4c6a8b5aabf96ad7f5dcd043dd0';
|
||||
r'e013b174218fcad81981f7d939cf161c3b002adc';
|
||||
|
||||
abstract class _$TabsViewModeController extends $Notifier<TabsViewMode> {
|
||||
TabsViewMode build();
|
||||
|
||||
@@ -30,7 +30,7 @@ import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart';
|
||||
@@ -123,7 +123,7 @@ class TabTreeDialog extends HookConsumerWidget {
|
||||
final id = node.key!.value as String;
|
||||
return SizedBox.fromSize(
|
||||
size: childSize,
|
||||
child: SingleTabPreview(
|
||||
child: SingleGridTabPreview(
|
||||
key: ValueKey(id),
|
||||
tabId: id,
|
||||
activeTabId: selectedTabId,
|
||||
|
||||
@@ -46,7 +46,9 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||
@@ -780,7 +782,18 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
||||
topRight: Radius.circular(28),
|
||||
),
|
||||
child: switch (tabsViewMode) {
|
||||
TabsViewMode.grid => ViewTabsWidget(
|
||||
TabsViewMode.list => ViewTabListWidget(
|
||||
scrollController: scrollController,
|
||||
showNewTabFab: true,
|
||||
tabsReorderable: tabsReorderable,
|
||||
draggableScrollableController: draggableScrollableController,
|
||||
onClose: () {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.requestDismiss();
|
||||
},
|
||||
),
|
||||
TabsViewMode.grid => ViewTabGridWidget(
|
||||
scrollController: scrollController,
|
||||
showNewTabFab: true,
|
||||
tabsReorderable: tabsReorderable,
|
||||
|
||||
@@ -23,7 +23,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
class TabViewScreen extends HookConsumerWidget {
|
||||
@@ -40,7 +42,16 @@ class TabViewScreen extends HookConsumerWidget {
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: switch (tabsViewMode) {
|
||||
TabsViewMode.grid => ViewTabsWidget(
|
||||
TabsViewMode.list => ViewTabListWidget(
|
||||
key: ValueKey(tabsReorderable),
|
||||
scrollController: scrollController,
|
||||
tabsReorderable: tabsReorderable,
|
||||
showNewTabFab: false,
|
||||
onClose: () {
|
||||
const BrowserRoute().go(context);
|
||||
},
|
||||
),
|
||||
TabsViewMode.grid => ViewTabGridWidget(
|
||||
key: ValueKey(tabsReorderable),
|
||||
scrollController: scrollController,
|
||||
tabsReorderable: tabsReorderable,
|
||||
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_reorderable_grid_view/widgets/custom_draggable.dart';
|
||||
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/providers/global_drop.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/data/models/drag_data.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
class _TabDraggable extends HookConsumerWidget {
|
||||
final TabEntity entity;
|
||||
final String? suggestedContainerId;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabDraggable({
|
||||
required this.entity,
|
||||
required this.onClose,
|
||||
this.suggestedContainerId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final dragData = ref.watch(
|
||||
willAcceptDropProvider.select((value) {
|
||||
final dragTabId = switch (value) {
|
||||
ContainerDropData() => value.tabId,
|
||||
DeleteDropData() => value.tabId,
|
||||
null => null,
|
||||
};
|
||||
|
||||
return (dragTabId == entity.tabId) ? value : null;
|
||||
}),
|
||||
);
|
||||
|
||||
// Cache the tab widget to avoid rebuilding
|
||||
final tab = useMemoized(() {
|
||||
return (suggestedContainerId != null)
|
||||
? SuggestedSingleGridTabPreview(
|
||||
key: ValueKey(entity.tabId),
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onTap: () async {
|
||||
final containerData = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(suggestedContainerId!);
|
||||
|
||||
if (containerData != null) {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignContainer(entity.tabId, containerData);
|
||||
}
|
||||
},
|
||||
)
|
||||
: SingleGridTabPreview(
|
||||
key: ValueKey(entity.tabId),
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
sourceSearchQuery: switch (entity) {
|
||||
DefaultTabEntity _ => null,
|
||||
final SearchResultTabEntity entity => entity.searchQuery,
|
||||
TabTreeEntity _ => throw UnimplementedError(),
|
||||
},
|
||||
);
|
||||
}, [entity.tabId, activeTab, suggestedContainerId]);
|
||||
|
||||
return switch (dragData) {
|
||||
ContainerDropData() => Opacity(
|
||||
opacity: 0.3,
|
||||
child: Transform.scale(scale: 0.9, child: tab),
|
||||
),
|
||||
DeleteDropData() => Opacity(
|
||||
opacity: 0.3,
|
||||
child: ColorFiltered(
|
||||
colorFilter: const ColorFilter.mode(Colors.red, BlendMode.modulate),
|
||||
child: tab,
|
||||
),
|
||||
),
|
||||
null => tab,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _TabGridView extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final bool tabsReorderable;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabGridView({
|
||||
required this.scrollController,
|
||||
required this.tabsReorderable,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final containerId = ref.watch(selectedContainerProvider);
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
searchPartition: TabSearchPartition.preview,
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterById(containerId: containerId),
|
||||
groupTrees: false,
|
||||
),
|
||||
);
|
||||
|
||||
final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider);
|
||||
|
||||
final suggestedTabEntities = tabSuggestionsEnabled
|
||||
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
||||
: EquatableValue(<TabEntity>[]);
|
||||
|
||||
final itemCount =
|
||||
filteredTabEntities.value.length +
|
||||
//Limit to 3 sugegstions for now
|
||||
math.min<int>(suggestedTabEntities.value.length, 3);
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(math.min(calculatedCount, itemCount), 2);
|
||||
}, [screenWidth, itemCount]);
|
||||
|
||||
final itemSize = useMemoized(
|
||||
() => calculateItemSize(
|
||||
screenWidth: screenWidth,
|
||||
childAspectRatio: 0.75,
|
||||
horizontalPadding: 4.0,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
[screenWidth, crossAxisCount],
|
||||
);
|
||||
|
||||
final lastScroll = useRef<String?>(null);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (scrollController.hasClients) {
|
||||
if (lastScroll.value != activeTab) {
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
final offset = (index ~/ 2) * itemSize.height;
|
||||
|
||||
if (offset != scrollController.offset) {
|
||||
lastScroll.value = activeTab;
|
||||
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FadingScroll(
|
||||
fadingSize: 5,
|
||||
controller: scrollController,
|
||||
builder: (context, controller) {
|
||||
return !tabsReorderable
|
||||
? _TabGrid(
|
||||
key: ValueKey(crossAxisCount),
|
||||
crossAxisCount: crossAxisCount,
|
||||
itemCount: itemCount,
|
||||
scrollController: controller,
|
||||
itemBuilder: (widget, _) {
|
||||
if (widget is CustomDraggable) {
|
||||
return LongPressDraggable(
|
||||
feedback: Material(
|
||||
color: Colors
|
||||
.transparent, // removes white corners when having shadow
|
||||
child: Transform.scale(
|
||||
scale: 1.05,
|
||||
child: SizedBox(
|
||||
height: itemSize.height,
|
||||
width: itemSize.width,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
data: widget.data,
|
||||
childWhenDragging: SizedBox(
|
||||
height: itemSize.height,
|
||||
width: itemSize.width,
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
return widget;
|
||||
},
|
||||
suggestedContainerId: containerId,
|
||||
filteredTabEntities: filteredTabEntities,
|
||||
suggestedTabEntities: suggestedTabEntities,
|
||||
onClose: onClose,
|
||||
)
|
||||
: ReorderableBuilder.builder(
|
||||
//Rebuild when cross axis count changes
|
||||
key: ValueKey(crossAxisCount),
|
||||
scrollController: controller,
|
||||
itemCount: itemCount,
|
||||
onDragStarted: (index) {
|
||||
ref.read(willAcceptDropProvider.notifier).clear();
|
||||
},
|
||||
onReorderPositions: (positions) async {
|
||||
assert(
|
||||
positions.length == 1,
|
||||
'Not ready for multiple reorders',
|
||||
);
|
||||
|
||||
final oldIndex = positions.first.oldIndex;
|
||||
final newIndex = positions.first.newIndex;
|
||||
|
||||
final containerRepository = ref.read(
|
||||
containerRepositoryProvider.notifier,
|
||||
);
|
||||
|
||||
//Suggestions are at the end and not reorderable, so skip
|
||||
if (oldIndex >= filteredTabEntities.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tabId = filteredTabEntities.value[oldIndex].tabId;
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabContainerId(tabId);
|
||||
|
||||
final String key;
|
||||
if (newIndex <= 0) {
|
||||
key = await containerRepository.getLeadingOrderKey(
|
||||
containerId,
|
||||
);
|
||||
} else if (newIndex >=
|
||||
filteredTabEntities.value.length - 1) {
|
||||
key = await containerRepository.getTrailingOrderKey(
|
||||
containerId,
|
||||
);
|
||||
} else {
|
||||
if (newIndex < oldIndex) {
|
||||
key = (await containerRepository.getOrderKeyAfterTab(
|
||||
filteredTabEntities.value[newIndex - 1].tabId,
|
||||
containerId,
|
||||
))!;
|
||||
} else {
|
||||
key = await containerRepository.getOrderKeyBeforeTab(
|
||||
filteredTabEntities.value[newIndex + 1].tabId,
|
||||
containerId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignOrderKey(tabId, key);
|
||||
},
|
||||
childBuilder: (itemBuilder) {
|
||||
return _TabGrid(
|
||||
crossAxisCount: crossAxisCount,
|
||||
itemCount: itemCount,
|
||||
scrollController: controller,
|
||||
itemBuilder: itemBuilder,
|
||||
suggestedContainerId: containerId,
|
||||
filteredTabEntities: filteredTabEntities,
|
||||
suggestedTabEntities: suggestedTabEntities,
|
||||
onClose: onClose,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabGrid extends StatelessWidget {
|
||||
const _TabGrid({
|
||||
super.key,
|
||||
required this.crossAxisCount,
|
||||
required this.scrollController,
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
required this.suggestedContainerId,
|
||||
required this.filteredTabEntities,
|
||||
required this.suggestedTabEntities,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
final int crossAxisCount;
|
||||
final int itemCount;
|
||||
final ScrollController? scrollController;
|
||||
final String? suggestedContainerId;
|
||||
final EquatableValue<List<TabEntity>> filteredTabEntities;
|
||||
final EquatableValue<List<TabEntity>> suggestedTabEntities;
|
||||
final Widget Function(Widget, int)? itemBuilder;
|
||||
final VoidCallback onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
final Widget tab;
|
||||
if (index < filteredTabEntities.value.length) {
|
||||
final entity = filteredTabEntities.value[index];
|
||||
tab = CustomDraggable(
|
||||
key: Key(entity.tabId),
|
||||
data: TabDragData(entity.tabId),
|
||||
child: _TabDraggable(entity: entity, onClose: onClose),
|
||||
);
|
||||
} else {
|
||||
final suggestedIndex = index - filteredTabEntities.value.length;
|
||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||
|
||||
tab = CustomDraggable(
|
||||
key: Key('suggested_${entity.tabId}'),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
suggestedContainerId: suggestedContainerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return (itemBuilder != null) ? itemBuilder!(tab, index) : tab;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewTabGridWidget extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final DraggableScrollableController? draggableScrollableController;
|
||||
final bool showNewTabFab;
|
||||
final bool tabsReorderable;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const ViewTabGridWidget({
|
||||
required this.onClose,
|
||||
required this.scrollController,
|
||||
this.draggableScrollableController,
|
||||
required this.tabsReorderable,
|
||||
required this.showNewTabFab,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
NestedScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
draggableScrollableController.mapNotNull(
|
||||
(draggableScrollableController) =>
|
||||
DraggableScrollableHeader(
|
||||
controller: draggableScrollableController,
|
||||
child: TabViewHeader(
|
||||
onClose: onClose,
|
||||
tabsViewMode: TabsViewMode.grid,
|
||||
),
|
||||
),
|
||||
) ??
|
||||
TabViewHeader(
|
||||
onClose: onClose,
|
||||
tabsViewMode: TabsViewMode.grid,
|
||||
),
|
||||
),
|
||||
],
|
||||
body: _TabGridView(
|
||||
scrollController: scrollController,
|
||||
tabsReorderable: tabsReorderable,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
if (showNewTabFab)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: TabViewHeader.headerSize + 4,
|
||||
right: 4,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.defaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
onClose();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_reorderable_grid_view/widgets/custom_draggable.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/providers/global_drop.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/data/models/drag_data.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
class _TabDraggable extends HookConsumerWidget {
|
||||
final TabEntity entity;
|
||||
final String? suggestedContainerId;
|
||||
final VoidCallback onClose;
|
||||
final double height;
|
||||
|
||||
const _TabDraggable({
|
||||
required this.entity,
|
||||
required this.onClose,
|
||||
required this.height,
|
||||
this.suggestedContainerId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final dragData = ref.watch(
|
||||
willAcceptDropProvider.select((value) {
|
||||
final dragTabId = switch (value) {
|
||||
ContainerDropData() => value.tabId,
|
||||
DeleteDropData() => value.tabId,
|
||||
null => null,
|
||||
};
|
||||
|
||||
return (dragTabId == entity.tabId) ? value : null;
|
||||
}),
|
||||
);
|
||||
|
||||
// Cache the tab widget to avoid rebuilding
|
||||
final tab = useMemoized(() {
|
||||
return (suggestedContainerId != null)
|
||||
? SuggestedSingleListTabPreview(
|
||||
key: ValueKey(entity.tabId),
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onTap: () async {
|
||||
final containerData = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(suggestedContainerId!);
|
||||
|
||||
if (containerData != null) {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignContainer(entity.tabId, containerData);
|
||||
}
|
||||
},
|
||||
)
|
||||
: SingleListTabPreview(
|
||||
key: ValueKey(entity.tabId),
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
sourceSearchQuery: switch (entity) {
|
||||
DefaultTabEntity _ => null,
|
||||
final SearchResultTabEntity entity => entity.searchQuery,
|
||||
TabTreeEntity _ => throw UnimplementedError(),
|
||||
},
|
||||
);
|
||||
}, [entity.tabId, activeTab, suggestedContainerId]);
|
||||
|
||||
return switch (dragData) {
|
||||
ContainerDropData() => Opacity(
|
||||
opacity: 0.3,
|
||||
child: Transform.scale(
|
||||
scale: 0.9,
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: tab,
|
||||
),
|
||||
),
|
||||
),
|
||||
DeleteDropData() => Opacity(
|
||||
opacity: 0.3,
|
||||
child: ColorFiltered(
|
||||
colorFilter: const ColorFilter.mode(Colors.red, BlendMode.modulate),
|
||||
child: tab,
|
||||
),
|
||||
),
|
||||
null => tab,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _TabListView extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final bool tabsReorderable;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabListView({
|
||||
required this.scrollController,
|
||||
required this.tabsReorderable,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
static const _itemHeight = 80.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final containerId = ref.watch(selectedContainerProvider);
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
searchPartition: TabSearchPartition.preview,
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterById(containerId: containerId),
|
||||
groupTrees: false,
|
||||
),
|
||||
);
|
||||
|
||||
final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider);
|
||||
|
||||
final suggestedTabEntities = tabSuggestionsEnabled
|
||||
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
||||
: EquatableValue(<TabEntity>[]);
|
||||
|
||||
final itemCount =
|
||||
filteredTabEntities.value.length +
|
||||
//Limit to 3 sugegstions for now
|
||||
math.min<int>(suggestedTabEntities.value.length, 3);
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final lastScroll = useRef<String?>(null);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (scrollController.hasClients) {
|
||||
if (lastScroll.value != activeTab) {
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
final offset = index * _itemHeight;
|
||||
|
||||
if (offset != scrollController.offset) {
|
||||
lastScroll.value = activeTab;
|
||||
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FadingScroll(
|
||||
fadingSize: 5,
|
||||
controller: scrollController,
|
||||
builder: (context, controller) {
|
||||
return !tabsReorderable
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: itemCount,
|
||||
itemExtent: _itemHeight,
|
||||
itemBuilder: (context, index) {
|
||||
final CustomDraggable tab;
|
||||
|
||||
if (index < filteredTabEntities.value.length) {
|
||||
final entity = filteredTabEntities.value[index];
|
||||
tab = CustomDraggable(
|
||||
key: Key(entity.tabId),
|
||||
data: TabDragData(entity.tabId),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
height: _itemHeight,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final suggestedIndex =
|
||||
index - filteredTabEntities.value.length;
|
||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||
|
||||
tab = CustomDraggable(
|
||||
key: Key('suggested_${entity.tabId}'),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
suggestedContainerId: containerId,
|
||||
height: _itemHeight,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return LongPressDraggable(
|
||||
feedback: Material(
|
||||
color: Colors
|
||||
.transparent, // removes white corners when having shadow
|
||||
child: Transform.scale(
|
||||
scale: 1.05,
|
||||
child: SizedBox(
|
||||
height: _itemHeight,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: tab.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
data: tab.data,
|
||||
childWhenDragging: const SizedBox(height: _itemHeight),
|
||||
child: tab.child,
|
||||
);
|
||||
},
|
||||
)
|
||||
: ReorderableListView.builder(
|
||||
scrollController: controller,
|
||||
itemCount: itemCount,
|
||||
onReorderStart: (index) {
|
||||
ref.read(willAcceptDropProvider.notifier).clear();
|
||||
},
|
||||
onReorder: (oldIndex, newIndex) async {
|
||||
final containerRepository = ref.read(
|
||||
containerRepositoryProvider.notifier,
|
||||
);
|
||||
|
||||
//Suggestions are at the end and not reorderable, so skip
|
||||
if (oldIndex >= filteredTabEntities.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tabId = filteredTabEntities.value[oldIndex].tabId;
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabContainerId(tabId);
|
||||
|
||||
final String key;
|
||||
if (newIndex <= 0) {
|
||||
key = await containerRepository.getLeadingOrderKey(
|
||||
containerId,
|
||||
);
|
||||
} else if (newIndex >=
|
||||
filteredTabEntities.value.length - 1) {
|
||||
key = await containerRepository.getTrailingOrderKey(
|
||||
containerId,
|
||||
);
|
||||
} else {
|
||||
if (newIndex < oldIndex) {
|
||||
key = (await containerRepository.getOrderKeyAfterTab(
|
||||
filteredTabEntities.value[newIndex - 1].tabId,
|
||||
containerId,
|
||||
))!;
|
||||
} else {
|
||||
key = await containerRepository.getOrderKeyBeforeTab(
|
||||
filteredTabEntities.value[newIndex + 1].tabId,
|
||||
containerId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignOrderKey(tabId, key);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final CustomDraggable tab;
|
||||
|
||||
if (index < filteredTabEntities.value.length) {
|
||||
final entity = filteredTabEntities.value[index];
|
||||
tab = CustomDraggable(
|
||||
key: Key(entity.tabId),
|
||||
data: TabDragData(entity.tabId),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
height: _itemHeight,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final suggestedIndex =
|
||||
index - filteredTabEntities.value.length;
|
||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||
|
||||
tab = CustomDraggable(
|
||||
key: Key('suggested_${entity.tabId}'),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
suggestedContainerId: containerId,
|
||||
height: _itemHeight,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// return LongPressDraggable(
|
||||
// key: tab.key,
|
||||
// feedback: Material(
|
||||
// color: Colors
|
||||
// .transparent, // removes white corners when having shadow
|
||||
// child: Transform.scale(
|
||||
// scale: 1.05,
|
||||
// child: SizedBox(
|
||||
// height: _itemHeight,
|
||||
// width: MediaQuery.of(context).size.width,
|
||||
// child: tab.child,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// data: tab.data,
|
||||
// child: tab.child,
|
||||
// );
|
||||
return tab;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewTabListWidget extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final DraggableScrollableController? draggableScrollableController;
|
||||
final bool showNewTabFab;
|
||||
final bool tabsReorderable;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const ViewTabListWidget({
|
||||
required this.onClose,
|
||||
required this.scrollController,
|
||||
this.draggableScrollableController,
|
||||
required this.tabsReorderable,
|
||||
required this.showNewTabFab,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
NestedScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
draggableScrollableController.mapNotNull(
|
||||
(draggableScrollableController) =>
|
||||
DraggableScrollableHeader(
|
||||
controller: draggableScrollableController,
|
||||
child: TabViewHeader(
|
||||
onClose: onClose,
|
||||
tabsViewMode: TabsViewMode.list,
|
||||
),
|
||||
),
|
||||
) ??
|
||||
TabViewHeader(
|
||||
onClose: onClose,
|
||||
tabsViewMode: TabsViewMode.list,
|
||||
),
|
||||
),
|
||||
],
|
||||
body: _TabListView(
|
||||
scrollController: scrollController,
|
||||
tabsReorderable: tabsReorderable,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
if (showNewTabFab)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: TabViewHeader.headerSize + 4,
|
||||
right: 4,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.defaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
onClose();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+296
-166
@@ -24,24 +24,22 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
class TabContainer extends StatelessWidget {
|
||||
class GridTabItemContainer extends StatelessWidget {
|
||||
final bool isActive;
|
||||
final bool isPrivate;
|
||||
final Widget? child;
|
||||
|
||||
const TabContainer({
|
||||
const GridTabItemContainer({
|
||||
required this.isActive,
|
||||
required this.isPrivate,
|
||||
this.child,
|
||||
@@ -71,38 +69,7 @@ class TabContainer extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class TabMiniPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
|
||||
const TabMiniPreview({super.key, required this.tabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final thumbnail = ref.watch(
|
||||
tabStateProvider(tabId).select((value) => value?.thumbnail),
|
||||
);
|
||||
|
||||
return TabContainer(
|
||||
isActive: false,
|
||||
isPrivate: false,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
||||
child: Skeleton.replace(
|
||||
replace: thumbnail == null,
|
||||
replacement: const Bone.square(size: double.infinity),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: RepaintBoundary(
|
||||
child: RawImage(image: thumbnail?.value, fit: BoxFit.fitWidth),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabPreview extends HookConsumerWidget {
|
||||
class GridTabPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
final bool isActive;
|
||||
|
||||
@@ -114,7 +81,7 @@ class TabPreview extends HookConsumerWidget {
|
||||
|
||||
final Widget? trailingChild;
|
||||
|
||||
const TabPreview({
|
||||
const GridTabPreview({
|
||||
required this.tabId,
|
||||
required this.isActive,
|
||||
this.onTap,
|
||||
@@ -135,7 +102,7 @@ class TabPreview extends HookConsumerWidget {
|
||||
|
||||
final extendedDeleteMenuController = useMenuController();
|
||||
|
||||
return TabContainer(
|
||||
return GridTabItemContainer(
|
||||
isActive: isActive,
|
||||
isPrivate: tabState.isPrivate,
|
||||
child: InkWell(
|
||||
@@ -258,7 +225,145 @@ class TabPreview extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SingleTabPreview extends HookConsumerWidget {
|
||||
class ListTabPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
final bool isActive;
|
||||
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final VoidCallback? onDelete;
|
||||
final void Function(String host)? onDeleteAll;
|
||||
|
||||
final Widget? trailingChild;
|
||||
|
||||
const ListTabPreview({
|
||||
required this.tabId,
|
||||
required this.isActive,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onDelete,
|
||||
this.onDeleteAll,
|
||||
this.trailingChild,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final tabState = ref.watch(tabStateProvider(tabId));
|
||||
if (tabState == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final extendedDeleteMenuController = useMenuController();
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: tabState.isPrivate ? const Color(0xFF25003E) : null,
|
||||
border: isActive ? Border.all(color: colorScheme.primary) : null,
|
||||
// borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
key: ValueKey(tabState.id),
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
contentPadding: const EdgeInsets.only(left: 4),
|
||||
leading: (tabState.thumbnail != null)
|
||||
? RepaintBoundary(
|
||||
child: RawImage(
|
||||
image: tabState.thumbnail!.value,
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
)
|
||||
: TabIcon(tabState: tabState, iconSize: 32),
|
||||
title: Text(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
tabState.titleOrAuthority,
|
||||
maxLines: 2,
|
||||
style: tabState.isPrivate
|
||||
? const TextStyle(color: Colors.white)
|
||||
: null,
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: UriBreadcrumb(
|
||||
uri: tabState.url,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: tabState.isPrivate ? Colors.white : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (tabState.isPrivate) ...[
|
||||
const SizedBox(width: 6.0),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
width: 24,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned(
|
||||
top: -4,
|
||||
child: Icon(
|
||||
MdiIcons.dominoMask,
|
||||
color: Color(0xFF8000D7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (onDelete != null || onDeleteAll != null)
|
||||
MenuAnchor(
|
||||
controller: extendedDeleteMenuController,
|
||||
builder: (context, controller, child) {
|
||||
return child!;
|
||||
},
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
onDeleteAll?.call(tabState.url.host);
|
||||
},
|
||||
|
||||
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
||||
child: Text('Close all from ${tabState.url.host}'),
|
||||
),
|
||||
],
|
||||
child: IconButton(
|
||||
onPressed: onDelete,
|
||||
onLongPress: onDeleteAll != null
|
||||
? () {
|
||||
if (extendedDeleteMenuController.isOpen) {
|
||||
extendedDeleteMenuController.close();
|
||||
} else {
|
||||
extendedDeleteMenuController.open();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
?trailingChild,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SingleGridTabPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
final String? activeTabId;
|
||||
final String? sourceSearchQuery;
|
||||
@@ -267,7 +372,7 @@ class SingleTabPreview extends HookConsumerWidget {
|
||||
final void Function() onClose;
|
||||
final void Function()? onBeforeDelete;
|
||||
|
||||
const SingleTabPreview({
|
||||
const SingleGridTabPreview({
|
||||
required this.tabId,
|
||||
required this.activeTabId,
|
||||
required this.onClose,
|
||||
@@ -319,7 +424,7 @@ class SingleTabPreview extends HookConsumerWidget {
|
||||
},
|
||||
child: Opacity(
|
||||
opacity: 1.0 - draggedDistance.value / deleteThreshold,
|
||||
child: TabPreview(
|
||||
child: GridTabPreview(
|
||||
tabId: tabId,
|
||||
isActive: tabId == activeTabId,
|
||||
onTap: () async {
|
||||
@@ -341,7 +446,7 @@ class SingleTabPreview extends HookConsumerWidget {
|
||||
onDeleteAll: (host) async {
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabId);
|
||||
.getTabContainerId(tabId);
|
||||
|
||||
final count = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
@@ -382,14 +487,138 @@ class SingleTabPreview extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestedSingleTabPreview extends StatelessWidget {
|
||||
class SingleListTabPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
final String? activeTabId;
|
||||
final String? sourceSearchQuery;
|
||||
final double deleteThreshold;
|
||||
|
||||
final void Function() onClose;
|
||||
final void Function()? onBeforeDelete;
|
||||
|
||||
const SingleListTabPreview({
|
||||
required this.tabId,
|
||||
required this.activeTabId,
|
||||
required this.onClose,
|
||||
required this.sourceSearchQuery,
|
||||
this.deleteThreshold = 100,
|
||||
this.onBeforeDelete,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hasTabState = ref.watch(
|
||||
tabStateProvider(tabId).select((value) => value != null),
|
||||
);
|
||||
|
||||
if (!hasTabState) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final dragStartPosition = useRef(Offset.zero);
|
||||
final draggedDistance = useState(0.0);
|
||||
|
||||
return GestureDetector(
|
||||
onHorizontalDragStart: (details) {
|
||||
dragStartPosition.value = details.globalPosition;
|
||||
draggedDistance.value = 0.0;
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
final cappedDistance = math.min(
|
||||
(dragStartPosition.value - details.globalPosition).dx.abs(),
|
||||
deleteThreshold,
|
||||
);
|
||||
|
||||
draggedDistance.value = cappedDistance;
|
||||
},
|
||||
onHorizontalDragEnd: (details) async {
|
||||
if (draggedDistance.value >= deleteThreshold) {
|
||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
draggedDistance.value = 0.0;
|
||||
},
|
||||
child: Opacity(
|
||||
opacity: 1.0 - draggedDistance.value / deleteThreshold,
|
||||
child: ListTabPreview(
|
||||
tabId: tabId,
|
||||
isActive: tabId == activeTabId,
|
||||
onTap: () async {
|
||||
if (tabId != activeTabId) {
|
||||
//Close first to avoid rebuilds
|
||||
onClose();
|
||||
await ref.read(tabRepositoryProvider.notifier).selectTab(tabId);
|
||||
if (sourceSearchQuery.isNotEmpty &&
|
||||
ref.read(findInPageControllerProvider(tabId)) ==
|
||||
FindInPageState.hidden()) {
|
||||
await ref
|
||||
.read(findInPageControllerProvider(tabId).notifier)
|
||||
.findAll(text: sourceSearchQuery!);
|
||||
}
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
onDeleteAll: (host) async {
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabContainerId(tabId);
|
||||
|
||||
final count = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.closeAllTabsByHost(containerId, host);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||
count: count,
|
||||
);
|
||||
}
|
||||
},
|
||||
// onDoubleTap: () {
|
||||
// ref.read(overlayDialogControllerProvider.notifier).show(
|
||||
// TabActionDialog(
|
||||
// initialTab: tab,
|
||||
// onDismiss:
|
||||
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
onDelete: () async {
|
||||
onBeforeDelete?.call();
|
||||
|
||||
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestedSingleGridTabPreview extends StatelessWidget {
|
||||
final String tabId;
|
||||
|
||||
final String? activeTabId;
|
||||
|
||||
final void Function()? onTap;
|
||||
|
||||
const SuggestedSingleTabPreview({
|
||||
const SuggestedSingleGridTabPreview({
|
||||
required this.tabId,
|
||||
required this.activeTabId,
|
||||
this.onTap,
|
||||
@@ -400,7 +629,7 @@ class SuggestedSingleTabPreview extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: 0.5,
|
||||
child: TabPreview(
|
||||
child: GridTabPreview(
|
||||
tabId: tabId,
|
||||
isActive: tabId == activeTabId,
|
||||
onTap: onTap,
|
||||
@@ -414,133 +643,34 @@ class SuggestedSingleTabPreview extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class TabTreePreview extends HookConsumerWidget {
|
||||
final TabTreeEntity entity;
|
||||
class SuggestedSingleListTabPreview extends StatelessWidget {
|
||||
final String tabId;
|
||||
|
||||
final String? activeTabId;
|
||||
|
||||
final Offset stackPadding;
|
||||
final void Function()? onTap;
|
||||
|
||||
final void Function() onClose;
|
||||
|
||||
TabTreePreview({
|
||||
required this.entity,
|
||||
const SuggestedSingleListTabPreview({
|
||||
required this.tabId,
|
||||
required this.activeTabId,
|
||||
required this.onClose,
|
||||
required this.stackPadding,
|
||||
}) : super(key: ValueKey(entity));
|
||||
|
||||
Widget _addPadding(int stackCount, int totalCount, Widget child) {
|
||||
final topPadding = stackPadding * stackCount.toDouble();
|
||||
final bottomPadding = stackPadding * (totalCount - stackCount).toDouble();
|
||||
|
||||
return Positioned(
|
||||
top: topPadding.dy,
|
||||
left: topPadding.dx,
|
||||
right: bottomPadding.dx,
|
||||
bottom: bottomPadding.dy,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isTabPrivate = ref.watch(
|
||||
tabStateProvider(entity.tabId).select((value) => value?.isPrivate),
|
||||
);
|
||||
|
||||
if (isTabPrivate == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final stackCount = math.min(entity.totalTabs, 3) - 1;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
for (var index = 0; index < stackCount; index++)
|
||||
_addPadding(
|
||||
index,
|
||||
stackCount,
|
||||
TabContainer(
|
||||
isActive: entity.tabId == activeTabId,
|
||||
isPrivate: isTabPrivate == true,
|
||||
),
|
||||
),
|
||||
_addPadding(
|
||||
stackCount,
|
||||
stackCount,
|
||||
Badge.count(
|
||||
isLabelVisible: entity.totalTabs > 1,
|
||||
count: entity.totalTabs,
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
offset: const Offset(-8, -24),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
child: TabPreview(
|
||||
tabId: entity.tabId,
|
||||
isActive: entity.tabId == activeTabId,
|
||||
onLongPress: () async {
|
||||
if (entity.tabId != activeTabId) {
|
||||
//Close first to avoid rebuilds
|
||||
onClose();
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.selectTab(entity.tabId);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
onTap: () async {
|
||||
if (entity.totalTabs > 1) {
|
||||
await TabTreeRoute(entity.rootId).push(context);
|
||||
} else if (entity.tabId != activeTabId) {
|
||||
//Close first to avoid rebuilds
|
||||
onClose();
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.selectTab(entity.tabId);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
// onDeleteAll: (host) async {
|
||||
// final containerId = await ref
|
||||
// .read(tabDataRepositoryProvider.notifier)
|
||||
// .containerTabId(tab.id);
|
||||
|
||||
// await ref
|
||||
// .read(tabDataRepositoryProvider.notifier)
|
||||
// .closeAllTabsByHost(containerId, host);
|
||||
// },
|
||||
// onDoubleTap: () {
|
||||
// ref.read(overlayDialogControllerProvider.notifier).show(
|
||||
// TabActionDialog(
|
||||
// initialTab: tab,
|
||||
// onDismiss:
|
||||
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
onDelete: () async {
|
||||
final tabs = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabDescendants(entity.rootId);
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.closeTabs(tabs.keys.toList());
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||
count: tabs.length,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: 0.5,
|
||||
child: ListTabPreview(
|
||||
tabId: tabId,
|
||||
isActive: tabId == activeTabId,
|
||||
onTap: onTap,
|
||||
trailingChild: const IconButton(
|
||||
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
|
||||
icon: Icon(MdiIcons.creation),
|
||||
onPressed: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
class _TabTreePreview extends HookConsumerWidget {
|
||||
final TabTreeEntity entity;
|
||||
final String? activeTabId;
|
||||
|
||||
final Offset stackPadding;
|
||||
|
||||
final void Function() onClose;
|
||||
|
||||
_TabTreePreview({
|
||||
required this.entity,
|
||||
required this.activeTabId,
|
||||
required this.onClose,
|
||||
required this.stackPadding,
|
||||
}) : super(key: ValueKey(entity));
|
||||
|
||||
Widget _addPadding(int stackCount, int totalCount, Widget child) {
|
||||
final topPadding = stackPadding * stackCount.toDouble();
|
||||
final bottomPadding = stackPadding * (totalCount - stackCount).toDouble();
|
||||
|
||||
return Positioned(
|
||||
top: topPadding.dy,
|
||||
left: topPadding.dx,
|
||||
right: bottomPadding.dx,
|
||||
bottom: bottomPadding.dy,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isTabPrivate = ref.watch(
|
||||
tabStateProvider(entity.tabId).select((value) => value?.isPrivate),
|
||||
);
|
||||
|
||||
if (isTabPrivate == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final stackCount = math.min(entity.totalTabs, 3) - 1;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
for (var index = 0; index < stackCount; index++)
|
||||
_addPadding(
|
||||
index,
|
||||
stackCount,
|
||||
GridTabItemContainer(
|
||||
isActive: entity.tabId == activeTabId,
|
||||
isPrivate: isTabPrivate == true,
|
||||
),
|
||||
),
|
||||
_addPadding(
|
||||
stackCount,
|
||||
stackCount,
|
||||
Badge.count(
|
||||
isLabelVisible: entity.totalTabs > 1,
|
||||
count: entity.totalTabs,
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
offset: const Offset(-8, -24),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
child: GridTabPreview(
|
||||
tabId: entity.tabId,
|
||||
isActive: entity.tabId == activeTabId,
|
||||
onLongPress: () async {
|
||||
if (entity.tabId != activeTabId) {
|
||||
//Close first to avoid rebuilds
|
||||
onClose();
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.selectTab(entity.tabId);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
onTap: () async {
|
||||
if (entity.totalTabs > 1) {
|
||||
await TabTreeRoute(entity.rootId).push(context);
|
||||
} else if (entity.tabId != activeTabId) {
|
||||
//Close first to avoid rebuilds
|
||||
onClose();
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.selectTab(entity.tabId);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
// onDeleteAll: (host) async {
|
||||
// final containerId = await ref
|
||||
// .read(tabDataRepositoryProvider.notifier)
|
||||
// .containerTabId(tab.id);
|
||||
|
||||
// await ref
|
||||
// .read(tabDataRepositoryProvider.notifier)
|
||||
// .closeAllTabsByHost(containerId, host);
|
||||
// },
|
||||
// onDoubleTap: () {
|
||||
// ref.read(overlayDialogControllerProvider.notifier).show(
|
||||
// TabActionDialog(
|
||||
// initialTab: tab,
|
||||
// onDismiss:
|
||||
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
onDelete: () async {
|
||||
final tabs = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getTabDescendants(entity.rootId);
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.closeTabs(tabs.keys.toList());
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||
count: tabs.length,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewTabTreesWidget extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final VoidCallback onClose;
|
||||
final bool showNewTabFab;
|
||||
|
||||
const ViewTabTreesWidget({
|
||||
required this.onClose,
|
||||
required this.scrollController,
|
||||
required this.showNewTabFab,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
TabViewHeader(onClose: onClose, tabsViewMode: TabsViewMode.tree),
|
||||
Expanded(
|
||||
child: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
searchPartition: TabSearchPartition.preview,
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterDisabled(),
|
||||
groupTrees: true,
|
||||
),
|
||||
);
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(
|
||||
calculatedCount,
|
||||
filteredTabEntities.value.length,
|
||||
),
|
||||
2,
|
||||
);
|
||||
}, [screenWidth, filteredTabEntities.value.length]);
|
||||
|
||||
final itemSize = useMemoized(
|
||||
() => calculateItemSize(
|
||||
screenWidth: screenWidth,
|
||||
childAspectRatio: 0.75,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 8.0,
|
||||
),
|
||||
[screenWidth, crossAxisCount],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
final offset = (index ~/ 2) * itemSize.height;
|
||||
|
||||
if (offset != scrollController.offset) {
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
final tabs = useMemoized(() {
|
||||
return filteredTabEntities.value
|
||||
.whereType<TabTreeEntity>()
|
||||
.map((entity) {
|
||||
return _TabTreePreview(
|
||||
entity: entity,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
stackPadding: const Offset(8, 8),
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: GridView.builder(
|
||||
controller: scrollController,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: tabs.length,
|
||||
itemBuilder: (context, index) => tabs[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showNewTabFab)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: TabViewHeader.headerSize + 4,
|
||||
right: 4,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.defaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
onClose();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+371
@@ -0,0 +1,371 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/hooks/listenable_callback.dart';
|
||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
class TabViewHeader extends HookConsumerWidget {
|
||||
static const headerSize = 124.0;
|
||||
|
||||
final TabsViewMode tabsViewMode;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const TabViewHeader({required this.onClose, required this.tabsViewMode});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchMode = useState(false);
|
||||
final searchTextFocus = useFocusNode();
|
||||
final searchTextController = useTextEditingController();
|
||||
|
||||
final viewModeMenuController = useMenuController();
|
||||
final tabsActionMenuController = useMenuController();
|
||||
|
||||
final hasSearchText = useListenableSelector(
|
||||
searchTextController,
|
||||
() => searchTextController.text.isNotEmpty,
|
||||
);
|
||||
|
||||
final enableAiFeatures = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(settings) => settings.enableLocalAiFeatures,
|
||||
),
|
||||
);
|
||||
|
||||
useListenableCallback(searchTextController, () async {
|
||||
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
||||
await ref
|
||||
.read(
|
||||
tabSearchRepositoryProvider(TabSearchPartition.preview).notifier,
|
||||
)
|
||||
.addQuery(searchTextController.text);
|
||||
}
|
||||
});
|
||||
|
||||
return Material(
|
||||
//Fix layout issue https://github.com/flutter/flutter/issues/78748#issuecomment-1194680555
|
||||
child: Align(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0, left: 8.0, top: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!searchMode.value)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(MdiIcons.tabSearch),
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
switch (ref.read(tabsViewModeControllerProvider)) {
|
||||
case TabsViewMode.tree:
|
||||
case TabsViewMode.list:
|
||||
break;
|
||||
case TabsViewMode.grid:
|
||||
ref
|
||||
.read(
|
||||
tabsViewModeControllerProvider.notifier,
|
||||
)
|
||||
.set(TabsViewMode.list);
|
||||
}
|
||||
|
||||
searchMode.value = true;
|
||||
searchTextFocus.requestFocus();
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
child: VerticalDivider(indent: 4, endIndent: 4),
|
||||
),
|
||||
MenuAnchor(
|
||||
controller: viewModeMenuController,
|
||||
menuChildren: TabsViewMode.values
|
||||
.map(
|
||||
(mode) => MenuItemButton(
|
||||
leadingIcon: Icon(mode.icon),
|
||||
child: Text(mode.label),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
tabsViewModeControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.set(mode);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (viewModeMenuController.isOpen) {
|
||||
viewModeMenuController.close();
|
||||
} else {
|
||||
viewModeMenuController.open();
|
||||
}
|
||||
},
|
||||
icon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(tabsViewMode.icon, size: 18),
|
||||
const Icon(Icons.arrow_drop_down, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (enableAiFeatures &&
|
||||
switch (tabsViewMode) {
|
||||
TabsViewMode.list || TabsViewMode.grid => true,
|
||||
TabsViewMode.tree => false,
|
||||
})
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabSuggestionsEnabled = ref.watch(
|
||||
tabSuggestionsControllerProvider,
|
||||
);
|
||||
|
||||
return IconButton.filledTonal(
|
||||
icon: const Icon(MdiIcons.imageAutoAdjust),
|
||||
isSelected: tabSuggestionsEnabled,
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
tabSuggestionsControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.toggle();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if (switch (tabsViewMode) {
|
||||
TabsViewMode.list || TabsViewMode.grid => true,
|
||||
TabsViewMode.tree => false,
|
||||
})
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabsReorderabe = ref.watch(
|
||||
tabsReorderableControllerProvider,
|
||||
);
|
||||
|
||||
return IconButton.filledTonal(
|
||||
icon: const Icon(
|
||||
MdiIcons.orderNumericAscending,
|
||||
),
|
||||
isSelected: tabsReorderabe,
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
tabsReorderableControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.toggle();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
MenuAnchor(
|
||||
controller: tabsActionMenuController,
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
||||
child: const Text('Bookmark all'),
|
||||
onPressed: () async {
|
||||
final containerId = ref.read(
|
||||
selectedContainerProvider,
|
||||
);
|
||||
|
||||
final tabData = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabsData(containerId);
|
||||
|
||||
for (final tab in tabData) {
|
||||
if (context.mounted) {
|
||||
await BookmarkEntryAddRoute(
|
||||
bookmarkInfo: jsonEncode(
|
||||
BookmarkInfo(
|
||||
title: tab.title,
|
||||
url: tab.url.toString(),
|
||||
).encode(),
|
||||
),
|
||||
).push(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
||||
child: const Text('Close All'),
|
||||
onPressed: () async {
|
||||
final result = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
icon: const Icon(Icons.warning),
|
||||
title: const Text('Close All Tabs'),
|
||||
content: const Text(
|
||||
'Are you sure you want to close all displayed tabs?',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
final container = ref.read(
|
||||
selectedContainerProvider,
|
||||
);
|
||||
|
||||
final count = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.closeAllTabsByContainer(container);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.undoClose,
|
||||
count: count,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (tabsActionMenuController.isOpen) {
|
||||
tabsActionMenuController.close();
|
||||
} else {
|
||||
tabsActionMenuController.open();
|
||||
}
|
||||
},
|
||||
icon: const Icon(MdiIcons.dotsVertical),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
TextField(
|
||||
controller: searchTextController,
|
||||
focusNode: searchTextFocus,
|
||||
// enableIMEPersonalizedLearning: !incognitoEnabled,
|
||||
decoration: InputDecoration(
|
||||
// border: InputBorder.none,
|
||||
prefixIcon: const Icon(MdiIcons.tabSearch, size: 18),
|
||||
hintText: 'Search inside tabs...',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!hasSearchText)
|
||||
SpeechToTextButton(
|
||||
onTextReceived: (data) {
|
||||
searchTextController.text = data.toString();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
searchTextController.clear();
|
||||
searchTextFocus.requestFocus();
|
||||
searchMode.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (switch (tabsViewMode) {
|
||||
TabsViewMode.list || TabsViewMode.grid => true,
|
||||
TabsViewMode.tree => false,
|
||||
})
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final selectedContainer = ref.watch(
|
||||
selectedContainerDataProvider.select(
|
||||
(value) => value.value,
|
||||
),
|
||||
);
|
||||
|
||||
return ContainerChips(
|
||||
showGroupSuggestions: true,
|
||||
selectedContainer: selectedContainer,
|
||||
onSelected: (container) async {
|
||||
if (container != null) {
|
||||
final result = await ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.setContainerId(container.id);
|
||||
|
||||
if (context.mounted &&
|
||||
result == SetContainerResult.successHasProxy) {
|
||||
await ref
|
||||
.read(startProxyControllerProvider.notifier)
|
||||
.maybeStartProxy(context);
|
||||
}
|
||||
} else {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
}
|
||||
},
|
||||
onDeleted: (container) {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
},
|
||||
onLongPress: (container) async {
|
||||
await ContainerEditRoute(
|
||||
containerData: jsonEncode(container.toJson()),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,988 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:flutter_reorderable_grid_view/widgets/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/providers/global_drop.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/data/models/drag_data.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/hooks/listenable_callback.dart';
|
||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
int calculateCrossAxisItemCount({
|
||||
required double screenWidth,
|
||||
required double horizontalPadding,
|
||||
required double crossAxisSpacing,
|
||||
}) {
|
||||
final totalHorizontalPadding = horizontalPadding * 2;
|
||||
final availableWidth =
|
||||
screenWidth - totalHorizontalPadding - crossAxisSpacing;
|
||||
|
||||
final crossAxisCount = availableWidth ~/ 180.0;
|
||||
|
||||
return crossAxisCount;
|
||||
}
|
||||
|
||||
Size calculateItemSize({
|
||||
required double screenWidth,
|
||||
required double childAspectRatio,
|
||||
required double horizontalPadding,
|
||||
required double mainAxisSpacing,
|
||||
required double crossAxisSpacing,
|
||||
required int crossAxisCount,
|
||||
}) {
|
||||
final totalHorizontalPadding = horizontalPadding * 2;
|
||||
final totalCrossAxisSpacing = crossAxisSpacing * (crossAxisCount - 1);
|
||||
final availableWidth =
|
||||
screenWidth - totalHorizontalPadding - totalCrossAxisSpacing;
|
||||
final itemWidth = availableWidth / crossAxisCount;
|
||||
final itemHeight = itemWidth / childAspectRatio;
|
||||
|
||||
return Size(itemWidth, itemHeight + mainAxisSpacing);
|
||||
}
|
||||
|
||||
class _TabDraggable extends HookConsumerWidget {
|
||||
final TabEntity entity;
|
||||
final String? suggestedContainerId;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabDraggable({
|
||||
required this.entity,
|
||||
required this.onClose,
|
||||
this.suggestedContainerId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final dragData = ref.watch(
|
||||
willAcceptDropProvider.select((value) {
|
||||
final dragTabId = switch (value) {
|
||||
ContainerDropData() => value.tabId,
|
||||
DeleteDropData() => value.tabId,
|
||||
null => null,
|
||||
};
|
||||
|
||||
return (dragTabId == entity.tabId) ? value : null;
|
||||
}),
|
||||
);
|
||||
|
||||
// Cache the tab widget to avoid rebuilding
|
||||
final tab = useMemoized(() {
|
||||
return (suggestedContainerId != null)
|
||||
? SuggestedSingleTabPreview(
|
||||
key: ValueKey(entity.tabId),
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onTap: () async {
|
||||
final containerData = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(suggestedContainerId!);
|
||||
|
||||
if (containerData != null) {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignContainer(entity.tabId, containerData);
|
||||
}
|
||||
},
|
||||
)
|
||||
: SingleTabPreview(
|
||||
key: ValueKey(entity.tabId),
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
sourceSearchQuery: switch (entity) {
|
||||
DefaultTabEntity _ => null,
|
||||
final SearchResultTabEntity entity => entity.searchQuery,
|
||||
TabTreeEntity _ => throw UnimplementedError(),
|
||||
},
|
||||
);
|
||||
}, [entity.tabId, activeTab, suggestedContainerId]);
|
||||
|
||||
return switch (dragData) {
|
||||
ContainerDropData() => Opacity(
|
||||
opacity: 0.3,
|
||||
child: Transform.scale(scale: 0.9, child: tab),
|
||||
),
|
||||
DeleteDropData() => Opacity(
|
||||
opacity: 0.3,
|
||||
child: ColorFiltered(
|
||||
colorFilter: const ColorFilter.mode(Colors.red, BlendMode.modulate),
|
||||
child: tab,
|
||||
),
|
||||
),
|
||||
null => tab,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _TabViewHeader extends HookConsumerWidget {
|
||||
static const headerSize = 124.0;
|
||||
|
||||
final TabsViewMode tabsViewMode;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabViewHeader({required this.onClose, required this.tabsViewMode});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchMode = useState(false);
|
||||
final searchTextFocus = useFocusNode();
|
||||
final searchTextController = useTextEditingController();
|
||||
|
||||
final viewModeMenuController = useMenuController();
|
||||
final tabsActionMenuController = useMenuController();
|
||||
|
||||
final hasSearchText = useListenableSelector(
|
||||
searchTextController,
|
||||
() => searchTextController.text.isNotEmpty,
|
||||
);
|
||||
|
||||
final enableAiFeatures = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(settings) => settings.enableLocalAiFeatures,
|
||||
),
|
||||
);
|
||||
|
||||
useListenableCallback(searchTextController, () async {
|
||||
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
||||
await ref
|
||||
.read(
|
||||
tabSearchRepositoryProvider(TabSearchPartition.preview).notifier,
|
||||
)
|
||||
.addQuery(searchTextController.text);
|
||||
}
|
||||
});
|
||||
|
||||
return Material(
|
||||
//Fix layout issue https://github.com/flutter/flutter/issues/78748#issuecomment-1194680555
|
||||
child: Align(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0, left: 8.0, top: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!searchMode.value)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(MdiIcons.tabSearch),
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(tabsViewModeControllerProvider.notifier)
|
||||
.set(TabsViewMode.grid);
|
||||
|
||||
searchMode.value = true;
|
||||
searchTextFocus.requestFocus();
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
child: VerticalDivider(indent: 4, endIndent: 4),
|
||||
),
|
||||
MenuAnchor(
|
||||
controller: viewModeMenuController,
|
||||
menuChildren: TabsViewMode.values
|
||||
.map(
|
||||
(mode) => MenuItemButton(
|
||||
leadingIcon: Icon(mode.icon),
|
||||
child: Text(mode.label),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
tabsViewModeControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.set(mode);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (viewModeMenuController.isOpen) {
|
||||
viewModeMenuController.close();
|
||||
} else {
|
||||
viewModeMenuController.open();
|
||||
}
|
||||
},
|
||||
icon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(tabsViewMode.icon, size: 18),
|
||||
const Icon(Icons.arrow_drop_down, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (enableAiFeatures)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabSuggestionsEnabled = ref.watch(
|
||||
tabSuggestionsControllerProvider,
|
||||
);
|
||||
|
||||
return IconButton.filledTonal(
|
||||
icon: const Icon(MdiIcons.imageAutoAdjust),
|
||||
isSelected: tabSuggestionsEnabled,
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
tabSuggestionsControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.toggle();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabsReorderabe = ref.watch(
|
||||
tabsReorderableControllerProvider,
|
||||
);
|
||||
|
||||
return IconButton.filledTonal(
|
||||
icon: const Icon(MdiIcons.orderNumericAscending),
|
||||
isSelected: tabsReorderabe,
|
||||
iconSize: 18,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(
|
||||
tabsReorderableControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.toggle();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
MenuAnchor(
|
||||
controller: tabsActionMenuController,
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
||||
child: const Text('Bookmark all'),
|
||||
onPressed: () async {
|
||||
final containerId = ref.read(
|
||||
selectedContainerProvider,
|
||||
);
|
||||
|
||||
final tabData = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabsData(containerId);
|
||||
|
||||
for (final tab in tabData) {
|
||||
if (context.mounted) {
|
||||
await BookmarkEntryAddRoute(
|
||||
bookmarkInfo: jsonEncode(
|
||||
BookmarkInfo(
|
||||
title: tab.title,
|
||||
url: tab.url.toString(),
|
||||
).encode(),
|
||||
),
|
||||
).push(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
||||
child: const Text('Close All'),
|
||||
onPressed: () async {
|
||||
final result = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
icon: const Icon(Icons.warning),
|
||||
title: const Text('Close All Tabs'),
|
||||
content: const Text(
|
||||
'Are you sure you want to close all displayed tabs?',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
final container = ref.read(
|
||||
selectedContainerProvider,
|
||||
);
|
||||
|
||||
final count = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.closeAllTabsByContainer(container);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.undoClose,
|
||||
count: count,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (tabsActionMenuController.isOpen) {
|
||||
tabsActionMenuController.close();
|
||||
} else {
|
||||
tabsActionMenuController.open();
|
||||
}
|
||||
},
|
||||
icon: const Icon(MdiIcons.dotsVertical),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
TextField(
|
||||
controller: searchTextController,
|
||||
focusNode: searchTextFocus,
|
||||
// enableIMEPersonalizedLearning: !incognitoEnabled,
|
||||
decoration: InputDecoration(
|
||||
// border: InputBorder.none,
|
||||
prefixIcon: const Icon(MdiIcons.tabSearch, size: 18),
|
||||
hintText: 'Search inside tabs...',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!hasSearchText)
|
||||
SpeechToTextButton(
|
||||
onTextReceived: (data) {
|
||||
searchTextController.text = data.toString();
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
searchTextController.clear();
|
||||
searchTextFocus.requestFocus();
|
||||
searchMode.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (tabsViewMode == TabsViewMode.grid)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final selectedContainer = ref.watch(
|
||||
selectedContainerDataProvider.select(
|
||||
(value) => value.value,
|
||||
),
|
||||
);
|
||||
|
||||
return ContainerChips(
|
||||
showGroupSuggestions: true,
|
||||
selectedContainer: selectedContainer,
|
||||
onSelected: (container) async {
|
||||
if (container != null) {
|
||||
final result = await ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.setContainerId(container.id);
|
||||
|
||||
if (context.mounted &&
|
||||
result == SetContainerResult.successHasProxy) {
|
||||
await ref
|
||||
.read(startProxyControllerProvider.notifier)
|
||||
.maybeStartProxy(context);
|
||||
}
|
||||
} else {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
}
|
||||
},
|
||||
onDeleted: (container) {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
},
|
||||
onLongPress: (container) async {
|
||||
await ContainerEditRoute(
|
||||
containerData: jsonEncode(container.toJson()),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabView extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final bool tabsReorderable;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabView({
|
||||
required this.scrollController,
|
||||
required this.tabsReorderable,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final containerId = ref.watch(selectedContainerProvider);
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
searchPartition: TabSearchPartition.preview,
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterById(containerId: containerId),
|
||||
groupTrees: false,
|
||||
),
|
||||
);
|
||||
|
||||
final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider);
|
||||
|
||||
final suggestedTabEntities = tabSuggestionsEnabled
|
||||
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
||||
: EquatableValue(<TabEntity>[]);
|
||||
|
||||
final itemCount =
|
||||
filteredTabEntities.value.length +
|
||||
//Limit to 3 sugegstions for now
|
||||
math.min<int>(suggestedTabEntities.value.length, 3);
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(math.min(calculatedCount, itemCount), 2);
|
||||
}, [screenWidth, itemCount]);
|
||||
|
||||
final itemSize = useMemoized(
|
||||
() => calculateItemSize(
|
||||
screenWidth: screenWidth,
|
||||
childAspectRatio: 0.75,
|
||||
horizontalPadding: 4.0,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
[screenWidth, crossAxisCount],
|
||||
);
|
||||
|
||||
final lastScroll = useRef<String?>(null);
|
||||
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (scrollController.hasClients) {
|
||||
if (lastScroll.value != activeTab) {
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
final offset = (index ~/ 2) * itemSize.height;
|
||||
|
||||
if (offset != scrollController.offset) {
|
||||
lastScroll.value = activeTab;
|
||||
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: FadingScroll(
|
||||
fadingSize: 5,
|
||||
controller: scrollController,
|
||||
builder: (context, controller) {
|
||||
return !tabsReorderable
|
||||
? _TabGrid(
|
||||
key: ValueKey(crossAxisCount),
|
||||
crossAxisCount: crossAxisCount,
|
||||
itemCount: itemCount,
|
||||
scrollController: controller,
|
||||
itemBuilder: (widget, _) {
|
||||
if (widget is CustomDraggable) {
|
||||
return LongPressDraggable(
|
||||
feedback: Material(
|
||||
color: Colors
|
||||
.transparent, // removes white corners when having shadow
|
||||
child: Transform.scale(
|
||||
scale: 1.05,
|
||||
child: SizedBox(
|
||||
height: itemSize.height,
|
||||
width: itemSize.width,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
data: widget.data,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
return widget;
|
||||
},
|
||||
suggestedContainerId: containerId,
|
||||
filteredTabEntities: filteredTabEntities,
|
||||
suggestedTabEntities: suggestedTabEntities,
|
||||
onClose: onClose,
|
||||
)
|
||||
: ReorderableBuilder.builder(
|
||||
//Rebuild when cross axis count changes
|
||||
key: ValueKey(crossAxisCount),
|
||||
scrollController: controller,
|
||||
itemCount: itemCount,
|
||||
onDragStarted: (index) {
|
||||
ref.read(willAcceptDropProvider.notifier).clear();
|
||||
},
|
||||
onReorderPositions: (positions) async {
|
||||
assert(
|
||||
positions.length == 1,
|
||||
'Not ready for multiple reorders',
|
||||
);
|
||||
|
||||
final oldIndex = positions.first.oldIndex;
|
||||
final newIndex = positions.first.newIndex;
|
||||
|
||||
final containerRepository = ref.read(
|
||||
containerRepositoryProvider.notifier,
|
||||
);
|
||||
|
||||
//Suggestions are at the end and not reorderable, so skip
|
||||
if (oldIndex >= filteredTabEntities.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tabId = filteredTabEntities.value[oldIndex].tabId;
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabId);
|
||||
|
||||
final String key;
|
||||
if (newIndex <= 0) {
|
||||
key = await containerRepository.getLeadingOrderKey(
|
||||
containerId,
|
||||
);
|
||||
} else if (newIndex >=
|
||||
filteredTabEntities.value.length - 1) {
|
||||
key = await containerRepository.getTrailingOrderKey(
|
||||
containerId,
|
||||
);
|
||||
} else {
|
||||
if (newIndex < oldIndex) {
|
||||
key = (await containerRepository.getOrderKeyAfterTab(
|
||||
filteredTabEntities.value[newIndex - 1].tabId,
|
||||
containerId,
|
||||
))!;
|
||||
} else {
|
||||
key = await containerRepository.getOrderKeyBeforeTab(
|
||||
filteredTabEntities.value[newIndex + 1].tabId,
|
||||
containerId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignOrderKey(tabId, key);
|
||||
},
|
||||
childBuilder: (itemBuilder) {
|
||||
return _TabGrid(
|
||||
crossAxisCount: crossAxisCount,
|
||||
itemCount: itemCount,
|
||||
scrollController: controller,
|
||||
itemBuilder: itemBuilder,
|
||||
suggestedContainerId: containerId,
|
||||
filteredTabEntities: filteredTabEntities,
|
||||
suggestedTabEntities: suggestedTabEntities,
|
||||
onClose: onClose,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabGrid extends StatelessWidget {
|
||||
const _TabGrid({
|
||||
super.key,
|
||||
required this.crossAxisCount,
|
||||
required this.scrollController,
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
required this.suggestedContainerId,
|
||||
required this.filteredTabEntities,
|
||||
required this.suggestedTabEntities,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
final int crossAxisCount;
|
||||
final int itemCount;
|
||||
final ScrollController? scrollController;
|
||||
final String? suggestedContainerId;
|
||||
final EquatableValue<List<TabEntity>> filteredTabEntities;
|
||||
final EquatableValue<List<TabEntity>> suggestedTabEntities;
|
||||
final Widget Function(Widget, int)? itemBuilder;
|
||||
final VoidCallback onClose;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
final Widget tab;
|
||||
if (index < filteredTabEntities.value.length) {
|
||||
final entity = filteredTabEntities.value[index];
|
||||
tab = CustomDraggable(
|
||||
key: Key(entity.tabId),
|
||||
data: TabDragData(entity.tabId),
|
||||
child: _TabDraggable(entity: entity, onClose: onClose),
|
||||
);
|
||||
} else {
|
||||
final suggestedIndex = index - filteredTabEntities.value.length;
|
||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||
|
||||
tab = CustomDraggable(
|
||||
key: Key('suggested_${entity.tabId}'),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
suggestedContainerId: suggestedContainerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return (itemBuilder != null) ? itemBuilder!(tab, index) : tab;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewTabsWidget extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final DraggableScrollableController? draggableScrollableController;
|
||||
final bool showNewTabFab;
|
||||
final bool tabsReorderable;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const ViewTabsWidget({
|
||||
required this.onClose,
|
||||
required this.scrollController,
|
||||
this.draggableScrollableController,
|
||||
required this.tabsReorderable,
|
||||
required this.showNewTabFab,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
NestedScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
draggableScrollableController.mapNotNull(
|
||||
(draggableScrollableController) =>
|
||||
DraggableScrollableHeader(
|
||||
controller: draggableScrollableController,
|
||||
child: _TabViewHeader(
|
||||
onClose: onClose,
|
||||
tabsViewMode: TabsViewMode.grid,
|
||||
),
|
||||
),
|
||||
) ??
|
||||
_TabViewHeader(
|
||||
onClose: onClose,
|
||||
tabsViewMode: TabsViewMode.grid,
|
||||
),
|
||||
),
|
||||
],
|
||||
body: _TabView(
|
||||
scrollController: scrollController,
|
||||
tabsReorderable: tabsReorderable,
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
if (showNewTabFab)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: _TabViewHeader.headerSize + 4,
|
||||
right: 4,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.defaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
onClose();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewTabTreesWidget extends HookConsumerWidget {
|
||||
final ScrollController scrollController;
|
||||
final VoidCallback onClose;
|
||||
final bool showNewTabFab;
|
||||
|
||||
const ViewTabTreesWidget({
|
||||
required this.onClose,
|
||||
required this.scrollController,
|
||||
required this.showNewTabFab,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
_TabViewHeader(onClose: onClose, tabsViewMode: TabsViewMode.tree),
|
||||
Expanded(
|
||||
child: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
searchPartition: TabSearchPartition.preview,
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterDisabled(),
|
||||
groupTrees: true,
|
||||
),
|
||||
);
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(
|
||||
calculatedCount,
|
||||
filteredTabEntities.value.length,
|
||||
),
|
||||
2,
|
||||
);
|
||||
}, [screenWidth, filteredTabEntities.value.length]);
|
||||
|
||||
final itemSize = useMemoized(
|
||||
() => calculateItemSize(
|
||||
screenWidth: screenWidth,
|
||||
childAspectRatio: 0.75,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 8.0,
|
||||
),
|
||||
[screenWidth, crossAxisCount],
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
final index = filteredTabEntities.value.indexWhere(
|
||||
(entity) => entity.tabId == activeTab,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
final offset = (index ~/ 2) * itemSize.height;
|
||||
|
||||
if (offset != scrollController.offset) {
|
||||
unawaited(
|
||||
scrollController.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
final tabs = useMemoized(() {
|
||||
return filteredTabEntities.value
|
||||
.whereType<TabTreeEntity>()
|
||||
.map((entity) {
|
||||
return TabTreePreview(
|
||||
entity: entity,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
stackPadding: const Offset(8, 8),
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
}, [filteredTabEntities, activeTab]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: GridView.builder(
|
||||
controller: scrollController,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: tabs.length,
|
||||
itemBuilder: (context, index) => tabs[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showNewTabFab)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: _TabViewHeader.headerSize + 4,
|
||||
right: 4,
|
||||
),
|
||||
child: FloatingActionButton.small(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.defaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
onClose();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:ui';
|
||||
|
||||
int calculateCrossAxisItemCount({
|
||||
required double screenWidth,
|
||||
required double horizontalPadding,
|
||||
required double crossAxisSpacing,
|
||||
}) {
|
||||
final totalHorizontalPadding = horizontalPadding * 2;
|
||||
final availableWidth =
|
||||
screenWidth - totalHorizontalPadding - crossAxisSpacing;
|
||||
|
||||
final crossAxisCount = availableWidth ~/ 180.0;
|
||||
|
||||
return crossAxisCount;
|
||||
}
|
||||
|
||||
Size calculateItemSize({
|
||||
required double screenWidth,
|
||||
required double childAspectRatio,
|
||||
required double horizontalPadding,
|
||||
required double mainAxisSpacing,
|
||||
required double crossAxisSpacing,
|
||||
required int crossAxisCount,
|
||||
}) {
|
||||
final totalHorizontalPadding = horizontalPadding * 2;
|
||||
final totalCrossAxisSpacing = crossAxisSpacing * (crossAxisCount - 1);
|
||||
final availableWidth =
|
||||
screenWidth - totalHorizontalPadding - totalCrossAxisSpacing;
|
||||
final itemWidth = availableWidth / crossAxisCount;
|
||||
final itemHeight = itemWidth / childAspectRatio;
|
||||
|
||||
return Size(itemWidth, itemHeight + mainAxisSpacing);
|
||||
}
|
||||
@@ -17,11 +17,10 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/experimental/json_persist.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/entities/history_filter_options.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||
@@ -30,6 +29,7 @@ import 'package:weblibre/features/user/data/providers.dart';
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
@JsonPersist()
|
||||
class HistoryFilter extends _$HistoryFilter {
|
||||
void updateVisitType(VisitType type, bool value) {
|
||||
if (value) {
|
||||
@@ -49,15 +49,9 @@ class HistoryFilter extends _$HistoryFilter {
|
||||
|
||||
@override
|
||||
HistoryFilterOptions build() {
|
||||
final riverpodStorage = ref.watch(riverpodDatabaseStorageProvider);
|
||||
|
||||
persist(
|
||||
riverpodStorage,
|
||||
ref.watch(riverpodDatabaseStorageProvider),
|
||||
key: 'HistoryFilterOptions',
|
||||
decode: (encoded) => HistoryFilterOptions.fromJson(
|
||||
jsonDecode(encoded) as Map<String, dynamic>,
|
||||
),
|
||||
encode: (state) => jsonEncode(state.toJson()),
|
||||
);
|
||||
|
||||
return stateOrNull ?? HistoryFilterOptions.withDefaults();
|
||||
|
||||
@@ -10,8 +10,10 @@ part of 'providers.dart';
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(HistoryFilter)
|
||||
@JsonPersist()
|
||||
const historyFilterProvider = HistoryFilterProvider._();
|
||||
|
||||
@JsonPersist()
|
||||
final class HistoryFilterProvider
|
||||
extends $NotifierProvider<HistoryFilter, HistoryFilterOptions> {
|
||||
const HistoryFilterProvider._()
|
||||
@@ -41,9 +43,10 @@ final class HistoryFilterProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$historyFilterHash() => r'1f8269e5f1227d6efde194e00c3d79a268ab1e4d';
|
||||
String _$historyFilterHash() => r'685b92590011ec450ef0c19820e4a6750d26b79d';
|
||||
|
||||
abstract class _$HistoryFilter extends $Notifier<HistoryFilterOptions> {
|
||||
@JsonPersist()
|
||||
abstract class _$HistoryFilterBase extends $Notifier<HistoryFilterOptions> {
|
||||
HistoryFilterOptions build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
@@ -100,3 +103,40 @@ final class BrowsingHistoryProvider
|
||||
}
|
||||
|
||||
String _$browsingHistoryHash() => r'3b6ee5853387481d7544bc9733741a5519f84f40';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
abstract class _$HistoryFilter extends _$HistoryFilterBase {
|
||||
/// The default key used by [persist].
|
||||
String get key {
|
||||
const resolvedKey = "HistoryFilter";
|
||||
return resolvedKey;
|
||||
}
|
||||
|
||||
/// A variant of [persist], for JSON-specific encoding.
|
||||
///
|
||||
/// You can override [key] to customize the key used for storage.
|
||||
PersistResult persist(
|
||||
FutureOr<Storage<String, String>> storage, {
|
||||
String? key,
|
||||
String Function(HistoryFilterOptions state)? encode,
|
||||
HistoryFilterOptions Function(String encoded)? decode,
|
||||
StorageOptions options = const StorageOptions(),
|
||||
}) {
|
||||
return NotifierPersistX(this).persist<String, String>(
|
||||
storage,
|
||||
key: key ?? this.key,
|
||||
encode: encode ?? $jsonCodex.encode,
|
||||
decode:
|
||||
decode ??
|
||||
(encoded) {
|
||||
final e = $jsonCodex.decode(encoded);
|
||||
return HistoryFilterOptions.fromJson(e as Map<String, Object?>);
|
||||
},
|
||||
options: options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,18 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
||||
return query.map((row) => row.read(db.tab.containerId));
|
||||
}
|
||||
|
||||
Selectable<MapEntry<String, String?>> getTabsContainerId(
|
||||
Iterable<String> tabIds,
|
||||
) {
|
||||
final query = selectOnly(db.tab)
|
||||
..addColumns([db.tab.id, db.tab.containerId])
|
||||
..where(db.tab.id.isIn(tabIds));
|
||||
|
||||
return query.map(
|
||||
(row) => MapEntry(row.read(db.tab.id)!, row.read(db.tab.containerId)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _generateOrderKey({
|
||||
required Value<String?> parentId,
|
||||
required Value<String?> containerId,
|
||||
|
||||
@@ -140,6 +140,7 @@ queryTabsBasic WITH TabQueryResult:
|
||||
)
|
||||
SELECT
|
||||
t.id,
|
||||
t.container_id,
|
||||
t.title,
|
||||
CAST(t.url AS TEXT) AS url,
|
||||
t.url AS clean_url,
|
||||
@@ -166,6 +167,7 @@ queryTabsFullContent WITH TabQueryResult:
|
||||
)
|
||||
SELECT
|
||||
t.id,
|
||||
t.container_id,
|
||||
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
||||
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
|
||||
|
||||
@@ -2161,12 +2161,13 @@ class DefinitionsDrift extends i7.ModularAccessor {
|
||||
|
||||
i0.Selectable<i8.TabQueryResult> queryTabsBasic({required String query}) {
|
||||
return customSelect(
|
||||
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, t.title, CAST(t.url AS TEXT) AS url, t.url AS clean_url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?1 OR fts.url LIKE ?1 ORDER BY weighted_rank ASC, t.timestamp DESC',
|
||||
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, t.container_id, t.title, CAST(t.url AS TEXT) AS url, t.url AS clean_url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?1 OR fts.url LIKE ?1 ORDER BY weighted_rank ASC, t.timestamp DESC',
|
||||
variables: [i0.Variable<String>(query)],
|
||||
readsFrom: {tab, tabFts},
|
||||
).map(
|
||||
(i0.QueryRow row) => i8.TabQueryResult(
|
||||
id: row.read<String>('id'),
|
||||
containerId: row.readNullable<String>('container_id'),
|
||||
title: row.readNullable<String>('title'),
|
||||
url: row.readNullable<String>('url'),
|
||||
cleanUrl: i3.Tab.$converterurl.fromSql(
|
||||
@@ -2185,7 +2186,7 @@ class DefinitionsDrift extends i7.ModularAccessor {
|
||||
required String query,
|
||||
}) {
|
||||
return customSelect(
|
||||
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content, t.url AS clean_url,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
|
||||
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, t.container_id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content, t.url AS clean_url,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
|
||||
variables: [
|
||||
i0.Variable<String>(beforeMatch),
|
||||
i0.Variable<String>(afterMatch),
|
||||
@@ -2197,6 +2198,7 @@ class DefinitionsDrift extends i7.ModularAccessor {
|
||||
).map(
|
||||
(i0.QueryRow row) => i8.TabQueryResult(
|
||||
id: row.read<String>('id'),
|
||||
containerId: row.readNullable<String>('container_id'),
|
||||
title: row.readNullable<String>('title'),
|
||||
url: row.readNullable<String>('url'),
|
||||
cleanUrl: i3.Tab.$converterurl.fromSql(
|
||||
|
||||
@@ -26,23 +26,29 @@ sealed class TabEntity with FastEquatable {
|
||||
class DefaultTabEntity extends TabEntity {
|
||||
@override
|
||||
final String tabId;
|
||||
final String? containerId;
|
||||
|
||||
DefaultTabEntity({required this.tabId});
|
||||
DefaultTabEntity({required this.tabId, required this.containerId});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [tabId];
|
||||
List<Object?> get hashParameters => [tabId, containerId];
|
||||
}
|
||||
|
||||
class SearchResultTabEntity extends TabEntity {
|
||||
@override
|
||||
final String tabId;
|
||||
final String? containerId;
|
||||
|
||||
final String searchQuery;
|
||||
|
||||
SearchResultTabEntity({required this.tabId, required this.searchQuery});
|
||||
SearchResultTabEntity({
|
||||
required this.tabId,
|
||||
required this.searchQuery,
|
||||
required this.containerId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [tabId, searchQuery];
|
||||
List<Object?> get hashParameters => [tabId, searchQuery, containerId];
|
||||
}
|
||||
|
||||
class TabTreeEntity extends TabEntity {
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class TabQueryResult with FastEquatable {
|
||||
final String id;
|
||||
final String? containerId;
|
||||
|
||||
final String? title;
|
||||
final Uri? cleanUrl;
|
||||
@@ -33,6 +34,7 @@ class TabQueryResult with FastEquatable {
|
||||
|
||||
TabQueryResult({
|
||||
required this.id,
|
||||
required this.containerId,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.cleanUrl,
|
||||
@@ -44,6 +46,7 @@ class TabQueryResult with FastEquatable {
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
id,
|
||||
containerId,
|
||||
title,
|
||||
cleanUrl,
|
||||
url,
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -129,6 +130,19 @@ Stream<String?> watchContainerTabId(Ref ref, String tabId) {
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<Map<String, String?>> watchTabsContainerId(
|
||||
Ref ref,
|
||||
EquatableValue<List<String>> tabIds,
|
||||
) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.tabDao
|
||||
.getTabsContainerId(tabIds.value)
|
||||
.watch()
|
||||
.map(Map.fromEntries);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
||||
return ref.read(tabDatabaseProvider).containerDao.allAssignedSites().watch();
|
||||
|
||||
@@ -671,6 +671,88 @@ final class WatchContainerTabIdFamily extends $Family
|
||||
String toString() => r'watchContainerTabIdProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(watchTabsContainerId)
|
||||
const watchTabsContainerIdProvider = WatchTabsContainerIdFamily._();
|
||||
|
||||
final class WatchTabsContainerIdProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<Map<String, String?>>,
|
||||
Map<String, String?>,
|
||||
Stream<Map<String, String?>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<Map<String, String?>>,
|
||||
$StreamProvider<Map<String, String?>> {
|
||||
const WatchTabsContainerIdProvider._({
|
||||
required WatchTabsContainerIdFamily super.from,
|
||||
required EquatableValue<List<String>> super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'watchTabsContainerIdProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$watchTabsContainerIdHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'watchTabsContainerIdProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<Map<String, String?>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<Map<String, String?>> create(Ref ref) {
|
||||
final argument = this.argument as EquatableValue<List<String>>;
|
||||
return watchTabsContainerId(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is WatchTabsContainerIdProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$watchTabsContainerIdHash() =>
|
||||
r'ffdebb10f7118cdff93695c821ef5fbb0d434de1';
|
||||
|
||||
final class WatchTabsContainerIdFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
Stream<Map<String, String?>>,
|
||||
EquatableValue<List<String>>
|
||||
> {
|
||||
const WatchTabsContainerIdFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'watchTabsContainerIdProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
WatchTabsContainerIdProvider call(EquatableValue<List<String>> tabIds) =>
|
||||
WatchTabsContainerIdProvider._(argument: tabIds, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'watchTabsContainerIdProvider';
|
||||
}
|
||||
|
||||
@ProviderFor(watchAllAssignedSites)
|
||||
const watchAllAssignedSitesProvider = WatchAllAssignedSitesProvider._();
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ class SelectedContainer extends _$SelectedContainer {
|
||||
if (next != null) {
|
||||
final tabContainerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(next);
|
||||
.getTabContainerId(next);
|
||||
|
||||
if (ref.mounted && tabContainerId != stateOrNull) {
|
||||
if (tabContainerId != null) {
|
||||
|
||||
@@ -41,7 +41,7 @@ final class SelectedContainerProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedContainerHash() => r'fd41c7d7ff0cfaff2829fd93e2afd41d883b8317';
|
||||
String _$selectedContainerHash() => r'35a8fc409e563758156a197bb8b114387d31b389';
|
||||
|
||||
abstract class _$SelectedContainer extends $Notifier<String?> {
|
||||
String? build();
|
||||
|
||||
@@ -34,7 +34,7 @@ class TabDataRepository extends _$TabDataRepository {
|
||||
ContainerData targetContainer, {
|
||||
bool closeOldTab = true,
|
||||
}) async {
|
||||
final currentContainerId = await getContainerTabId(tabId);
|
||||
final currentContainerId = await getTabContainerId(tabId);
|
||||
|
||||
final currentContainerData = await currentContainerId.mapNotNull(
|
||||
(containerId) => ref
|
||||
@@ -118,7 +118,7 @@ class TabDataRepository extends _$TabDataRepository {
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<String?> getContainerTabId(String tabId) {
|
||||
Future<String?> getTabContainerId(String tabId) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.tabDao
|
||||
@@ -126,6 +126,15 @@ class TabDataRepository extends _$TabDataRepository {
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<Map<String, String?>> getTabsContainerId(Iterable<String> tabIds) {
|
||||
return ref
|
||||
.read(tabDatabaseProvider)
|
||||
.tabDao
|
||||
.getTabsContainerId(tabIds)
|
||||
.get()
|
||||
.then(Map.fromEntries);
|
||||
}
|
||||
|
||||
Future<Map<String, String?>> getTabDescendants(String tabId) async {
|
||||
final results = await ref
|
||||
.read(tabDatabaseProvider)
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabDataRepositoryHash() => r'e12750bc1962878a5bf5140d97521d642001994f';
|
||||
String _$tabDataRepositoryHash() => r'3d9149efb88f256cc826eb538f13331b49438a7c';
|
||||
|
||||
abstract class _$TabDataRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+4
-4
@@ -30,8 +30,8 @@ import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/core/uuid.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||
@@ -147,7 +147,7 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
return (selectedContainerTabs.value[equatable]
|
||||
?.contains(tabId) ==
|
||||
true)
|
||||
? TabPreview(
|
||||
? GridTabPreview(
|
||||
tabId: tabId,
|
||||
isActive: false,
|
||||
onDelete: () {
|
||||
@@ -160,7 +160,7 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
};
|
||||
},
|
||||
)
|
||||
: SuggestedSingleTabPreview(
|
||||
: SuggestedSingleGridTabPreview(
|
||||
tabId: tabId,
|
||||
activeTabId: null,
|
||||
onTap: () {
|
||||
|
||||
@@ -87,7 +87,7 @@ Future<WebPageInfo> pageInfo(
|
||||
if (tabState?.id != null) {
|
||||
final containerId = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabId(tabState!.id);
|
||||
.getTabContainerId(tabState!.id);
|
||||
|
||||
final containerData = await containerId.mapNotNull(
|
||||
(containerId) => ref
|
||||
|
||||
@@ -163,7 +163,7 @@ final class PageInfoProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$pageInfoHash() => r'87d23da9b25c3557e7270d90c2dee6f37eaaf8e0';
|
||||
String _$pageInfoHash() => r'b756ee75e155bcee1e54658556eb8f4ef417d118';
|
||||
|
||||
final class PageInfoFamily extends $Family
|
||||
with
|
||||
|
||||
Reference in New Issue
Block a user