clear data of isolated containers; improved tab handling/state/duplication;

This commit is contained in:
Fabian Freund
2025-12-31 06:22:48 +01:00
parent 62f53ce327
commit d4585921ce
22 changed files with 909 additions and 280 deletions
@@ -59,6 +59,10 @@ class BrowserDataService extends _$BrowserDataService {
}
}
Future<void> clearDataForContext(String contextId) {
return _service.clearDataForContext(contextId);
}
@override
void build() {}
}
@@ -42,7 +42,7 @@ final class BrowserDataServiceProvider
}
String _$browserDataServiceHash() =>
r'0a00db5b143d3851f2c171f4f939940f959b67bb';
r'502dff526bcf7c10c218d2d9fa2cd0f41ad25622';
abstract class _$BrowserDataService extends $Notifier<void> {
void build();
@@ -196,13 +196,20 @@ class TabMenu extends HookConsumerWidget {
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
final tabId = await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: tabState.url,
private: false,
selectTab: false,
);
final tabId = (tabState.isPrivate)
? await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: tabState.url,
private: false,
selectTab: false,
)
: await ref
.read(tabRepositoryProvider.notifier)
.duplicateTab(
selectTabId: selectedTabId,
containerId: tabState.contextId,
);
if (context.mounted) {
//save reference before pop `ref` gets disposed
@@ -223,13 +230,20 @@ class TabMenu extends HookConsumerWidget {
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
final tabId = await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: tabState.url,
private: true,
selectTab: false,
);
final tabId = (!tabState.isPrivate)
? await ref
.read(tabRepositoryProvider.notifier)
.addTab(
url: tabState.url,
private: true,
selectTab: false,
)
: await ref
.read(tabRepositoryProvider.notifier)
.duplicateTab(
selectTabId: selectedTabId,
containerId: tabState.contextId,
);
if (context.mounted) {
//save reference before pop `ref` gets disposed
@@ -24,6 +24,7 @@ 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:weblibre/features/geckoview/domain/entities/states/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/presentation/widgets/tab_icon.dart';
@@ -95,10 +96,8 @@ class GridTabPreview extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabState = ref.watch(tabStateProvider(tabId));
if (tabState == null) {
return const SizedBox.shrink();
}
final tabState =
ref.watch(tabStateProvider(tabId)) ?? TabState.$default(tabId);
final extendedDeleteMenuController = useMenuController();
@@ -255,10 +254,8 @@ class ListTabPreview extends HookConsumerWidget {
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 tabState =
ref.watch(tabStateProvider(tabId)) ?? TabState.$default(tabId);
final extendedDeleteMenuController = useMenuController();
@@ -388,14 +385,6 @@ class SingleGridTabPreview extends HookConsumerWidget {
@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);
@@ -512,14 +501,6 @@ class SingleListTabPreview extends HookConsumerWidget {
@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);
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
@@ -8,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.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';
@@ -381,6 +383,182 @@ class TabViewHeader extends HookConsumerWidget {
}
},
),
Consumer(
builder: (context, ref, child) {
final selectedContainer = ref.watch(
selectedContainerDataProvider.select(
(value) => value.value,
),
);
// Only show if container has cookie isolation
if (selectedContainer
?.metadata
.contextualIdentity ==
null) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
MenuItemButton(
closeOnActivate: false,
leadingIcon: const Icon(
Icons.cleaning_services,
),
child: const Text('Clear Container Data'),
onPressed: () async {
final containerId = selectedContainer!.id;
final tabs = await ref
.read(
tabDataRepositoryProvider.notifier,
)
.getContainerTabsData(containerId);
if (!context.mounted) return;
final result = await showDialog<bool?>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
icon: const Icon(
Icons.cleaning_services,
),
title: const Text(
'Clear Container Data?',
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'This will clear all data for this container:',
),
const SizedBox(height: 8),
const Text('• Cookies'),
const Text('• Site data'),
const Text('• Cache'),
const Text('• Permissions'),
const SizedBox(height: 8),
Text(
'${tabs.length} tab(s) will be closed and reopened fresh.',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(
context,
).colorScheme.tertiary,
),
),
],
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context, false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context, true);
},
child: const Text('Clear Data'),
),
],
);
},
);
if (result == true) {
try {
await ref
.read(
tabDataRepositoryProvider
.notifier,
)
.closeContainerTabs(containerId);
await ref
.read(
browserDataServiceProvider
.notifier,
)
.clearDataForContext(
selectedContainer
.metadata
.contextualIdentity!,
);
await ref
.read(
tabRepositoryProvider.notifier,
)
.addMultipleTabs(
tabs: tabs
.map(
(tab) => AddTabParams(
url: tab.url.toString(),
startLoading: true,
parentId: tab.parentId,
private:
tab.isPrivate ??
false,
flags: LoadUrlFlags.NONE
.toValue(),
source: Internal.newTab
.toValue(),
contextId: selectedContainer
.metadata
.contextualIdentity,
),
)
.toList(),
container: Value(
selectedContainer,
),
);
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(
const SnackBar(
content: Text(
'Container data cleared successfully',
),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'Error clearing data: $e',
),
backgroundColor: Theme.of(
context,
).colorScheme.error,
),
);
}
}
}
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
),
],
);
},
),
],
child: IconButton(
onPressed: () {