tab trees

This commit is contained in:
Fabian Freund
2025-06-23 19:43:36 +02:00
parent 8445b768f0
commit 4f682744da
28 changed files with 2687 additions and 2256 deletions
+15
View File
@@ -28,6 +28,10 @@ part of 'routes.dart';
), ),
], ],
), ),
TypedGoRoute<TabTreeRoute>(
name: 'TabTreeRoute',
path: 'tab_tree/:rootTabId',
),
], ],
) )
class BrowserRoute extends GoRouteData with _$BrowserRoute { class BrowserRoute extends GoRouteData with _$BrowserRoute {
@@ -128,3 +132,14 @@ class ContextMenuRoute extends GoRouteData with _$ContextMenuRoute {
); );
} }
} }
class TabTreeRoute extends GoRouteData with _$TabTreeRoute {
final String rootTabId;
const TabTreeRoute(this.rootTabId);
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return DialogPage(builder: (_) => TabTreeDialog(rootTabId));
}
}
+1
View File
@@ -8,6 +8,7 @@ import 'package:weblibre/features/about/presentation/screens/about.dart';
import 'package:weblibre/features/bangs/presentation/screens/categories.dart'; import 'package:weblibre/features/bangs/presentation/screens/categories.dart';
import 'package:weblibre/features/bangs/presentation/screens/list.dart'; import 'package:weblibre/features/bangs/presentation/screens/list.dart';
import 'package:weblibre/features/bangs/presentation/screens/search.dart'; import 'package:weblibre/features/bangs/presentation/screens/search.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/tab_tree.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/screens/browser.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/screens/browser.dart';
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart'; import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
+31
View File
@@ -300,6 +300,12 @@ RouteBase get $browserRoute => GoRouteData.$route(
), ),
], ],
), ),
GoRouteData.$route(
path: 'tab_tree/:rootTabId',
name: 'TabTreeRoute',
factory: _$TabTreeRoute._fromState,
),
], ],
); );
@@ -500,6 +506,31 @@ mixin _$ContainerEditRoute on GoRouteData {
context.replace(location, extra: _self.$extra); context.replace(location, extra: _self.$extra);
} }
mixin _$TabTreeRoute on GoRouteData {
static TabTreeRoute _fromState(GoRouterState state) =>
TabTreeRoute(state.pathParameters['rootTabId']!);
TabTreeRoute get _self => this as TabTreeRoute;
@override
String get location => GoRouteData.$location(
'/tab_tree/${Uri.encodeComponent(_self.rootTabId)}',
);
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
extension<T extends Enum> on Map<T, String> { extension<T extends Enum> on Map<T, String> {
T? _$fromName(String? value) => T? _$fromName(String? value) =>
entries.where((element) => element.value == value).firstOrNull?.key; entries.where((element) => element.value == value).firstOrNull?.key;
@@ -16,6 +16,8 @@ class TabState extends WebPageInfo {
@CopyWithField(immutable: true) @CopyWithField(immutable: true)
final String id; final String id;
final String? parentId;
final String? contextId; final String? contextId;
@override @override
@@ -47,6 +49,7 @@ class TabState extends WebPageInfo {
TabState({ TabState({
required this.id, required this.id,
required this.parentId,
required this.contextId, required this.contextId,
required super.url, required super.url,
required String title, required String title,
@@ -64,6 +67,7 @@ class TabState extends WebPageInfo {
factory TabState.$default(String tabId) => TabState( factory TabState.$default(String tabId) => TabState(
id: tabId, id: tabId,
parentId: null,
contextId: null, contextId: null,
url: Uri.parse('about:blank'), url: Uri.parse('about:blank'),
title: "", title: "",
@@ -83,6 +87,7 @@ class TabState extends WebPageInfo {
List<Object?> get hashParameters => [ List<Object?> get hashParameters => [
...super.hashParameters, ...super.hashParameters,
id, id,
parentId,
contextId, contextId,
icon, icon,
thumbnail, thumbnail,
@@ -7,6 +7,8 @@ part of 'tab.dart';
// ************************************************************************** // **************************************************************************
abstract class _$TabStateCWProxy { abstract class _$TabStateCWProxy {
TabState parentId(String? parentId);
TabState contextId(String? contextId); TabState contextId(String? contextId);
TabState url(Uri url); TabState url(Uri url);
@@ -40,6 +42,7 @@ abstract class _$TabStateCWProxy {
/// TabState(...).copyWith(id: 12, name: "My name") /// TabState(...).copyWith(id: 12, name: "My name")
/// ```` /// ````
TabState call({ TabState call({
String? parentId,
String? contextId, String? contextId,
Uri url, Uri url,
String title, String title,
@@ -62,6 +65,9 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
final TabState _value; final TabState _value;
@override
TabState parentId(String? parentId) => this(parentId: parentId);
@override @override
TabState contextId(String? contextId) => this(contextId: contextId); TabState contextId(String? contextId) => this(contextId: contextId);
@@ -113,6 +119,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
/// TabState(...).copyWith(id: 12, name: "My name") /// TabState(...).copyWith(id: 12, name: "My name")
/// ```` /// ````
TabState call({ TabState call({
Object? parentId = const $CopyWithPlaceholder(),
Object? contextId = const $CopyWithPlaceholder(), Object? contextId = const $CopyWithPlaceholder(),
Object? url = const $CopyWithPlaceholder(), Object? url = const $CopyWithPlaceholder(),
Object? title = const $CopyWithPlaceholder(), Object? title = const $CopyWithPlaceholder(),
@@ -129,6 +136,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
}) { }) {
return TabState( return TabState(
id: _value.id, id: _value.id,
parentId: parentId == const $CopyWithPlaceholder()
? _value.parentId
// ignore: cast_nullable_to_non_nullable
: parentId as String?,
contextId: contextId == const $CopyWithPlaceholder() contextId: contextId == const $CopyWithPlaceholder()
? _value.contextId ? _value.contextId
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
@@ -24,6 +24,7 @@ class TabStates extends _$TabStates {
state[contentState.id] ?? TabState.$default(contentState.id); state[contentState.id] ?? TabState.$default(contentState.id);
state = {...state} state = {...state}
..[contentState.id] = current.copyWith( ..[contentState.id] = current.copyWith(
parentId: contentState.parentId,
contextId: contentState.contextId, contextId: contentState.contextId,
url: Uri.parse(contentState.url), url: Uri.parse(contentState.url),
title: (contentState.title.isNotEmpty) title: (contentState.title.isNotEmpty)
@@ -53,23 +53,27 @@ class TabRepository extends _$TabRepository {
.read(selectedContainerProvider.notifier) .read(selectedContainerProvider.notifier)
.fetchData(); .fetchData();
return ref.read(tabDatabaseProvider).tabDao.upsertContainerTabTransactional( return ref
() { .read(tabDatabaseProvider)
return _tabsService.addTab( .tabDao
url: url, .upsertContainerTabTransactional(
selectTab: selectTab, () {
startLoading: startLoading, return _tabsService.addTab(
parentId: parentId, url: url,
flags: flags, selectTab: selectTab,
contextId: selectedContainer?.metadata.contextualIdentity, startLoading: startLoading,
source: source, parentId: parentId,
private: private, flags: flags,
historyMetadata: historyMetadata, contextId: selectedContainer?.metadata.contextualIdentity,
additionalHeaders: additionalHeaders, source: source,
private: private,
historyMetadata: historyMetadata,
additionalHeaders: additionalHeaders,
);
},
parentId: Value(parentId),
containerId: Value(selectedContainer?.id),
); );
},
containerId: Value(selectedContainer?.id),
);
} }
Future<String> duplicateTab({ Future<String> duplicateTab({
@@ -83,16 +87,20 @@ class TabRepository extends _$TabRepository {
.getContainerData(containerId), .getContainerData(containerId),
); );
return ref.read(tabDatabaseProvider).tabDao.upsertContainerTabTransactional( return ref
() { .read(tabDatabaseProvider)
return _tabsService.duplicateTab( .tabDao
selectTabId: selectTabId, .upsertContainerTabTransactional(
newContextId: containerData?.metadata.contextualIdentity, () {
selectNewTab: selectTab, return _tabsService.duplicateTab(
selectTabId: selectTabId,
newContextId: containerData?.metadata.contextualIdentity,
selectNewTab: selectTab,
);
},
parentId: const Value.absent(),
containerId: Value(containerData?.id),
); );
},
containerId: Value(containerData?.id),
);
} }
Future<bool> selectPreviousTab() async { Future<bool> selectPreviousTab() async {
@@ -227,6 +235,7 @@ class TabRepository extends _$TabRepository {
final containerId = ref.read(selectedContainerProvider); final containerId = ref.read(selectedContainerProvider);
await db.tabDao.upsertUnassignedTab( await db.tabDao.upsertUnassignedTab(
tabId, tabId,
parentId: const Value.absent(),
containerId: Value(containerId), containerId: Value(containerId),
); );
}); });
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabRepositoryHash() => r'9b7ef03bec41f149b3cb456b73d7ee0e1b2804df'; String _$tabRepositoryHash() => r'db220096fef5d125a698063e9a88272db92e75c5';
/// See also [TabRepository]. /// See also [TabRepository].
@ProviderFor(TabRepository) @ProviderFor(TabRepository)
@@ -8,6 +8,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/search/domain/entities/tab_preview.dart'; import 'package:weblibre/features/geckoview/features/search/domain/entities/tab_preview.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.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.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
@@ -87,23 +88,62 @@ EquatableValue<Map<String, TabState>> availableTabStates(
} }
@Riverpod() @Riverpod()
EquatableValue<List<String>> seamlessFilteredTabIds( EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
Ref ref, Ref ref, {
TabSearchPartition searchPartition, required TabSearchPartition searchPartition,
ContainerFilter containerFilter, required ContainerFilter containerFilter,
) { required bool groupTrees,
}) {
final tabSearchResults = ref final tabSearchResults = ref
.watch( .watch(
tabSearchRepositoryProvider(searchPartition).select( tabSearchRepositoryProvider(searchPartition).select(
(value) => (value) => EquatableValue(
EquatableValue(value.valueOrNull?.map((tab) => tab.id).toList()), value.valueOrNull
?.map((tab) => SingleTabEntity(tabId: tab.id))
.toList(),
),
), ),
) )
.value; .value;
final availableTabs = ref.watch(availableTabIdsProvider(containerFilter)); final availableTabs = ref.watch(
availableTabIdsProvider(containerFilter).select(
(value) => EquatableValue(
value.value.map((tab) => SingleTabEntity(tabId: tab)).toList(),
),
),
);
if (tabSearchResults == null) { if (tabSearchResults == null) {
if (groupTrees) {
final trees = ref.watch(
tabTreesProvider.select(
(value) => EquatableValue(
value.valueOrNull
?.map(
(tree) => TabTreeEntity(
tabId: tree.latestTabId,
rootId: tree.rootTabId,
totalTabs: tree.totalTabs,
),
)
.toList() ??
[],
),
),
);
return EquatableValue(
trees.value
.where(
(tree) => availableTabs.value.any(
(available) => available.tabId == tree.tabId,
),
)
.toList(),
);
}
return availableTabs; return availableTabs;
} }
@@ -282,32 +282,41 @@ class _AvailableTabStatesProviderElement
(origin as AvailableTabStatesProvider).containerFilter; (origin as AvailableTabStatesProvider).containerFilter;
} }
String _$seamlessFilteredTabIdsHash() => String _$seamlessFilteredTabEntitiesHash() =>
r'171e0b942735066e25894dc2c1d99c367bf0d01a'; r'0ef5c14f3fa018e3417e097759817bc871be3e0d';
/// See also [seamlessFilteredTabIds]. /// See also [seamlessFilteredTabEntities].
@ProviderFor(seamlessFilteredTabIds) @ProviderFor(seamlessFilteredTabEntities)
const seamlessFilteredTabIdsProvider = SeamlessFilteredTabIdsFamily(); const seamlessFilteredTabEntitiesProvider = SeamlessFilteredTabEntitiesFamily();
/// See also [seamlessFilteredTabIds]. /// See also [seamlessFilteredTabEntities].
class SeamlessFilteredTabIdsFamily class SeamlessFilteredTabEntitiesFamily
extends Family<EquatableValue<List<String>>> { extends Family<EquatableValue<List<TabEntity>>> {
/// See also [seamlessFilteredTabIds]. /// See also [seamlessFilteredTabEntities].
const SeamlessFilteredTabIdsFamily(); const SeamlessFilteredTabEntitiesFamily();
/// See also [seamlessFilteredTabIds]. /// See also [seamlessFilteredTabEntities].
SeamlessFilteredTabIdsProvider call( SeamlessFilteredTabEntitiesProvider call({
TabSearchPartition searchPartition, required TabSearchPartition searchPartition,
ContainerFilter containerFilter, required ContainerFilter containerFilter,
) { required bool groupTrees,
return SeamlessFilteredTabIdsProvider(searchPartition, containerFilter); }) {
return SeamlessFilteredTabEntitiesProvider(
searchPartition: searchPartition,
containerFilter: containerFilter,
groupTrees: groupTrees,
);
} }
@override @override
SeamlessFilteredTabIdsProvider getProviderOverride( SeamlessFilteredTabEntitiesProvider getProviderOverride(
covariant SeamlessFilteredTabIdsProvider provider, covariant SeamlessFilteredTabEntitiesProvider provider,
) { ) {
return call(provider.searchPartition, provider.containerFilter); return call(
searchPartition: provider.searchPartition,
containerFilter: provider.containerFilter,
groupTrees: provider.groupTrees,
);
} }
static const Iterable<ProviderOrFamily>? _dependencies = null; static const Iterable<ProviderOrFamily>? _dependencies = null;
@@ -322,35 +331,38 @@ class SeamlessFilteredTabIdsFamily
_allTransitiveDependencies; _allTransitiveDependencies;
@override @override
String? get name => r'seamlessFilteredTabIdsProvider'; String? get name => r'seamlessFilteredTabEntitiesProvider';
} }
/// See also [seamlessFilteredTabIds]. /// See also [seamlessFilteredTabEntities].
class SeamlessFilteredTabIdsProvider class SeamlessFilteredTabEntitiesProvider
extends AutoDisposeProvider<EquatableValue<List<String>>> { extends AutoDisposeProvider<EquatableValue<List<TabEntity>>> {
/// See also [seamlessFilteredTabIds]. /// See also [seamlessFilteredTabEntities].
SeamlessFilteredTabIdsProvider( SeamlessFilteredTabEntitiesProvider({
TabSearchPartition searchPartition, required TabSearchPartition searchPartition,
ContainerFilter containerFilter, required ContainerFilter containerFilter,
) : this._internal( required bool groupTrees,
(ref) => seamlessFilteredTabIds( }) : this._internal(
ref as SeamlessFilteredTabIdsRef, (ref) => seamlessFilteredTabEntities(
searchPartition, ref as SeamlessFilteredTabEntitiesRef,
containerFilter, searchPartition: searchPartition,
), containerFilter: containerFilter,
from: seamlessFilteredTabIdsProvider, groupTrees: groupTrees,
name: r'seamlessFilteredTabIdsProvider', ),
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') from: seamlessFilteredTabEntitiesProvider,
? null name: r'seamlessFilteredTabEntitiesProvider',
: _$seamlessFilteredTabIdsHash, debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
dependencies: SeamlessFilteredTabIdsFamily._dependencies, ? null
allTransitiveDependencies: : _$seamlessFilteredTabEntitiesHash,
SeamlessFilteredTabIdsFamily._allTransitiveDependencies, dependencies: SeamlessFilteredTabEntitiesFamily._dependencies,
searchPartition: searchPartition, allTransitiveDependencies:
containerFilter: containerFilter, SeamlessFilteredTabEntitiesFamily._allTransitiveDependencies,
); searchPartition: searchPartition,
containerFilter: containerFilter,
groupTrees: groupTrees,
);
SeamlessFilteredTabIdsProvider._internal( SeamlessFilteredTabEntitiesProvider._internal(
super._createNotifier, { super._createNotifier, {
required super.name, required super.name,
required super.dependencies, required super.dependencies,
@@ -359,20 +371,24 @@ class SeamlessFilteredTabIdsProvider
required super.from, required super.from,
required this.searchPartition, required this.searchPartition,
required this.containerFilter, required this.containerFilter,
required this.groupTrees,
}) : super.internal(); }) : super.internal();
final TabSearchPartition searchPartition; final TabSearchPartition searchPartition;
final ContainerFilter containerFilter; final ContainerFilter containerFilter;
final bool groupTrees;
@override @override
Override overrideWith( Override overrideWith(
EquatableValue<List<String>> Function(SeamlessFilteredTabIdsRef provider) EquatableValue<List<TabEntity>> Function(
SeamlessFilteredTabEntitiesRef provider,
)
create, create,
) { ) {
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SeamlessFilteredTabIdsProvider._internal( override: SeamlessFilteredTabEntitiesProvider._internal(
(ref) => create(ref as SeamlessFilteredTabIdsRef), (ref) => create(ref as SeamlessFilteredTabEntitiesRef),
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
@@ -380,20 +396,22 @@ class SeamlessFilteredTabIdsProvider
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
searchPartition: searchPartition, searchPartition: searchPartition,
containerFilter: containerFilter, containerFilter: containerFilter,
groupTrees: groupTrees,
), ),
); );
} }
@override @override
AutoDisposeProviderElement<EquatableValue<List<String>>> createElement() { AutoDisposeProviderElement<EquatableValue<List<TabEntity>>> createElement() {
return _SeamlessFilteredTabIdsProviderElement(this); return _SeamlessFilteredTabEntitiesProviderElement(this);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SeamlessFilteredTabIdsProvider && return other is SeamlessFilteredTabEntitiesProvider &&
other.searchPartition == searchPartition && other.searchPartition == searchPartition &&
other.containerFilter == containerFilter; other.containerFilter == containerFilter &&
other.groupTrees == groupTrees;
} }
@override @override
@@ -401,6 +419,7 @@ class SeamlessFilteredTabIdsProvider
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, searchPartition.hashCode); hash = _SystemHash.combine(hash, searchPartition.hashCode);
hash = _SystemHash.combine(hash, containerFilter.hashCode); hash = _SystemHash.combine(hash, containerFilter.hashCode);
hash = _SystemHash.combine(hash, groupTrees.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@@ -408,26 +427,32 @@ class SeamlessFilteredTabIdsProvider
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
mixin SeamlessFilteredTabIdsRef mixin SeamlessFilteredTabEntitiesRef
on AutoDisposeProviderRef<EquatableValue<List<String>>> { on AutoDisposeProviderRef<EquatableValue<List<TabEntity>>> {
/// The parameter `searchPartition` of this provider. /// The parameter `searchPartition` of this provider.
TabSearchPartition get searchPartition; TabSearchPartition get searchPartition;
/// The parameter `containerFilter` of this provider. /// The parameter `containerFilter` of this provider.
ContainerFilter get containerFilter; ContainerFilter get containerFilter;
/// The parameter `groupTrees` of this provider.
bool get groupTrees;
} }
class _SeamlessFilteredTabIdsProviderElement class _SeamlessFilteredTabEntitiesProviderElement
extends AutoDisposeProviderElement<EquatableValue<List<String>>> extends AutoDisposeProviderElement<EquatableValue<List<TabEntity>>>
with SeamlessFilteredTabIdsRef { with SeamlessFilteredTabEntitiesRef {
_SeamlessFilteredTabIdsProviderElement(super.provider); _SeamlessFilteredTabEntitiesProviderElement(super.provider);
@override @override
TabSearchPartition get searchPartition => TabSearchPartition get searchPartition =>
(origin as SeamlessFilteredTabIdsProvider).searchPartition; (origin as SeamlessFilteredTabEntitiesProvider).searchPartition;
@override @override
ContainerFilter get containerFilter => ContainerFilter get containerFilter =>
(origin as SeamlessFilteredTabIdsProvider).containerFilter; (origin as SeamlessFilteredTabEntitiesProvider).containerFilter;
@override
bool get groupTrees =>
(origin as SeamlessFilteredTabEntitiesProvider).groupTrees;
} }
String _$seamlessFilteredTabPreviewsHash() => String _$seamlessFilteredTabPreviewsHash() =>
@@ -0,0 +1,15 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tree_view.g.dart';
@Riverpod(keepAlive: true)
class TreeViewController extends _$TreeViewController {
void toggle() {
state = !state;
}
@override
bool build() {
return false;
}
}
@@ -0,0 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tree_view.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$treeViewControllerHash() =>
r'53da9cc3cc89eab2fee4a3aaf4feccc5ecbfeb20';
/// See also [TreeViewController].
@ProviderFor(TreeViewController)
final treeViewControllerProvider =
NotifierProvider<TreeViewController, bool>.internal(
TreeViewController.new,
name: r'treeViewControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$treeViewControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TreeViewController = Notifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -0,0 +1,129 @@
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:go_router/go_router.dart';
import 'package:graphview/GraphView.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
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/tabs/domain/providers.dart';
class TabTreeDialog extends HookConsumerWidget {
final String tabId;
final Size childSize;
const TabTreeDialog(
this.tabId, {
super.key,
this.childSize = const Size(200, 300),
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final transforamtionController = useTransformationController();
final builder = useMemoized(() {
final config = BuchheimWalkerConfiguration()
..siblingSeparation = 100
..levelSeparation = 150
..subtreeSeparation = 150
..orientation = BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM;
return BuchheimWalkerAlgorithm(config, TreeEdgeRenderer(config));
});
final selectedTabId = ref.watch(selectedTabProvider);
final tabs = ref.watch(tabDescendantsProvider(tabId));
final graph = useMemoized(() {
final graph = Graph();
graph.isTree = true;
if (tabs.hasValue) {
for (final MapEntry(:key, :value) in tabs.valueOrNull!.entries) {
if (value != null) {
final current = Node.Id(key);
final parent = Node.Id(value);
graph.addEdge(parent, current);
}
}
}
return graph;
}, [EquatableValue(tabs.valueOrNull)]);
return Dialog.fullscreen(
child: Scaffold(
appBar: AppBar(
backgroundColor: const Color(0x44000000),
elevation: 0,
leading: IconButton(
onPressed: () {
context.pop();
},
icon: const Icon(Icons.close),
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(MdiIcons.target),
onPressed: () {
final node = graph.getNodeUsingId(selectedTabId);
final scale = transforamtionController.value.getMaxScaleOnAxis();
final newPosition =
(node.position -
Offset(childSize.width / 2, childSize.height / 2)) *
scale;
transforamtionController.value =
transforamtionController.value.clone()..setTranslation(
Vector3(-newPosition.dx, -newPosition.dy, 0),
);
},
),
extendBodyBehindAppBar: true,
body: InteractiveViewer(
transformationController: transforamtionController,
constrained: false,
boundaryMargin: const EdgeInsets.all(250),
minScale: 0.1,
maxScale: 5.0,
child: Skeletonizer(
enabled: !tabs.hasValue || tabs.valueOrNull?.isEmpty == true,
child: Skeleton.replace(
replacement: const Bone.square(),
child: GraphView(
graph: graph,
algorithm: builder,
paint: Paint()
..color = Theme.of(context).colorScheme.outline
..strokeWidth = 1
..style = PaintingStyle.stroke,
builder: (Node node) {
final id = node.key!.value as String;
return SizedBox.fromSize(
size: childSize,
child: SingleTabPreview(
tabId: id,
activeTabId: selectedTabId,
onClose: () {
context.pop();
ref
.read(bottomSheetControllerProvider.notifier)
.dismiss();
},
),
);
},
),
),
),
),
),
);
}
}
@@ -17,6 +17,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.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/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
@@ -385,6 +386,8 @@ class _ViewTabsSheet extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final draggableScrollableController = useDraggableScrollableController(); final draggableScrollableController = useDraggableScrollableController();
final treeModeEnabled = ref.watch(treeViewControllerProvider);
return DraggableScrollableSheet( return DraggableScrollableSheet(
controller: draggableScrollableController, controller: draggableScrollableController,
expand: false, expand: false,
@@ -396,13 +399,20 @@ class _ViewTabsSheet extends HookConsumerWidget {
topLeft: Radius.circular(28), topLeft: Radius.circular(28),
topRight: Radius.circular(28), topRight: Radius.circular(28),
), ),
child: ViewTabsSheetWidget( child: treeModeEnabled
sheetScrollController: scrollController, ? ViewTabTreesSheetWidget(
draggableScrollableController: draggableScrollableController, sheetScrollController: scrollController,
onClose: () { onClose: () {
ref.read(bottomSheetControllerProvider.notifier).dismiss(); ref.read(bottomSheetControllerProvider.notifier).dismiss();
}, },
), )
: ViewTabsSheetWidget(
sheetScrollController: scrollController,
draggableScrollableController: draggableScrollableController,
onClose: () {
ref.read(bottomSheetControllerProvider.notifier).dismiss();
},
),
); );
}, },
); );
@@ -13,9 +13,11 @@ 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/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.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/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.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/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/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/providers/selected_container.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/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.dart';
@@ -28,9 +30,10 @@ import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
class _TabSheetHeader extends HookConsumerWidget { class _TabSheetHeader extends HookConsumerWidget {
static const headerSize = 124.0; static const headerSize = 124.0;
final bool treeViewEnabled;
final VoidCallback onClose; final VoidCallback onClose;
const _TabSheetHeader({required this.onClose}); const _TabSheetHeader({required this.onClose, required this.treeViewEnabled});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -63,13 +66,31 @@ class _TabSheetHeader extends HookConsumerWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
TextButton.icon( Row(
icon: const Icon(MdiIcons.tabSearch), mainAxisSize: MainAxisSize.min,
label: const Text('Search'), children: [
onPressed: () { IconButton(
searchMode.value = true; icon: const Icon(MdiIcons.tabSearch),
searchTextFocus.requestFocus(); iconSize: 18,
}, padding: EdgeInsets.zero,
onPressed: () {
searchMode.value = true;
searchTextFocus.requestFocus();
},
),
IconButton(
icon: const Icon(MdiIcons.graph),
selectedIcon: const Icon(MdiIcons.table),
isSelected: treeViewEnabled,
iconSize: 18,
padding: EdgeInsets.zero,
onPressed: () {
ref
.read(treeViewControllerProvider.notifier)
.toggle();
},
),
],
), ),
TextButton.icon( TextButton.icon(
onPressed: () async { onPressed: () async {
@@ -116,39 +137,40 @@ class _TabSheetHeader extends HookConsumerWidget {
), ),
), ),
), ),
Padding( if (!treeViewEnabled)
padding: const EdgeInsets.only(left: 8.0, top: 12), Padding(
child: Consumer( padding: const EdgeInsets.only(left: 8.0, top: 12),
builder: (context, ref, child) { child: Consumer(
final selectedContainer = ref.watch( builder: (context, ref, child) {
selectedContainerDataProvider.select( final selectedContainer = ref.watch(
(value) => value.valueOrNull, selectedContainerDataProvider.select(
), (value) => value.valueOrNull,
); ),
);
return ContainerChips( return ContainerChips(
selectedContainer: selectedContainer, selectedContainer: selectedContainer,
onSelected: (container) async { onSelected: (container) async {
final result = await ref final result = await ref
.read(selectedContainerProvider.notifier) .read(selectedContainerProvider.notifier)
.setContainerId(container.id); .setContainerId(container.id);
if (context.mounted && if (context.mounted &&
result == SetContainerResult.successHasProxy) { result == SetContainerResult.successHasProxy) {
await ref await ref
.read(startProxyControllerProvider.notifier) .read(startProxyControllerProvider.notifier)
.maybeStartProxy(context); .maybeStartProxy(context);
} }
}, },
onDeleted: (container) { onDeleted: (container) {
ref ref
.read(selectedContainerProvider.notifier) .read(selectedContainerProvider.notifier)
.clearContainer(); .clearContainer();
}, },
); );
}, },
),
), ),
),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
), ),
@@ -212,7 +234,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
children: [ children: [
DraggableScrollableHeader( DraggableScrollableHeader(
controller: draggableScrollableController, controller: draggableScrollableController,
child: _TabSheetHeader(onClose: onClose), child: _TabSheetHeader(onClose: onClose, treeViewEnabled: false),
), ),
Expanded( Expanded(
child: HookConsumer( child: HookConsumer(
@@ -220,11 +242,14 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
final container = ref.watch(selectedContainerProvider); final container = ref.watch(selectedContainerProvider);
final filteredTabIds = ref.watch( final filteredTabIds = ref.watch(
seamlessFilteredTabIdsProvider( seamlessFilteredTabEntitiesProvider(
TabSearchPartition.preview, searchPartition: TabSearchPartition.preview,
// ignore: document_ignores using fast equatable // ignore: document_ignores using fast equatable
// ignore: provider_parameters // ignore: provider_parameters
ContainerFilterById(containerId: container), containerFilter: ContainerFilterById(
containerId: container,
),
groupTrees: false,
), ),
); );
@@ -263,7 +288,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
useEffect(() { useEffect(() {
final index = filteredTabIds.value.indexWhere( final index = filteredTabIds.value.indexWhere(
(webView) => webView == activeTab, (entity) => entity.tabId == activeTab,
); );
if (index > -1) { if (index > -1) {
@@ -285,53 +310,58 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
final tabs = useMemoized(() { final tabs = useMemoized(() {
return filteredTabIds.value return filteredTabIds.value
.mapIndexed( .whereType<SingleTabEntity>()
(index, tabId) => CustomDraggable( .mapIndexed((index, entity) {
key: Key(tabId), final child = Consumer(
data: TabDragData(tabId), child: SingleTabPreview(
child: Consumer( tabId: entity.tabId,
child: TabPreviewDraggable( activeTabId: activeTab,
tabId: tabId, onClose: onClose,
activeTabId: activeTab,
onClose: onClose,
),
builder: (context, ref, child) {
final dragData = ref.watch(
willAcceptDropProvider.select((value) {
final dragTabId = switch (value) {
ContainerDropData() => value.tabId,
DeleteDropData() => value.tabId,
null => null,
};
return (dragTabId == tabId) ? value : null;
}),
);
return switch (dragData) {
ContainerDropData() => Opacity(
opacity: 0.3,
child: Transform.scale(
scale: 0.9,
child: child,
),
),
DeleteDropData() => Opacity(
opacity: 0.3,
child: ColorFiltered(
colorFilter: const ColorFilter.mode(
Colors.red,
BlendMode.modulate,
),
child: child,
),
),
null => child!,
};
},
), ),
), builder: (context, ref, child) {
) 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;
}),
);
return switch (dragData) {
ContainerDropData() => Opacity(
opacity: 0.3,
child: Transform.scale(
scale: 0.9,
child: child,
),
),
DeleteDropData() => Opacity(
opacity: 0.3,
child: ColorFiltered(
colorFilter: const ColorFilter.mode(
Colors.red,
BlendMode.modulate,
),
child: child,
),
),
null => child!,
};
},
);
return CustomDraggable(
key: Key(entity.tabId),
data: TabDragData(entity.tabId),
child: child,
);
})
.toList(); .toList();
}, [filteredTabIds, activeTab]); }, [filteredTabIds, activeTab]);
@@ -341,7 +371,6 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
//Rebuild when cross axis count changes //Rebuild when cross axis count changes
key: ValueKey(crossAxisCount), key: ValueKey(crossAxisCount),
scrollController: sheetScrollController, scrollController: sheetScrollController,
children: tabs,
onDragStarted: (index) { onDragStarted: (index) {
ref.read(willAcceptDropProvider.notifier).clear(); ref.read(willAcceptDropProvider.notifier).clear();
}, },
@@ -358,7 +387,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
containerRepositoryProvider.notifier, containerRepositoryProvider.notifier,
); );
final tabId = filteredTabIds.value[oldIndex]; final tabId = filteredTabIds.value[oldIndex].tabId;
final containerId = await ref final containerId = await ref
.read(tabDataRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
.containerTabId(tabId); .containerTabId(tabId);
@@ -376,7 +405,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
} else { } else {
final orderAfterIndex = newIndex; final orderAfterIndex = newIndex;
key = await containerRepository.getOrderKeyAfterTab( key = await containerRepository.getOrderKeyAfterTab(
filteredTabIds.value[orderAfterIndex], filteredTabIds.value[orderAfterIndex].tabId,
containerId, containerId,
); );
} }
@@ -399,6 +428,182 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
itemBuilder: (context, index) => children[index], itemBuilder: (context, index) => children[index],
); );
}, },
children: tabs,
),
);
},
),
),
],
),
Padding(
padding: const EdgeInsets.only(
top: _TabSheetHeader.headerSize + 4,
right: 4,
),
child: FloatingActionButton.small(
onPressed: () async {
final isCurrentPrivate =
ref.read(selectedTabStateProvider)?.isPrivate ?? false;
await SearchRoute(
tabType: isCurrentPrivate ? TabType.private : TabType.regular,
).push(context);
onClose();
},
child: const Icon(Icons.add),
),
),
],
);
}
}
class ViewTabTreesSheetWidget extends HookConsumerWidget {
final ScrollController sheetScrollController;
final VoidCallback onClose;
const ViewTabTreesSheetWidget({
required this.onClose,
required this.sheetScrollController,
super.key,
});
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 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);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return Stack(
alignment: Alignment.bottomRight,
children: [
Column(
children: [
_TabSheetHeader(onClose: onClose, treeViewEnabled: true),
Expanded(
child: HookConsumer(
builder: (context, ref, child) {
final filteredTabIds = 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: MediaQuery.of(context).size.width,
horizontalPadding: 4.0,
crossAxisSpacing: 8.0,
);
return math.max(
math.min(calculatedCount, filteredTabIds.value.length),
2,
);
},
[
MediaQuery.of(context).size.width,
filteredTabIds.value.length,
],
);
final itemSize = useMemoized(
() =>
_calculateItemSize(
screenWidth: MediaQuery.of(context).size.width,
childAspectRatio: 0.75,
horizontalPadding: 4.0,
crossAxisSpacing: 8.0,
crossAxisCount: crossAxisCount,
) +
//mainAxisSpacing
const Offset(0, 8.0),
[MediaQuery.of(context).size.width, crossAxisCount],
);
useEffect(() {
final index = filteredTabIds.value.indexWhere(
(entity) => entity.tabId == activeTab,
);
if (index > -1) {
final offset = (index ~/ 2) * itemSize.height;
if (offset != sheetScrollController.offset) {
unawaited(
sheetScrollController.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
}
return null;
}, [filteredTabIds, activeTab]);
final tabs = useMemoized(() {
return filteredTabIds.value.whereType<TabTreeEntity>().map((
entity,
) {
return TabTreePreview(
entity: entity,
activeTabId: activeTab,
onClose: onClose,
stackPadding: const Offset(8, 8),
);
}).toList();
}, [filteredTabIds, activeTab]);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: GridView.builder(
controller: sheetScrollController,
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],
), ),
); );
}, },
@@ -1,39 +1,30 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart'; import 'package:nullability/nullability.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.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/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.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.dart';
import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart';
class TabPreview extends HookWidget { class _TabBox extends StatelessWidget {
final TabState tab;
final bool isActive; final bool isActive;
final bool isPrivate;
final Widget? child;
final VoidCallback? onTap; const _TabBox({required this.isActive, required this.isPrivate, this.child});
final VoidCallback? onDoubleTap;
final VoidCallback? onDelete;
final void Function(String host)? onDeleteAll;
const TabPreview({
required this.tab,
required this.isActive,
this.onTap,
this.onDoubleTap,
this.onDelete,
this.onDeleteAll,
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final extendedDeleteMenuController = useMenuController();
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
@@ -43,31 +34,66 @@ class TabPreview extends HookWidget {
borderRadius: const BorderRadius.all(Radius.circular(16.0)), borderRadius: const BorderRadius.all(Radius.circular(16.0)),
), ),
child: Material( child: Material(
color: tab.isPrivate color: isPrivate
? const Color(0xFF25003E) ? const Color(0xFF25003E)
: colorScheme.surfaceContainerHighest, : colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.all(Radius.circular(14.0)), borderRadius: const BorderRadius.all(Radius.circular(14.0)),
child: InkWell( child: child,
borderRadius: const BorderRadius.all(Radius.circular(14.0)), ),
onTap: onTap, );
onDoubleTap: onDoubleTap, }
child: Column( }
children: [
Row( class TabPreview extends HookWidget {
children: [ final TabState tab;
Expanded( final bool isActive;
child: Padding(
padding: const EdgeInsets.only(left: 6.0, top: 2.0), final VoidCallback? onTap;
child: Text( final VoidCallback? onDoubleTap;
overflow: TextOverflow.ellipsis, final VoidCallback? onLongPress;
tab.title, final VoidCallback? onDelete;
maxLines: 2, final void Function(String host)? onDeleteAll;
style: tab.isPrivate
? const TextStyle(color: Colors.white) const TabPreview({
: null, required this.tab,
), required this.isActive,
this.onTap,
this.onDoubleTap,
this.onLongPress,
this.onDelete,
this.onDeleteAll,
super.key,
});
@override
Widget build(BuildContext context) {
final extendedDeleteMenuController = useMenuController();
return _TabBox(
isActive: isActive,
isPrivate: tab.isPrivate,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
onTap: onTap,
onDoubleTap: onDoubleTap,
onLongPress: onLongPress,
child: Column(
children: [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 6.0, top: 2.0),
child: Text(
overflow: TextOverflow.ellipsis,
tab.title,
maxLines: 2,
style: tab.isPrivate
? const TextStyle(color: Colors.white)
: null,
), ),
), ),
),
if (onDelete != null || onDeleteAll != null)
MenuAnchor( MenuAnchor(
controller: extendedDeleteMenuController, controller: extendedDeleteMenuController,
builder: (context, controller, child) { builder: (context, controller, child) {
@@ -100,77 +126,76 @@ class TabPreview extends HookWidget {
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
), ),
), ),
], ],
), ),
Row( Row(
children: [ children: [
const SizedBox(width: 6.0), const SizedBox(width: 6.0),
TabIcon(state: tab), TabIcon(state: tab),
const SizedBox(width: 6.0), const SizedBox(width: 6.0),
Expanded( Expanded(
child: Text( child: Text(
tab.url.authority, tab.url.authority,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: tab.isPrivate ? Colors.white : null, color: tab.isPrivate ? Colors.white : null,
),
), ),
), ),
if (tab.isPrivate) ...[ ),
const SizedBox(width: 6.0), if (tab.isPrivate) ...[
const SizedBox( const SizedBox(width: 6.0),
height: 16, const SizedBox(
width: 24, height: 16,
child: Stack( width: 24,
fit: StackFit.expand, child: Stack(
children: [ fit: StackFit.expand,
Positioned( children: [
top: -4, Positioned(
child: Icon( top: -4,
MdiIcons.dominoMask, child: Icon(
color: Color(0xFF8000D7), MdiIcons.dominoMask,
), color: Color(0xFF8000D7),
), ),
],
),
),
],
const SizedBox(width: 8.0),
],
),
const SizedBox(height: 6),
if (tab.thumbnail != null)
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(14.0),
bottomRight: Radius.circular(14.0),
),
child: SizedBox(
width: double.infinity,
child: RepaintBoundary(
child: RawImage(
image: tab.thumbnail!.value,
fit: BoxFit.fitWidth,
), ),
],
),
),
],
const SizedBox(width: 8.0),
],
),
const SizedBox(height: 6),
if (tab.thumbnail != null)
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(14.0),
bottomRight: Radius.circular(14.0),
),
child: SizedBox(
width: double.infinity,
child: RepaintBoundary(
child: RawImage(
image: tab.thumbnail!.value,
fit: BoxFit.fitWidth,
), ),
), ),
), ),
), ),
], ),
), ],
), ),
), ),
); );
} }
} }
class TabPreviewDraggable extends HookConsumerWidget { class SingleTabPreview extends HookConsumerWidget {
final String tabId; final String tabId;
final String? activeTabId; final String? activeTabId;
final void Function() onClose; final void Function() onClose;
TabPreviewDraggable({ SingleTabPreview({
required this.tabId, required this.tabId,
required this.activeTabId, required this.activeTabId,
required this.onClose, required this.onClose,
@@ -220,3 +245,110 @@ class TabPreviewDraggable extends HookConsumerWidget {
); );
} }
} }
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 tab = ref.watch(tabStateProvider(entity.tabId));
if (tab == 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,
_TabBox(
isActive: entity.tabId == activeTabId,
isPrivate: tab.isPrivate,
),
),
_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(
tab: tab,
isActive: entity.tabId == activeTabId,
onLongPress: () async {
if (entity.tabId != activeTabId) {
//Close first to avoid rebuilds
onClose();
await ref
.read(tabRepositoryProvider.notifier)
.selectTab(tab.id);
} else {
onClose();
}
},
onTap: () async {
if (entity.totalTabs > 1) {
await TabTreeRoute(entity.rootId).push(context);
}
},
// 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 {
// await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id);
// },
),
),
),
],
);
}
}
@@ -38,6 +38,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
Future<String> upsertContainerTabTransactional( Future<String> upsertContainerTabTransactional(
Future<String> Function() createTab, { Future<String> Function() createTab, {
required Value<String?> parentId,
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(), Value<String?> orderKey = const Value.absent(),
}) { }) {
@@ -53,12 +54,14 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
await db.tab.insertOne( await db.tab.insertOne(
TabCompanion.insert( TabCompanion.insert(
id: tabId, id: tabId,
parentId: parentId,
timestamp: DateTime.now(), timestamp: DateTime.now(),
containerId: containerId, containerId: containerId,
orderKey: currentOrderKey, orderKey: currentOrderKey,
), ),
onConflict: DoUpdate( onConflict: DoUpdate(
(old) => TabCompanion( (old) => TabCompanion(
parentId: parentId,
containerId: containerId, containerId: containerId,
orderKey: Value.absentIfNull(orderKey.value), orderKey: Value.absentIfNull(orderKey.value),
), ),
@@ -72,6 +75,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
//Upsert an tab only if there is no container assigned yet //Upsert an tab only if there is no container assigned yet
Future<String> upsertUnassignedTab( Future<String> upsertUnassignedTab(
String tabId, { String tabId, {
required Value<String?> parentId,
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(), Value<String?> orderKey = const Value.absent(),
}) { }) {
@@ -85,12 +89,14 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
await db.tab.insertOne( await db.tab.insertOne(
TabCompanion.insert( TabCompanion.insert(
id: tabId, id: tabId,
parentId: parentId,
timestamp: DateTime.now(), timestamp: DateTime.now(),
containerId: containerId, containerId: containerId,
orderKey: currentOrderKey, orderKey: currentOrderKey,
), ),
onConflict: DoUpdate( onConflict: DoUpdate(
(old) => TabCompanion( (old) => TabCompanion(
parentId: parentId,
containerId: containerId, containerId: containerId,
orderKey: Value.absentIfNull(orderKey.value), orderKey: Value.absentIfNull(orderKey.value),
), ),
@@ -152,6 +158,9 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
batch.update( batch.update(
db.tab, db.tab,
TabCompanion( TabCompanion(
parentId: (previousState?.parentId != state.parentId)
? Value(state.parentId)
: const Value.absent(),
url: (previousState?.url != state.url) url: (previousState?.url != state.url)
? Value(state.url) ? Value(state.url)
: const Value.absent(), : const Value.absent(),
@@ -11,8 +11,9 @@ CREATE TABLE container (
metadata TEXT MAPPED BY `const ContainerMetadataConverter()` metadata TEXT MAPPED BY `const ContainerMetadataConverter()`
) WITH ContainerData; ) WITH ContainerData;
CREATE TABLE tab ( CREATE TABLE tab(
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
parent_id TEXT REFERENCES tab (id) ON DELETE SET NULL,
container_id TEXT REFERENCES container (id) ON DELETE CASCADE, container_id TEXT REFERENCES container (id) ON DELETE CASCADE,
order_key TEXT NOT NULL, order_key TEXT NOT NULL,
url TEXT MAPPED BY `const UriConverter()`, url TEXT MAPPED BY `const UriConverter()`,
@@ -35,6 +36,14 @@ CREATE VIRTUAL TABLE tab_fts
tokenize="trigram" tokenize="trigram"
); );
-- Create trigger to handle parent deletion
CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN
-- Update all children of the deleted row to point to its parent
UPDATE tab
SET parent_id = OLD.parent_id
WHERE parent_id = OLD.id;
END;
-- Triggers to keep the FTS index up to date. -- Triggers to keep the FTS index up to date.
CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN
INSERT INTO INSERT INTO
@@ -159,4 +168,64 @@ queryTabsFullContent WITH TabQueryResult:
CROSS JOIN weights CROSS JOIN weights
ORDER BY ORDER BY
weighted_rank ASC, weighted_rank ASC,
t.timestamp DESC; t.timestamp DESC;
tabTrees:
WITH RECURSIVE descendants AS (
-- Base case: all root tabs (no parent)
SELECT
id,
parent_id,
timestamp,
id AS root_id
FROM tab
WHERE parent_id IS NULL
UNION ALL
-- Recursive case: find all descendants
SELECT
t.id,
t.parent_id,
t.timestamp,
d.root_id
FROM tab t
JOIN descendants d ON t.parent_id = d.id
),
root_stats AS (
-- Calculate stats for each root
SELECT
root_id,
MAX(timestamp) AS max_timestamp,
COUNT(*) AS total_children
FROM descendants
GROUP BY root_id
)
-- Get the actual tab records with latest timestamp per root
SELECT
d.root_id AS root_tab_id,
d.id AS latest_tab_id,
d.timestamp AS latest_timestamp,
rs.total_children AS total_tabs
FROM descendants d
JOIN root_stats rs
ON
d.root_id = rs.root_id AND
d.timestamp = rs.max_timestamp
ORDER BY d.timestamp DESC;
unorderedTabDescendants:
WITH RECURSIVE descendants AS (
SELECT id, parent_id
FROM tab
WHERE id = :tab_id
UNION ALL
-- Recursive case: find all descendants
SELECT t.id, t.parent_id
FROM tab t
JOIN descendants d ON t.parent_id = d.id
)
SELECT id, parent_id
FROM descendants;
@@ -196,6 +196,14 @@ class Tab extends Table with TableInfo<Tab, TabData> {
requiredDuringInsert: true, requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL', $customConstraints: 'PRIMARY KEY NOT NULL',
); );
late final GeneratedColumn<String> parentId = GeneratedColumn<String>(
'parent_id',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: 'REFERENCES tab(id)ON DELETE SET NULL',
);
late final GeneratedColumn<String> containerId = GeneratedColumn<String>( late final GeneratedColumn<String> containerId = GeneratedColumn<String>(
'container_id', 'container_id',
aliasedName, aliasedName,
@@ -283,6 +291,7 @@ class Tab extends Table with TableInfo<Tab, TabData> {
@override @override
List<GeneratedColumn> get $columns => [ List<GeneratedColumn> get $columns => [
id, id,
parentId,
containerId, containerId,
orderKey, orderKey,
url, url,
@@ -309,6 +318,10 @@ class Tab extends Table with TableInfo<Tab, TabData> {
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}id'], data['${effectivePrefix}id'],
)!, )!,
parentId: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}parent_id'],
),
containerId: attachedDatabase.typeMapping.read( containerId: attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}container_id'], data['${effectivePrefix}container_id'],
@@ -368,6 +381,7 @@ class Tab extends Table with TableInfo<Tab, TabData> {
class TabData extends DataClass implements Insertable<TabData> { class TabData extends DataClass implements Insertable<TabData> {
final String id; final String id;
final String? parentId;
final String? containerId; final String? containerId;
final String orderKey; final String orderKey;
final Uri? url; final Uri? url;
@@ -380,6 +394,7 @@ class TabData extends DataClass implements Insertable<TabData> {
final DateTime timestamp; final DateTime timestamp;
const TabData({ const TabData({
required this.id, required this.id,
this.parentId,
this.containerId, this.containerId,
required this.orderKey, required this.orderKey,
this.url, this.url,
@@ -395,6 +410,9 @@ class TabData extends DataClass implements Insertable<TabData> {
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['id'] = Variable<String>(id); map['id'] = Variable<String>(id);
if (!nullToAbsent || parentId != null) {
map['parent_id'] = Variable<String>(parentId);
}
if (!nullToAbsent || containerId != null) { if (!nullToAbsent || containerId != null) {
map['container_id'] = Variable<String>(containerId); map['container_id'] = Variable<String>(containerId);
} }
@@ -433,6 +451,7 @@ class TabData extends DataClass implements Insertable<TabData> {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return TabData( return TabData(
id: serializer.fromJson<String>(json['id']), id: serializer.fromJson<String>(json['id']),
parentId: serializer.fromJson<String?>(json['parent_id']),
containerId: serializer.fromJson<String?>(json['container_id']), containerId: serializer.fromJson<String?>(json['container_id']),
orderKey: serializer.fromJson<String>(json['order_key']), orderKey: serializer.fromJson<String>(json['order_key']),
url: serializer.fromJson<Uri?>(json['url']), url: serializer.fromJson<Uri?>(json['url']),
@@ -460,6 +479,7 @@ class TabData extends DataClass implements Insertable<TabData> {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'id': serializer.toJson<String>(id), 'id': serializer.toJson<String>(id),
'parent_id': serializer.toJson<String?>(parentId),
'container_id': serializer.toJson<String?>(containerId), 'container_id': serializer.toJson<String?>(containerId),
'order_key': serializer.toJson<String>(orderKey), 'order_key': serializer.toJson<String>(orderKey),
'url': serializer.toJson<Uri?>(url), 'url': serializer.toJson<Uri?>(url),
@@ -479,6 +499,7 @@ class TabData extends DataClass implements Insertable<TabData> {
TabData copyWith({ TabData copyWith({
String? id, String? id,
Value<String?> parentId = const Value.absent(),
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
String? orderKey, String? orderKey,
Value<Uri?> url = const Value.absent(), Value<Uri?> url = const Value.absent(),
@@ -491,6 +512,7 @@ class TabData extends DataClass implements Insertable<TabData> {
DateTime? timestamp, DateTime? timestamp,
}) => TabData( }) => TabData(
id: id ?? this.id, id: id ?? this.id,
parentId: parentId.present ? parentId.value : this.parentId,
containerId: containerId.present ? containerId.value : this.containerId, containerId: containerId.present ? containerId.value : this.containerId,
orderKey: orderKey ?? this.orderKey, orderKey: orderKey ?? this.orderKey,
url: url.present ? url.value : this.url, url: url.present ? url.value : this.url,
@@ -515,6 +537,7 @@ class TabData extends DataClass implements Insertable<TabData> {
TabData copyWithCompanion(TabCompanion data) { TabData copyWithCompanion(TabCompanion data) {
return TabData( return TabData(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
parentId: data.parentId.present ? data.parentId.value : this.parentId,
containerId: data.containerId.present containerId: data.containerId.present
? data.containerId.value ? data.containerId.value
: this.containerId, : this.containerId,
@@ -544,6 +567,7 @@ class TabData extends DataClass implements Insertable<TabData> {
String toString() { String toString() {
return (StringBuffer('TabData(') return (StringBuffer('TabData(')
..write('id: $id, ') ..write('id: $id, ')
..write('parentId: $parentId, ')
..write('containerId: $containerId, ') ..write('containerId: $containerId, ')
..write('orderKey: $orderKey, ') ..write('orderKey: $orderKey, ')
..write('url: $url, ') ..write('url: $url, ')
@@ -561,6 +585,7 @@ class TabData extends DataClass implements Insertable<TabData> {
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(
id, id,
parentId,
containerId, containerId,
orderKey, orderKey,
url, url,
@@ -577,6 +602,7 @@ class TabData extends DataClass implements Insertable<TabData> {
identical(this, other) || identical(this, other) ||
(other is TabData && (other is TabData &&
other.id == this.id && other.id == this.id &&
other.parentId == this.parentId &&
other.containerId == this.containerId && other.containerId == this.containerId &&
other.orderKey == this.orderKey && other.orderKey == this.orderKey &&
other.url == this.url && other.url == this.url &&
@@ -591,6 +617,7 @@ class TabData extends DataClass implements Insertable<TabData> {
class TabCompanion extends UpdateCompanion<TabData> { class TabCompanion extends UpdateCompanion<TabData> {
final Value<String> id; final Value<String> id;
final Value<String?> parentId;
final Value<String?> containerId; final Value<String?> containerId;
final Value<String> orderKey; final Value<String> orderKey;
final Value<Uri?> url; final Value<Uri?> url;
@@ -604,6 +631,7 @@ class TabCompanion extends UpdateCompanion<TabData> {
final Value<int> rowid; final Value<int> rowid;
const TabCompanion({ const TabCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.parentId = const Value.absent(),
this.containerId = const Value.absent(), this.containerId = const Value.absent(),
this.orderKey = const Value.absent(), this.orderKey = const Value.absent(),
this.url = const Value.absent(), this.url = const Value.absent(),
@@ -618,6 +646,7 @@ class TabCompanion extends UpdateCompanion<TabData> {
}); });
TabCompanion.insert({ TabCompanion.insert({
required String id, required String id,
this.parentId = const Value.absent(),
this.containerId = const Value.absent(), this.containerId = const Value.absent(),
required String orderKey, required String orderKey,
this.url = const Value.absent(), this.url = const Value.absent(),
@@ -634,6 +663,7 @@ class TabCompanion extends UpdateCompanion<TabData> {
timestamp = Value(timestamp); timestamp = Value(timestamp);
static Insertable<TabData> custom({ static Insertable<TabData> custom({
Expression<String>? id, Expression<String>? id,
Expression<String>? parentId,
Expression<String>? containerId, Expression<String>? containerId,
Expression<String>? orderKey, Expression<String>? orderKey,
Expression<String>? url, Expression<String>? url,
@@ -648,6 +678,7 @@ class TabCompanion extends UpdateCompanion<TabData> {
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
if (parentId != null) 'parent_id': parentId,
if (containerId != null) 'container_id': containerId, if (containerId != null) 'container_id': containerId,
if (orderKey != null) 'order_key': orderKey, if (orderKey != null) 'order_key': orderKey,
if (url != null) 'url': url, if (url != null) 'url': url,
@@ -668,6 +699,7 @@ class TabCompanion extends UpdateCompanion<TabData> {
TabCompanion copyWith({ TabCompanion copyWith({
Value<String>? id, Value<String>? id,
Value<String?>? parentId,
Value<String?>? containerId, Value<String?>? containerId,
Value<String>? orderKey, Value<String>? orderKey,
Value<Uri?>? url, Value<Uri?>? url,
@@ -682,6 +714,7 @@ class TabCompanion extends UpdateCompanion<TabData> {
}) { }) {
return TabCompanion( return TabCompanion(
id: id ?? this.id, id: id ?? this.id,
parentId: parentId ?? this.parentId,
containerId: containerId ?? this.containerId, containerId: containerId ?? this.containerId,
orderKey: orderKey ?? this.orderKey, orderKey: orderKey ?? this.orderKey,
url: url ?? this.url, url: url ?? this.url,
@@ -704,6 +737,9 @@ class TabCompanion extends UpdateCompanion<TabData> {
if (id.present) { if (id.present) {
map['id'] = Variable<String>(id.value); map['id'] = Variable<String>(id.value);
} }
if (parentId.present) {
map['parent_id'] = Variable<String>(parentId.value);
}
if (containerId.present) { if (containerId.present) {
map['container_id'] = Variable<String>(containerId.value); map['container_id'] = Variable<String>(containerId.value);
} }
@@ -752,6 +788,7 @@ class TabCompanion extends UpdateCompanion<TabData> {
String toString() { String toString() {
return (StringBuffer('TabCompanion(') return (StringBuffer('TabCompanion(')
..write('id: $id, ') ..write('id: $id, ')
..write('parentId: $parentId, ')
..write('containerId: $containerId, ') ..write('containerId: $containerId, ')
..write('orderKey: $orderKey, ') ..write('orderKey: $orderKey, ')
..write('url: $url, ') ..write('url: $url, ')
@@ -1051,6 +1088,10 @@ abstract class _$TabDatabase extends GeneratedDatabase {
late final Container container = Container(this); late final Container container = Container(this);
late final Tab tab = Tab(this); late final Tab tab = Tab(this);
late final TabFts tabFts = TabFts(this); late final TabFts tabFts = TabFts(this);
late final Trigger tabMaintainParentChainOnDelete = Trigger(
'CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN UPDATE tab SET parent_id = OLD.parent_id WHERE parent_id = OLD.id;END',
'tab_maintain_parent_chain_on_delete',
);
late final Trigger tabAfterInsert = Trigger( late final Trigger tabAfterInsert = Trigger(
'CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END', 'CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END',
'tab_after_insert', 'tab_after_insert',
@@ -1177,6 +1218,36 @@ abstract class _$TabDatabase extends GeneratedDatabase {
); );
} }
Selectable<TabTreesResult> tabTrees() {
return customSelect(
'WITH RECURSIVE descendants AS (SELECT id, parent_id, timestamp, id AS root_id FROM tab WHERE parent_id IS NULL UNION ALL SELECT t.id, t.parent_id, t.timestamp, d.root_id FROM tab AS t JOIN descendants AS d ON t.parent_id = d.id), root_stats AS (SELECT root_id, MAX(timestamp) AS max_timestamp, COUNT(*) AS total_children FROM descendants GROUP BY root_id) SELECT d.root_id AS root_tab_id, d.id AS latest_tab_id, d.timestamp AS latest_timestamp, rs.total_children AS total_tabs FROM descendants AS d JOIN root_stats AS rs ON d.root_id = rs.root_id AND d.timestamp = rs.max_timestamp ORDER BY d.timestamp DESC',
variables: [],
readsFrom: {tab},
).map(
(QueryRow row) => TabTreesResult(
rootTabId: row.read<String>('root_tab_id'),
latestTabId: row.read<String>('latest_tab_id'),
latestTimestamp: row.read<DateTime>('latest_timestamp'),
totalTabs: row.read<int>('total_tabs'),
),
);
}
Selectable<UnorderedTabDescendantsResult> unorderedTabDescendants({
required String tabId,
}) {
return customSelect(
'WITH RECURSIVE descendants AS (SELECT id, parent_id FROM tab WHERE id = ?1 UNION ALL SELECT t.id, t.parent_id FROM tab AS t JOIN descendants AS d ON t.parent_id = d.id) SELECT id, parent_id FROM descendants',
variables: [Variable<String>(tabId)],
readsFrom: {tab},
).map(
(QueryRow row) => UnorderedTabDescendantsResult(
id: row.read<String>('id'),
parentId: row.readNullable<String>('parent_id'),
),
);
}
@override @override
Iterable<TableInfo<Table, Object?>> get allTables => Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>(); allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@@ -1185,6 +1256,7 @@ abstract class _$TabDatabase extends GeneratedDatabase {
container, container,
tab, tab,
tabFts, tabFts,
tabMaintainParentChainOnDelete,
tabAfterInsert, tabAfterInsert,
tabAfterDelete, tabAfterDelete,
tabAfterUpdate, tabAfterUpdate,
@@ -1198,6 +1270,13 @@ abstract class _$TabDatabase extends GeneratedDatabase {
), ),
result: [TableUpdate('tab', kind: UpdateKind.delete)], result: [TableUpdate('tab', kind: UpdateKind.delete)],
), ),
WritePropagation(
on: TableUpdateQuery.onTableName(
'tab',
limitUpdateKind: UpdateKind.delete,
),
result: [TableUpdate('tab', kind: UpdateKind.update)],
),
WritePropagation( WritePropagation(
on: TableUpdateQuery.onTableName( on: TableUpdateQuery.onTableName(
'tab', 'tab',
@@ -1499,6 +1578,7 @@ typedef $ContainerProcessedTableManager =
typedef $TabCreateCompanionBuilder = typedef $TabCreateCompanionBuilder =
TabCompanion Function({ TabCompanion Function({
required String id, required String id,
Value<String?> parentId,
Value<String?> containerId, Value<String?> containerId,
required String orderKey, required String orderKey,
Value<Uri?> url, Value<Uri?> url,
@@ -1514,6 +1594,7 @@ typedef $TabCreateCompanionBuilder =
typedef $TabUpdateCompanionBuilder = typedef $TabUpdateCompanionBuilder =
TabCompanion Function({ TabCompanion Function({
Value<String> id, Value<String> id,
Value<String?> parentId,
Value<String?> containerId, Value<String?> containerId,
Value<String> orderKey, Value<String> orderKey,
Value<Uri?> url, Value<Uri?> url,
@@ -1561,6 +1642,11 @@ class $TabFilterComposer extends Composer<_$TabDatabase, Tab> {
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get parentId => $composableBuilder(
column: $table.parentId,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get orderKey => $composableBuilder( ColumnFilters<String> get orderKey => $composableBuilder(
column: $table.orderKey, column: $table.orderKey,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@@ -1644,6 +1730,11 @@ class $TabOrderingComposer extends Composer<_$TabDatabase, Tab> {
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get parentId => $composableBuilder(
column: $table.parentId,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get orderKey => $composableBuilder( ColumnOrderings<String> get orderKey => $composableBuilder(
column: $table.orderKey, column: $table.orderKey,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@@ -1724,6 +1815,9 @@ class $TabAnnotationComposer extends Composer<_$TabDatabase, Tab> {
GeneratedColumn<String> get id => GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column); $composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get parentId =>
$composableBuilder(column: $table.parentId, builder: (column) => column);
GeneratedColumn<String> get orderKey => GeneratedColumn<String> get orderKey =>
$composableBuilder(column: $table.orderKey, builder: (column) => column); $composableBuilder(column: $table.orderKey, builder: (column) => column);
@@ -1814,6 +1908,7 @@ class $TabTableManager
updateCompanionCallback: updateCompanionCallback:
({ ({
Value<String> id = const Value.absent(), Value<String> id = const Value.absent(),
Value<String?> parentId = const Value.absent(),
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
Value<String> orderKey = const Value.absent(), Value<String> orderKey = const Value.absent(),
Value<Uri?> url = const Value.absent(), Value<Uri?> url = const Value.absent(),
@@ -1827,6 +1922,7 @@ class $TabTableManager
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => TabCompanion( }) => TabCompanion(
id: id, id: id,
parentId: parentId,
containerId: containerId, containerId: containerId,
orderKey: orderKey, orderKey: orderKey,
url: url, url: url,
@@ -1842,6 +1938,7 @@ class $TabTableManager
createCompanionCallback: createCompanionCallback:
({ ({
required String id, required String id,
Value<String?> parentId = const Value.absent(),
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
required String orderKey, required String orderKey,
Value<Uri?> url = const Value.absent(), Value<Uri?> url = const Value.absent(),
@@ -1855,6 +1952,7 @@ class $TabTableManager
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => TabCompanion.insert( }) => TabCompanion.insert(
id: id, id: id,
parentId: parentId,
containerId: containerId, containerId: containerId,
orderKey: orderKey, orderKey: orderKey,
url: url, url: url,
@@ -2114,3 +2212,22 @@ class $TabDatabaseManager {
$TabTableManager get tab => $TabTableManager(_db, _db.tab); $TabTableManager get tab => $TabTableManager(_db, _db.tab);
$TabFtsTableManager get tabFts => $TabFtsTableManager(_db, _db.tabFts); $TabFtsTableManager get tabFts => $TabFtsTableManager(_db, _db.tabFts);
} }
class TabTreesResult {
final String rootTabId;
final String latestTabId;
final DateTime latestTimestamp;
final int totalTabs;
TabTreesResult({
required this.rootTabId,
required this.latestTabId,
required this.latestTimestamp,
required this.totalTabs,
});
}
class UnorderedTabDescendantsResult {
final String id;
final String? parentId;
UnorderedTabDescendantsResult({required this.id, this.parentId});
}
@@ -0,0 +1,33 @@
import 'package:fast_equatable/fast_equatable.dart';
sealed class TabEntity with FastEquatable {
String get tabId;
}
class SingleTabEntity extends TabEntity {
@override
final String tabId;
SingleTabEntity({required this.tabId});
@override
List<Object?> get hashParameters => [tabId];
}
class TabTreeEntity extends TabEntity {
@override
final String tabId;
final String rootId;
final int totalTabs;
TabTreeEntity({
required this.tabId,
required this.rootId,
required this.totalTabs,
});
@override
List<Object?> get hashParameters => [tabId, rootId, totalTabs];
}
@@ -1,6 +1,7 @@
import 'package:nullability/nullability.dart'; import 'package:nullability/nullability.dart';
import 'package:riverpod/riverpod.dart'; import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
@@ -47,3 +48,19 @@ Stream<List<String>> containerTabIds(Ref ref, ContainerFilter containerFilter) {
return db.tabDao.getAllTabIds().watch(); return db.tabDao.getAllTabIds().watch();
} }
} }
@Riverpod()
Stream<List<TabTreesResult>> tabTrees(Ref ref) {
final db = ref.watch(tabDatabaseProvider);
return db.tabTrees().watch();
}
@Riverpod()
Stream<Map<String, String?>> tabDescendants(Ref ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.unorderedTabDescendants(tabId: tabId).watch().map((results) {
return Map.fromEntries(
results.map((pair) => MapEntry(pair.id, pair.parentId)),
);
});
}
@@ -303,5 +303,144 @@ class _ContainerTabIdsProviderElement
(origin as ContainerTabIdsProvider).containerFilter; (origin as ContainerTabIdsProvider).containerFilter;
} }
String _$tabTreesHash() => r'b7b7f7136827207dd01a7894a915be3d17b1ae63';
/// See also [tabTrees].
@ProviderFor(tabTrees)
final tabTreesProvider =
AutoDisposeStreamProvider<List<TabTreesResult>>.internal(
tabTrees,
name: r'tabTreesProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$tabTreesHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef TabTreesRef = AutoDisposeStreamProviderRef<List<TabTreesResult>>;
String _$tabDescendantsHash() => r'93e19df9896e4876dc911370c02bf10f1ee9a9e6';
/// See also [tabDescendants].
@ProviderFor(tabDescendants)
const tabDescendantsProvider = TabDescendantsFamily();
/// See also [tabDescendants].
class TabDescendantsFamily extends Family<AsyncValue<Map<String, String?>>> {
/// See also [tabDescendants].
const TabDescendantsFamily();
/// See also [tabDescendants].
TabDescendantsProvider call(String tabId) {
return TabDescendantsProvider(tabId);
}
@override
TabDescendantsProvider getProviderOverride(
covariant TabDescendantsProvider provider,
) {
return call(provider.tabId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'tabDescendantsProvider';
}
/// See also [tabDescendants].
class TabDescendantsProvider
extends AutoDisposeStreamProvider<Map<String, String?>> {
/// See also [tabDescendants].
TabDescendantsProvider(String tabId)
: this._internal(
(ref) => tabDescendants(ref as TabDescendantsRef, tabId),
from: tabDescendantsProvider,
name: r'tabDescendantsProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$tabDescendantsHash,
dependencies: TabDescendantsFamily._dependencies,
allTransitiveDependencies:
TabDescendantsFamily._allTransitiveDependencies,
tabId: tabId,
);
TabDescendantsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
Override overrideWith(
Stream<Map<String, String?>> Function(TabDescendantsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TabDescendantsProvider._internal(
(ref) => create(ref as TabDescendantsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeStreamProviderElement<Map<String, String?>> createElement() {
return _TabDescendantsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TabDescendantsProvider && other.tabId == tabId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, tabId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin TabDescendantsRef on AutoDisposeStreamProviderRef<Map<String, String?>> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _TabDescendantsProviderElement
extends AutoDisposeStreamProviderElement<Map<String, String?>>
with TabDescendantsRef {
_TabDescendantsProviderElement(super.provider);
@override
String get tabId => (origin as TabDescendantsProvider).tabId;
}
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
+2
View File
@@ -30,6 +30,7 @@ dependencies:
flutter_svg: ^2.1.0 flutter_svg: ^2.1.0
go_router: ^15.2.0 go_router: ^15.2.0
google_fonts: ^6.2.1 google_fonts: ^6.2.1
graphview: ^1.2.0
home_widget: ^0.8.0 home_widget: ^0.8.0
hooks_riverpod: ^2.6.1 hooks_riverpod: ^2.6.1
html: ^0.15.6 html: ^0.15.6
@@ -75,6 +76,7 @@ dependencies:
url: https://github.com/FaFre/uri-to-file.git url: https://github.com/FaFre/uri-to-file.git
url_launcher: ^6.3.1 url_launcher: ^6.3.1
uuid: ^4.5.1 uuid: ^4.5.1
vector_math: ^2.1.4
dev_dependencies: dev_dependencies:
analyzer_plugin: ^0.13.1 analyzer_plugin: ^0.13.1
@@ -185,6 +185,7 @@ class GeckoTabsApiImpl : GeckoTabsApi {
System.currentTimeMillis(), System.currentTimeMillis(),
TabContentState( TabContentState(
id = tab.id, id = tab.id,
parentId = tab.parentId,
contextId = tab.contextId, contextId = tab.contextId,
url = tab.content.url, url = tab.content.url,
title = tab.content.title, title = tab.content.title,
@@ -158,6 +158,7 @@ class Events(
System.currentTimeMillis(), System.currentTimeMillis(),
TabContentState( TabContentState(
id = tab.id, id = tab.id,
parentId = tab.parentId,
contextId = tab.contextId, contextId = tab.contextId,
url = tab.content.url, url = tab.content.url,
title = tab.content.title, title = tab.content.title,
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v25.3.1), do not edit directly. // Autogenerated from Pigeon (v25.3.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon // See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -1194,6 +1194,7 @@ data class SecurityInfoState (
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class TabContentState ( data class TabContentState (
val id: String, val id: String,
val parentId: String? = null,
val contextId: String? = null, val contextId: String? = null,
val url: String, val url: String,
val title: String, val title: String,
@@ -1206,19 +1207,21 @@ data class TabContentState (
companion object { companion object {
fun fromList(pigeonVar_list: List<Any?>): TabContentState { fun fromList(pigeonVar_list: List<Any?>): TabContentState {
val id = pigeonVar_list[0] as String val id = pigeonVar_list[0] as String
val contextId = pigeonVar_list[1] as String? val parentId = pigeonVar_list[1] as String?
val url = pigeonVar_list[2] as String val contextId = pigeonVar_list[2] as String?
val title = pigeonVar_list[3] as String val url = pigeonVar_list[3] as String
val progress = pigeonVar_list[4] as Long val title = pigeonVar_list[4] as String
val isPrivate = pigeonVar_list[5] as Boolean val progress = pigeonVar_list[5] as Long
val isFullScreen = pigeonVar_list[6] as Boolean val isPrivate = pigeonVar_list[6] as Boolean
val isLoading = pigeonVar_list[7] as Boolean val isFullScreen = pigeonVar_list[7] as Boolean
return TabContentState(id, contextId, url, title, progress, isPrivate, isFullScreen, isLoading) val isLoading = pigeonVar_list[8] as Boolean
return TabContentState(id, parentId, contextId, url, title, progress, isPrivate, isFullScreen, isLoading)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
return listOf( return listOf(
id, id,
parentId,
contextId, contextId,
url, url,
title, title,
File diff suppressed because it is too large Load Diff
@@ -419,6 +419,7 @@ class SecurityInfoState {
class TabContentState { class TabContentState {
final String id; final String id;
final String? parentId;
final String? contextId; final String? contextId;
final String url; final String url;
@@ -432,6 +433,7 @@ class TabContentState {
TabContentState( TabContentState(
this.id, this.id,
this.parentId,
this.contextId, this.contextId,
this.url, this.url,
this.title, this.title,