add isolated tab feature
This commit is contained in:
@@ -27,6 +27,7 @@ import 'package:weblibre/features/geckoview/domain/entities/states/find_result.d
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/history.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/security.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
|
||||
@@ -61,7 +62,9 @@ class TabState extends WebPageInfo {
|
||||
|
||||
final int progress;
|
||||
|
||||
final bool isPrivate;
|
||||
final TabMode tabMode;
|
||||
String? get isolationContextId => tabMode.isolationContextId;
|
||||
|
||||
final bool isFullScreen;
|
||||
final bool isLoading;
|
||||
final bool showToolbarAsExpanded;
|
||||
@@ -82,7 +85,7 @@ class TabState extends WebPageInfo {
|
||||
required this.icon,
|
||||
required this.thumbnail,
|
||||
required this.progress,
|
||||
required this.isPrivate,
|
||||
this.tabMode = TabMode.regular,
|
||||
required this.isFullScreen,
|
||||
required this.isLoading,
|
||||
required this.showToolbarAsExpanded,
|
||||
@@ -101,7 +104,6 @@ class TabState extends WebPageInfo {
|
||||
icon: null,
|
||||
thumbnail: null,
|
||||
progress: 0,
|
||||
isPrivate: false,
|
||||
isFullScreen: false,
|
||||
isLoading: false,
|
||||
showToolbarAsExpanded: false,
|
||||
@@ -120,7 +122,7 @@ class TabState extends WebPageInfo {
|
||||
icon,
|
||||
thumbnail,
|
||||
progress,
|
||||
isPrivate,
|
||||
tabMode,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
showToolbarAsExpanded,
|
||||
|
||||
@@ -21,7 +21,7 @@ abstract class _$TabStateCWProxy {
|
||||
|
||||
TabState progress(int progress);
|
||||
|
||||
TabState isPrivate(bool isPrivate);
|
||||
TabState tabMode(TabMode tabMode);
|
||||
|
||||
TabState isFullScreen(bool isFullScreen);
|
||||
|
||||
@@ -52,7 +52,7 @@ abstract class _$TabStateCWProxy {
|
||||
EquatableImage? icon,
|
||||
EquatableImage? thumbnail,
|
||||
int progress,
|
||||
bool isPrivate,
|
||||
TabMode tabMode,
|
||||
bool isFullScreen,
|
||||
bool isLoading,
|
||||
bool showToolbarAsExpanded,
|
||||
@@ -92,7 +92,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
||||
TabState progress(int progress) => call(progress: progress);
|
||||
|
||||
@override
|
||||
TabState isPrivate(bool isPrivate) => call(isPrivate: isPrivate);
|
||||
TabState tabMode(TabMode tabMode) => call(tabMode: tabMode);
|
||||
|
||||
@override
|
||||
TabState isFullScreen(bool isFullScreen) => call(isFullScreen: isFullScreen);
|
||||
@@ -136,7 +136,7 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
||||
Object? icon = const $CopyWithPlaceholder(),
|
||||
Object? thumbnail = const $CopyWithPlaceholder(),
|
||||
Object? progress = const $CopyWithPlaceholder(),
|
||||
Object? isPrivate = const $CopyWithPlaceholder(),
|
||||
Object? tabMode = const $CopyWithPlaceholder(),
|
||||
Object? isFullScreen = const $CopyWithPlaceholder(),
|
||||
Object? isLoading = const $CopyWithPlaceholder(),
|
||||
Object? showToolbarAsExpanded = const $CopyWithPlaceholder(),
|
||||
@@ -175,10 +175,10 @@ class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
||||
? _value.progress
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: progress as int,
|
||||
isPrivate: isPrivate == const $CopyWithPlaceholder() || isPrivate == null
|
||||
? _value.isPrivate
|
||||
tabMode: tabMode == const $CopyWithPlaceholder() || tabMode == null
|
||||
? _value.tabMode
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: isPrivate as bool,
|
||||
: tabMode as TabMode,
|
||||
isFullScreen:
|
||||
isFullScreen == const $CopyWithPlaceholder() || isFullScreen == null
|
||||
? _value.isFullScreen
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -32,6 +31,7 @@ import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
@@ -51,13 +51,10 @@ GeckoSelectionActionService selectionActionService(Ref ref) {
|
||||
tabStatesProvider,
|
||||
)[ref.read(selectedTabProvider)];
|
||||
|
||||
final isPrivate = selectedTabState?.isPrivate.mapNotNull(
|
||||
(isCurrentPrivate) =>
|
||||
isCurrentPrivate ? TabType.private : TabType.regular,
|
||||
);
|
||||
final selectedTabType = selectedTabState?.tabMode.toTabType();
|
||||
|
||||
final route = SearchRoute(
|
||||
tabType: isPrivate ?? settings.defaultCreateTabType,
|
||||
tabType: selectedTabType ?? settings.defaultCreateTabType,
|
||||
searchText: text,
|
||||
);
|
||||
|
||||
@@ -76,19 +73,20 @@ GeckoSelectionActionService selectionActionService(Ref ref) {
|
||||
tabStatesProvider,
|
||||
)[ref.read(selectedTabProvider)];
|
||||
|
||||
final isPrivate =
|
||||
currentTab?.isPrivate ??
|
||||
ref
|
||||
.read(generalSettingsWithDefaultsProvider)
|
||||
.defaultCreateTabType ==
|
||||
TabType.private;
|
||||
final tabMode =
|
||||
currentTab?.tabMode ??
|
||||
TabMode.fromTabType(
|
||||
ref
|
||||
.read(generalSettingsWithDefaultsProvider)
|
||||
.defaultCreateTabType,
|
||||
);
|
||||
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: defaultSearchBang.getTemplateUrl(text),
|
||||
parentId: currentTab?.id,
|
||||
private: isPrivate,
|
||||
tabMode: tabMode,
|
||||
selectTab: true,
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -55,7 +55,7 @@ final class SelectionActionServiceProvider
|
||||
}
|
||||
|
||||
String _$selectionActionServiceHash() =>
|
||||
r'75fa0fa770e74e49554d4098102fde9f9e531c4f';
|
||||
r'75701bb0be3302b461d296a7b0b28e3a30a9d346';
|
||||
|
||||
@ProviderFor(eventService)
|
||||
final eventServiceProvider = EventServiceProvider._();
|
||||
|
||||
@@ -34,6 +34,8 @@ import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
@@ -78,13 +80,23 @@ class TabStates extends _$TabStates {
|
||||
resolvedTitle = contentState.title;
|
||||
}
|
||||
|
||||
// Infer tabMode from context ID if not already set and context is isolated
|
||||
final inferredTabMode = switch (current.tabMode) {
|
||||
IsolatedTabMode() => current.tabMode,
|
||||
_ when isIsolatedContextId(contentState.contextId) => TabMode.isolated(
|
||||
contentState.contextId!,
|
||||
),
|
||||
_ when contentState.isPrivate => TabMode.private,
|
||||
_ => TabMode.regular,
|
||||
};
|
||||
|
||||
final newState = current.copyWith(
|
||||
parentId: contentState.parentId,
|
||||
contextId: contentState.contextId,
|
||||
url: url,
|
||||
title: resolvedTitle,
|
||||
progress: contentState.progress,
|
||||
isPrivate: contentState.isPrivate,
|
||||
tabMode: inferredTabMode,
|
||||
isFullScreen: contentState.isFullScreen,
|
||||
isLoading: contentState.isLoading,
|
||||
showToolbarAsExpanded: contentState.showToolbarAsExpanded,
|
||||
@@ -373,7 +385,9 @@ Future<bool> isTabTunneled(Ref ref, String? tabId) async {
|
||||
final torSettings = ref.watch(torSettingsWithDefaultsProvider);
|
||||
|
||||
if (tabState != null) {
|
||||
if (tabState.isPrivate) {
|
||||
// Isolated tabs follow the same proxy rules as regular tabs
|
||||
// (container-based routing via proxy aliasing)
|
||||
if (tabState.tabMode is PrivateTabMode) {
|
||||
return torSettings.proxyPrivateTabsTor;
|
||||
} else {
|
||||
switch (torSettings.proxyRegularTabsMode) {
|
||||
@@ -402,13 +416,9 @@ TabState? selectedTabState(Ref ref) {
|
||||
|
||||
@Riverpod()
|
||||
TabType? selectedTabType(Ref ref) {
|
||||
final isPrivate = ref.watch(
|
||||
selectedTabStateProvider.select((value) => value?.isPrivate),
|
||||
);
|
||||
final selectedState = ref.watch(selectedTabStateProvider);
|
||||
|
||||
return isPrivate.mapNotNull(
|
||||
(isCurrentPrivate) => isCurrentPrivate ? TabType.private : TabType.regular,
|
||||
);
|
||||
return selectedState?.tabMode.toTabType();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabStatesProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabStatesHash() => r'975de1242f42e2017754dc560125788b83ad8b72';
|
||||
String _$tabStatesHash() => r'0fd97193ff963006e1943436b25b025dd3885c65';
|
||||
|
||||
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
|
||||
Map<String, TabState> build();
|
||||
@@ -258,7 +258,7 @@ final class IsTabTunneledProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$isTabTunneledHash() => r'55a42ccb73d1921eed27d32f6da3a54680d4ac8c';
|
||||
String _$isTabTunneledHash() => r'7265f5e6f32056846d3df38f48476d10c3a70bac';
|
||||
|
||||
final class IsTabTunneledFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
||||
@@ -358,7 +358,7 @@ final class SelectedTabTypeProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedTabTypeHash() => r'53093fc6db8becde7500163661e087fb0a2eb955';
|
||||
String _$selectedTabTypeHash() => r'fe14b5c9f81d8f5859ae9d758e440c42908e8a54';
|
||||
|
||||
@ProviderFor(selectedTabContainerId)
|
||||
final selectedTabContainerIdProvider = SelectedTabContainerIdProvider._();
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:drift/drift.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
|
||||
@@ -31,12 +32,16 @@ import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
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/features/browser/domain/services/browser_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/isolation_context.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_source.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/domain/providers/selected_container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/tor/domain/repositories/tor_proxy.dart';
|
||||
import 'package:weblibre/utils/debouncer.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
@@ -46,6 +51,8 @@ class TabRepository extends _$TabRepository {
|
||||
final _tabsService = GeckoTabService();
|
||||
|
||||
final _tabFromIntent = <String>{};
|
||||
final _closeLock = Lock();
|
||||
final _pendingIsolationCleanup = <String>{};
|
||||
|
||||
bool hasLaunchedFromIntent(String? tabId) {
|
||||
if (tabId == null) {
|
||||
@@ -79,13 +86,13 @@ class TabRepository extends _$TabRepository {
|
||||
}
|
||||
|
||||
Future<String> addTab({
|
||||
required TabMode tabMode,
|
||||
Uri? url,
|
||||
required bool selectTab,
|
||||
bool startLoading = true,
|
||||
String? parentId,
|
||||
LoadUrlFlags flags = LoadUrlFlags.NONE,
|
||||
Source source = Internal.newTab,
|
||||
required bool private,
|
||||
HistoryMetadataKey? historyMetadata,
|
||||
Map<String, String>? additionalHeaders,
|
||||
TabContainerSelection containerSelection =
|
||||
@@ -101,10 +108,20 @@ class TabRepository extends _$TabRepository {
|
||||
SpecificContainerTabSelection(:final container) => container,
|
||||
};
|
||||
|
||||
final validatedParentId = await _resolveParentIdForContext(
|
||||
parentId: parentId,
|
||||
targetContextId: assignedContainer?.metadata.contextualIdentity,
|
||||
);
|
||||
// For isolated tabs, skip parent context validation since
|
||||
// isolated tabs use their own immutable context ID.
|
||||
final validatedParentId = tabMode is IsolatedTabMode
|
||||
? parentId
|
||||
: await _resolveParentIdForContext(
|
||||
parentId: parentId,
|
||||
targetContextId: assignedContainer?.metadata.contextualIdentity,
|
||||
);
|
||||
|
||||
final effectiveIsolationContextId = tabMode.isolationContextId;
|
||||
|
||||
final effectiveContextId = tabMode is IsolatedTabMode
|
||||
? effectiveIsolationContextId
|
||||
: assignedContainer?.metadata.contextualIdentity;
|
||||
|
||||
final newTabId = await tabDao.upsertTabTransactional(
|
||||
() {
|
||||
@@ -114,17 +131,17 @@ class TabRepository extends _$TabRepository {
|
||||
startLoading: startLoading,
|
||||
parentId: validatedParentId,
|
||||
flags: flags,
|
||||
contextId: assignedContainer?.metadata.contextualIdentity,
|
||||
contextId: effectiveContextId,
|
||||
source: source,
|
||||
private: private,
|
||||
private: tabMode is PrivateTabMode,
|
||||
historyMetadata: historyMetadata,
|
||||
additionalHeaders: additionalHeaders,
|
||||
);
|
||||
},
|
||||
parentId: Value(validatedParentId),
|
||||
containerId: Value(assignedContainer?.id),
|
||||
isPrivate: Value(private),
|
||||
url: Value(url),
|
||||
tabMode: Value(tabMode),
|
||||
);
|
||||
|
||||
if (launchedFromIntent) {
|
||||
@@ -188,8 +205,14 @@ class TabRepository extends _$TabRepository {
|
||||
parentId: Value(validatedParentId),
|
||||
source: TabSource.manual,
|
||||
containerId: Value(assignedContainer?.id),
|
||||
isPrivate: Value(tab.private),
|
||||
url: Value(Uri.tryParse(tab.url)),
|
||||
tabMode: Value(
|
||||
isIsolatedContextId(tab.contextId)
|
||||
? TabMode.isolated(tab.contextId!)
|
||||
: tab.private
|
||||
? TabMode.private
|
||||
: TabMode.regular,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -204,19 +227,35 @@ class TabRepository extends _$TabRepository {
|
||||
}) async {
|
||||
final tabDao = ref.read(tabDatabaseProvider).tabDao;
|
||||
|
||||
final sourceTabMode =
|
||||
await tabDao.getTabMode(selectTabId).getSingleOrNull() ??
|
||||
TabMode.regular;
|
||||
|
||||
// Duplicating an isolated tab creates a new isolation group
|
||||
final duplicateIsolationContextId = sourceTabMode is IsolatedTabMode
|
||||
? newIsolatedContextId()
|
||||
: null;
|
||||
|
||||
final duplicateTabMode = sourceTabMode is IsolatedTabMode
|
||||
? TabMode.isolated(duplicateIsolationContextId!)
|
||||
: sourceTabMode;
|
||||
|
||||
// Isolated tabs always use their isolation context ID
|
||||
final effectiveContextId = sourceTabMode is IsolatedTabMode
|
||||
? duplicateIsolationContextId
|
||||
: containerData?.metadata.contextualIdentity;
|
||||
|
||||
return await tabDao.upsertTabTransactional(
|
||||
() {
|
||||
return _tabsService.duplicateTab(
|
||||
selectTabId: selectTabId,
|
||||
newContextId: containerData?.metadata.contextualIdentity,
|
||||
newContextId: effectiveContextId,
|
||||
selectNewTab: selectTab,
|
||||
);
|
||||
},
|
||||
parentId: const Value.absent(),
|
||||
containerId: Value(containerData?.id),
|
||||
isPrivate: Value(
|
||||
await tabDao.getTabIsPrivate(selectTabId).getSingleOrNull(),
|
||||
),
|
||||
tabMode: Value(duplicateTabMode),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -402,27 +441,148 @@ class TabRepository extends _$TabRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeTab(String tabId) async {
|
||||
if (ref.read(selectedTabProvider) == tabId) {
|
||||
await _selectNextTab(tabId);
|
||||
}
|
||||
Future<void> closeTab(String tabId) {
|
||||
return _closeLock.synchronized(() async {
|
||||
// Collect isolation context before close
|
||||
final isolationContextId = ref
|
||||
.read(tabStatesProvider)[tabId]
|
||||
?.isolationContextId;
|
||||
|
||||
return _tabsService.removeTab(tabId: tabId);
|
||||
if (ref.read(selectedTabProvider) == tabId) {
|
||||
await _selectNextTab(tabId);
|
||||
}
|
||||
|
||||
await _tabsService.removeTab(tabId: tabId);
|
||||
|
||||
// Queue isolation cleanup — actual cleanup runs after syncTabs
|
||||
// deletes the DB row, so the count check is accurate.
|
||||
if (isolationContextId != null) {
|
||||
_pendingIsolationCleanup.add(isolationContextId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> closeTabs(List<String> tabIds) async {
|
||||
final selectedTab = ref.read(selectedTabProvider);
|
||||
if (selectedTab.mapNotNull(tabIds.contains) ?? false) {
|
||||
await _selectNextTab(selectedTab!);
|
||||
Future<void> closeTabs(List<String> tabIds) {
|
||||
return _closeLock.synchronized(() async {
|
||||
// Collect isolation contexts from tabs being closed
|
||||
for (final tabId in tabIds) {
|
||||
final contextId = ref
|
||||
.read(tabStatesProvider)[tabId]
|
||||
?.isolationContextId;
|
||||
if (contextId != null) {
|
||||
_pendingIsolationCleanup.add(contextId);
|
||||
}
|
||||
}
|
||||
|
||||
final selectedTab = ref.read(selectedTabProvider);
|
||||
if (selectedTab.mapNotNull(tabIds.contains) ?? false) {
|
||||
await _selectNextTab(selectedTab!);
|
||||
}
|
||||
|
||||
await _tabsService.removeTabs(ids: tabIds);
|
||||
});
|
||||
}
|
||||
|
||||
/// Clears Gecko browsing data and removes proxy alias for an isolation
|
||||
/// context if no more tabs share it.
|
||||
Future<void> _cleanupIsolationContextIfEmpty(String contextId) async {
|
||||
final tabDao = ref.read(tabDatabaseProvider).tabDao;
|
||||
|
||||
// Re-verify count after close (handles concurrent close races)
|
||||
final remaining = await tabDao.tabsInIsolationGroup(contextId).getSingle();
|
||||
|
||||
if (remaining > 0) return;
|
||||
|
||||
// Guard against debounced DB persistence: a sibling tab can already be
|
||||
// active in-memory for this context before isolation_context_id is written.
|
||||
final activeTabs = ref.read(tabListProvider).value;
|
||||
final activeStates = ref.read(tabStatesProvider);
|
||||
final hasActiveSibling = activeTabs.any((tabId) {
|
||||
final state = activeStates[tabId];
|
||||
if (state == null) return false;
|
||||
|
||||
return state.isolationContextId == contextId ||
|
||||
state.contextId == contextId;
|
||||
});
|
||||
|
||||
if (hasActiveSibling) {
|
||||
logger.i(
|
||||
'Skipping isolation cleanup for active context still in memory: $contextId',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return _tabsService.removeTabs(ids: tabIds);
|
||||
logger.i('Cleaning up isolation context: $contextId');
|
||||
|
||||
// Clear Gecko browsing data for this context
|
||||
try {
|
||||
await ref
|
||||
.read(browserDataServiceProvider.notifier)
|
||||
.clearDataForContext(contextId);
|
||||
} catch (e, st) {
|
||||
logger.e(
|
||||
'Failed to clear data for isolation context $contextId',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
|
||||
// Best-effort: remove proxy alias (no-op if never set)
|
||||
try {
|
||||
await ref
|
||||
.read(torProxyRepositoryProvider.notifier)
|
||||
.removeContainerProxy(contextId);
|
||||
} catch (e, st) {
|
||||
logger.e(
|
||||
'Failed to remove proxy for isolation context $contextId',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> undoClose() {
|
||||
return _tabsService.undo();
|
||||
}
|
||||
|
||||
/// Cleans up isolation contexts from previous crashed sessions.
|
||||
/// Called once after tab list stabilizes on startup.
|
||||
// Future<void> _cleanupOrphanedIsolationContexts() async {
|
||||
// final tabDao = ref.read(tabDatabaseProvider).tabDao;
|
||||
|
||||
// try {
|
||||
// await _closeLock.synchronized(() async {
|
||||
// if (!ref.mounted) return;
|
||||
|
||||
// // Reconcile DB rows against the current engine tab snapshot, including
|
||||
// // valid empty-tab sessions (retainTabIds can be empty here).
|
||||
// final syncTabsResult = await tabDao.syncTabs(
|
||||
// retainTabIds: ref.read(tabListProvider).value,
|
||||
// );
|
||||
// _pendingIsolationCleanup.addAll(
|
||||
// syncTabsResult.deletedIsolationContextIds,
|
||||
// );
|
||||
|
||||
// if (_pendingIsolationCleanup.isNotEmpty) {
|
||||
// final pending = Set<String>.of(_pendingIsolationCleanup);
|
||||
// _pendingIsolationCleanup.clear();
|
||||
|
||||
// for (final contextId in pending) {
|
||||
// if (!ref.mounted) return;
|
||||
// logger.i('Cleaning orphaned isolation context: $contextId');
|
||||
// await _cleanupIsolationContextIfEmpty(contextId);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// } catch (e, st) {
|
||||
// logger.e(
|
||||
// 'Error during orphan isolation context cleanup',
|
||||
// error: e,
|
||||
// stackTrace: st,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
void build() {
|
||||
final eventSerivce = ref.watch(eventServiceProvider);
|
||||
@@ -438,7 +598,6 @@ class TabRepository extends _$TabRepository {
|
||||
parentId: const Value.absent(),
|
||||
source: TabSource.addedEvent,
|
||||
containerId: Value(containerId),
|
||||
isPrivate: const Value.absent(),
|
||||
);
|
||||
},
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
@@ -475,7 +634,7 @@ class TabRepository extends _$TabRepository {
|
||||
if (event.blocked || tabIsEmpty) {
|
||||
await addTab(
|
||||
url: uri,
|
||||
private: tabState.isPrivate,
|
||||
tabMode: tabState.tabMode,
|
||||
containerSelection: TabContainerSelection.specific(
|
||||
containerData,
|
||||
),
|
||||
@@ -567,12 +726,42 @@ class TabRepository extends _$TabRepository {
|
||||
tabListProvider,
|
||||
(previous, next) async {
|
||||
//Only sync tabs if there has been a previous value or is not empty
|
||||
final syncTabs =
|
||||
final shouldSyncTabs =
|
||||
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
|
||||
|
||||
if (syncTabs) {
|
||||
await db.tabDao.syncTabs(retainTabIds: next.value);
|
||||
if (shouldSyncTabs) {
|
||||
final syncTabsResult = await db.tabDao.syncTabs(
|
||||
retainTabIds: next.value,
|
||||
);
|
||||
// Capture isolation contexts from rows deleted by syncTabs
|
||||
// (orphaned tabs from crashes, or tabs the engine dropped).
|
||||
_pendingIsolationCleanup.addAll(
|
||||
syncTabsResult.deletedIsolationContextIds,
|
||||
);
|
||||
}
|
||||
|
||||
// Process pending isolation context cleanups after syncTabs
|
||||
// has deleted the rows, so the count check is accurate.
|
||||
if (_pendingIsolationCleanup.isNotEmpty) {
|
||||
final pending = Set<String>.of(_pendingIsolationCleanup);
|
||||
_pendingIsolationCleanup.clear();
|
||||
for (final contextId in pending) {
|
||||
if (!ref.mounted) break;
|
||||
await _cleanupIsolationContextIfEmpty(contextId);
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot orphan cleanup after tab list stabilizes (5s debounce).
|
||||
// Also runs for DB-only contexts whose rows were already deleted
|
||||
// by syncTabs above (those are handled via _pendingIsolationCleanup).
|
||||
// if (!orphanCleanupDone) {
|
||||
// orphanCleanupTimer?.cancel();
|
||||
// orphanCleanupTimer = Timer(const Duration(seconds: 5), () async {
|
||||
// if (orphanCleanupDone || !ref.mounted) return;
|
||||
// orphanCleanupDone = true;
|
||||
// await _cleanupOrphanedIsolationContexts();
|
||||
// });
|
||||
// }
|
||||
},
|
||||
onError: (Object error, StackTrace stackTrace) {
|
||||
logger.e(
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'd77e4e74bb7ee1b466172bca245b05c6bf03196c';
|
||||
String _$tabRepositoryHash() => r'a6953b50e4b9d0e7e297a058872b9ad1c7d55c1a';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
Reference in New Issue
Block a user