container strict mode feature
This commit is contained in:
@@ -900,6 +900,14 @@ class TabRepository extends _$TabRepository {
|
||||
|
||||
final containerSiteAssignementSub = eventSerivce.siteAssignementEvent.listen(
|
||||
(event) async {
|
||||
// Strict-mode blocks have no destination container to re-open into; the
|
||||
// navigation was already cancelled natively and the user is notified via
|
||||
// a snackbar (see the strict-block listener in the app shell). Nothing
|
||||
// to reconcile here.
|
||||
if (event.strict) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tabId = event.tabId;
|
||||
if (tabId != null) {
|
||||
final tabState = ref.read(tabStatesProvider)[tabId];
|
||||
|
||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$tabRepositoryHash() => r'982b80b8ea7c694958bc10cc7e8e15310af6a238';
|
||||
String _$tabRepositoryHash() => r'dd2f9c776be1d5437141e94dbd0b7ef382093b63';
|
||||
|
||||
abstract class _$TabRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
+8
@@ -289,6 +289,14 @@ class ProxySettingsReplication extends _$ProxySettingsReplication {
|
||||
}
|
||||
});
|
||||
|
||||
ref.listen(watchStrictContextAssignmentsProvider, (previous, next) async {
|
||||
if (next.hasValue) {
|
||||
await ref
|
||||
.read(containerProxyRepositoryProvider.notifier)
|
||||
.setStrictContexts(next.requireValue);
|
||||
}
|
||||
});
|
||||
|
||||
ref.listen(
|
||||
fireImmediately: true,
|
||||
watchIsolatedContextContainerMapProvider.select(
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ final class ProxySettingsReplicationProvider
|
||||
}
|
||||
|
||||
String _$proxySettingsReplicationHash() =>
|
||||
r'cdb624b7f7806a7ce4218da350253ed3d29986d0';
|
||||
r'69787c85c94ff165e3eeb0f0a3f3fc83e88a1b83';
|
||||
|
||||
abstract class _$ProxySettingsReplication extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -259,6 +259,10 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
||||
return db.definitionsDrift.containersToClearOnExit();
|
||||
}
|
||||
|
||||
Selectable<StrictContextAssignmentsResult> strictContextAssignments() {
|
||||
return db.definitionsDrift.strictContextAssignments();
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<ContainerData> getContainerByContextualIdentity(
|
||||
String contextId,
|
||||
) {
|
||||
|
||||
@@ -969,6 +969,40 @@ containersToClearOnExit:
|
||||
json_extract(container.metadata, '$.clearDataOnExit') = 1
|
||||
AND container.metadata ->> '$.contextualIdentity' IS NOT NULL;
|
||||
|
||||
-- Strict-mode enforcement map for the container-proxy extension. Each row maps
|
||||
-- a Gecko cookie-store context (`context_id`) that must be enforced to the
|
||||
-- container's base contextualIdentity (`assignment_context_id`) that its site
|
||||
-- assignments are keyed on. In strict mode a navigation is only allowed when
|
||||
-- the target origin is assigned to `assignment_context_id` exactly (no proxy
|
||||
-- equivalence).
|
||||
--
|
||||
-- Two sources, unioned:
|
||||
-- 1. The strict container's own base context (context_id == assignment).
|
||||
-- 2. Every isolation context in use by an isolated tab (tab_mode = 2) whose
|
||||
-- container is strict: those tabs load under `isolation_context_id` as
|
||||
-- their cookie store, but their assignments live on the container's base
|
||||
-- context, so they map isolation_context_id -> base context.
|
||||
-- Null contextualIdentity is excluded: strictness needs a Gecko contextId.
|
||||
strictContextAssignments:
|
||||
SELECT
|
||||
container.metadata ->> '$.contextualIdentity' AS context_id,
|
||||
container.metadata ->> '$.contextualIdentity' AS assignment_context_id
|
||||
FROM container
|
||||
WHERE
|
||||
json_extract(container.metadata, '$.strictMode') = 1
|
||||
AND container.metadata ->> '$.contextualIdentity' IS NOT NULL
|
||||
UNION
|
||||
SELECT DISTINCT
|
||||
t.isolation_context_id AS context_id,
|
||||
c.metadata ->> '$.contextualIdentity' AS assignment_context_id
|
||||
FROM tab t
|
||||
INNER JOIN container c ON c.id = t.container_id
|
||||
WHERE
|
||||
t.tab_mode = 2
|
||||
AND t.isolation_context_id IS NOT NULL
|
||||
AND json_extract(c.metadata, '$.strictMode') = 1
|
||||
AND c.metadata ->> '$.contextualIdentity' IS NOT NULL;
|
||||
|
||||
tabsInIsolationGroup:
|
||||
SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = :contextId;
|
||||
|
||||
|
||||
+19
@@ -6695,6 +6695,19 @@ class DefinitionsDrift extends i9.ModularAccessor {
|
||||
).map((i0.QueryRow row) => row.readNullable<String>('contextual_identity'));
|
||||
}
|
||||
|
||||
i0.Selectable<StrictContextAssignmentsResult> strictContextAssignments() {
|
||||
return customSelect(
|
||||
'SELECT container.metadata ->> \'\$.contextualIdentity\' AS context_id, container.metadata ->> \'\$.contextualIdentity\' AS assignment_context_id FROM container WHERE json_extract(container.metadata, \'\$.strictMode\') = 1 AND container.metadata ->> \'\$.contextualIdentity\' IS NOT NULL UNION SELECT DISTINCT t.isolation_context_id AS context_id, c.metadata ->> \'\$.contextualIdentity\' AS assignment_context_id FROM tab AS t INNER JOIN container AS c ON c.id = t.container_id WHERE t.tab_mode = 2 AND t.isolation_context_id IS NOT NULL AND json_extract(c.metadata, \'\$.strictMode\') = 1 AND c.metadata ->> \'\$.contextualIdentity\' IS NOT NULL',
|
||||
variables: [],
|
||||
readsFrom: {container, tab},
|
||||
).map(
|
||||
(i0.QueryRow row) => StrictContextAssignmentsResult(
|
||||
contextId: row.readNullable<String>('context_id'),
|
||||
assignmentContextId: row.readNullable<String>('assignment_context_id'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
i0.Selectable<int> tabsInIsolationGroup({String? contextId}) {
|
||||
return customSelect(
|
||||
'SELECT COUNT(*) AS count FROM tab WHERE isolation_context_id = ?1',
|
||||
@@ -6789,6 +6802,12 @@ class ContainerIdsByContextualIdentitiesResult {
|
||||
});
|
||||
}
|
||||
|
||||
class StrictContextAssignmentsResult {
|
||||
final String? contextId;
|
||||
final String? assignmentContextId;
|
||||
StrictContextAssignmentsResult({this.contextId, this.assignmentContextId});
|
||||
}
|
||||
|
||||
class IsolatedContextContainerPairsResult {
|
||||
final String? isolationContextId;
|
||||
final String? containerId;
|
||||
|
||||
@@ -73,6 +73,16 @@ class ContainerMetadata with FastEquatable {
|
||||
|
||||
final List<Uri>? assignedSites;
|
||||
|
||||
// When true, tabs in this container may only load origins listed in
|
||||
// [assignedSites]; any other top-level navigation is blocked. Read on the
|
||||
// native side via `json_extract(metadata, '$.strictMode')` (see the
|
||||
// `strictContextAssignments` query in definitions.drift) and pushed to the
|
||||
// container-proxy web extension. Requires a Gecko contextId — the extension
|
||||
// keys strictness on the tab's cookieStoreId — so it is normalized to false
|
||||
// when [contextualIdentity] is null (mirrors [excludeFromHistory]).
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool strictMode;
|
||||
|
||||
ContainerMetadata({
|
||||
required this.iconData,
|
||||
required this.contextualIdentity,
|
||||
@@ -83,6 +93,7 @@ class ContainerMetadata with FastEquatable {
|
||||
required this.bypassGlobalProxy,
|
||||
required this.useCustomColor,
|
||||
required this.assignedSites,
|
||||
required this.strictMode,
|
||||
});
|
||||
|
||||
ContainerMetadata.withDefaults({
|
||||
@@ -95,6 +106,7 @@ class ContainerMetadata with FastEquatable {
|
||||
bool? bypassGlobalProxy,
|
||||
bool? useCustomColor,
|
||||
List<Uri>? assignedSites,
|
||||
bool? strictMode,
|
||||
}) : this(
|
||||
iconData: iconData,
|
||||
contextualIdentity: contextualIdentity,
|
||||
@@ -112,6 +124,10 @@ class ContainerMetadata with FastEquatable {
|
||||
bypassGlobalProxy: bypassGlobalProxy ?? false,
|
||||
useCustomColor: useCustomColor ?? false,
|
||||
assignedSites: assignedSites,
|
||||
// Strict mode needs a contextId (the extension keys on cookieStoreId);
|
||||
// normalize away the invalid combination on read, and writers re-apply
|
||||
// it via [sanitized].
|
||||
strictMode: (strictMode ?? false) && contextualIdentity != null,
|
||||
);
|
||||
|
||||
/// Enforce the [excludeFromHistory] invariant before persistence: it can only
|
||||
@@ -120,10 +136,16 @@ class ContainerMetadata with FastEquatable {
|
||||
/// The primary constructor can't normalize (copy_with_extension_gen requires
|
||||
/// params to map 1:1 to fields), so writers route through this.
|
||||
ContainerMetadata sanitized() {
|
||||
if (excludeFromHistory && contextualIdentity == null) {
|
||||
return copyWith(excludeFromHistory: false);
|
||||
var result = this;
|
||||
if (result.excludeFromHistory && result.contextualIdentity == null) {
|
||||
result = result.copyWith(excludeFromHistory: false);
|
||||
}
|
||||
return this;
|
||||
// Strict mode is meaningless without a contextId: the extension keys
|
||||
// strictness on the tab's cookieStoreId.
|
||||
if (result.strictMode && result.contextualIdentity == null) {
|
||||
result = result.copyWith(strictMode: false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool get usesTorProxy => proxyConnectionId is TorProxyConnectionId;
|
||||
@@ -144,6 +166,7 @@ class ContainerMetadata with FastEquatable {
|
||||
bypassGlobalProxy,
|
||||
useCustomColor,
|
||||
assignedSites,
|
||||
strictMode,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites);
|
||||
|
||||
ContainerMetadata strictMode(bool strictMode);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerMetadata(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
@@ -42,6 +44,7 @@ abstract class _$ContainerMetadataCWProxy {
|
||||
bool bypassGlobalProxy,
|
||||
bool useCustomColor,
|
||||
List<Uri>? assignedSites,
|
||||
bool strictMode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,6 +90,9 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
ContainerMetadata assignedSites(List<Uri>? assignedSites) =>
|
||||
call(assignedSites: assignedSites);
|
||||
|
||||
@override
|
||||
ContainerMetadata strictMode(bool strictMode) => call(strictMode: strictMode);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContainerMetadata(...).copyWith.fieldName(value)`.
|
||||
@@ -105,6 +111,7 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
Object? bypassGlobalProxy = const $CopyWithPlaceholder(),
|
||||
Object? useCustomColor = const $CopyWithPlaceholder(),
|
||||
Object? assignedSites = const $CopyWithPlaceholder(),
|
||||
Object? strictMode = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return ContainerMetadata(
|
||||
iconData: iconData == const $CopyWithPlaceholder()
|
||||
@@ -153,6 +160,11 @@ class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy {
|
||||
? _value.assignedSites
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: assignedSites as List<Uri>?,
|
||||
strictMode:
|
||||
strictMode == const $CopyWithPlaceholder() || strictMode == null
|
||||
? _value.strictMode
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: strictMode as bool,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -295,6 +307,7 @@ ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
|
||||
assignedSites: (json['assignedSites'] as List<dynamic>?)
|
||||
?.map((e) => Uri.parse(e as String))
|
||||
.toList(),
|
||||
strictMode: json['strictMode'] as bool? ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ContainerMetadataToJson(
|
||||
@@ -312,6 +325,7 @@ Map<String, dynamic> _$ContainerMetadataToJson(
|
||||
'bypassGlobalProxy': instance.bypassGlobalProxy,
|
||||
'useCustomColor': instance.useCustomColor,
|
||||
'assignedSites': instance.assignedSites?.map((e) => e.toString()).toList(),
|
||||
'strictMode': instance.strictMode,
|
||||
};
|
||||
|
||||
Value? _$JsonConverterFromJson<Json, Value>(
|
||||
|
||||
@@ -190,6 +190,33 @@ Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
||||
return db.containerDao.allAssignedSites().watch();
|
||||
}
|
||||
|
||||
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||
/// ProxySettingsReplication.
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<Map<String, List<String>>> watchStrictContextAssignments(Ref ref) {
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
return db.containerDao.strictContextAssignments().watch().map((rows) {
|
||||
final assignments = <String, Set<String>>{};
|
||||
for (final row in rows) {
|
||||
final contextId = row.contextId;
|
||||
final assignmentContextId = row.assignmentContextId;
|
||||
if (contextId == null || assignmentContextId == null) continue;
|
||||
|
||||
assignments
|
||||
.putIfAbsent(contextId, () => <String>{})
|
||||
.add(assignmentContextId);
|
||||
}
|
||||
|
||||
return {
|
||||
for (final entry in assignments.entries)
|
||||
entry.key: entry.value.toList()..sort(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||
/// aliases for isolated contexts.
|
||||
|
||||
@@ -1146,6 +1146,66 @@ final class WatchAllAssignedSitesProvider
|
||||
String _$watchAllAssignedSitesHash() =>
|
||||
r'5f658b5733ee20192eb86d3aeb79aa9678dafba8';
|
||||
|
||||
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||
/// ProxySettingsReplication.
|
||||
|
||||
@ProviderFor(watchStrictContextAssignments)
|
||||
final watchStrictContextAssignmentsProvider =
|
||||
WatchStrictContextAssignmentsProvider._();
|
||||
|
||||
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||
/// ProxySettingsReplication.
|
||||
|
||||
final class WatchStrictContextAssignmentsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<Map<String, List<String>>>,
|
||||
Map<String, List<String>>,
|
||||
Stream<Map<String, List<String>>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<Map<String, List<String>>>,
|
||||
$StreamProvider<Map<String, List<String>>> {
|
||||
/// Strict-mode enforcement map: each Gecko cookie-store context that must be
|
||||
/// enforced (a strict container's base context, plus the isolation contexts of
|
||||
/// its isolated tabs) mapped to the base contextualIdentities its site
|
||||
/// assignments are keyed on. Replicated to the container-proxy extension by
|
||||
/// ProxySettingsReplication.
|
||||
WatchStrictContextAssignmentsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'watchStrictContextAssignmentsProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$watchStrictContextAssignmentsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<Map<String, List<String>>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<Map<String, List<String>>> create(Ref ref) {
|
||||
return watchStrictContextAssignments(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$watchStrictContextAssignmentsHash() =>
|
||||
r'01a92529a90baf955c96c35180a50ec21ff913a8';
|
||||
|
||||
/// Watches distinct (isolationContextId, containerId) pairs for isolated tabs
|
||||
/// assigned to containers. Used by ProxySettingsReplication to manage proxy
|
||||
/// aliases for isolated contexts.
|
||||
|
||||
+52
-24
@@ -108,6 +108,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
initialContainer.metadata.bypassGlobalProxy,
|
||||
);
|
||||
final assignedSites = useState(initialContainer.metadata.assignedSites);
|
||||
final strictMode = useState(initialContainer.metadata.strictMode);
|
||||
final isPinned = useState(initialContainer.isPinned);
|
||||
|
||||
final textController = useTextEditingController(
|
||||
@@ -141,6 +142,10 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
bypassGlobalProxy.value,
|
||||
useCustomColor: useCustomColor.value,
|
||||
assignedSites: assignedSites.value,
|
||||
// Strict mode requires a Gecko contextId (the extension keys
|
||||
// strictness on the tab's cookieStoreId). sanitized() enforces the
|
||||
// same invariant defensively on write.
|
||||
strictMode: strictMode.value && contextualIdentity.value != null,
|
||||
).sanitized(),
|
||||
);
|
||||
}
|
||||
@@ -593,31 +598,54 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
margin: EdgeInsets.zero,
|
||||
color: colorScheme.surfaceContainer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.web),
|
||||
title: const Text('Assigned Sites'),
|
||||
subtitle: assignedSiteCount > 0
|
||||
? Text(
|
||||
'$assignedSiteCount ${assignedSiteCount == 1 ? 'rule' : 'rules'} configured',
|
||||
)
|
||||
: const Text(
|
||||
'Route matching origins into this container',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
final result = await showDialog<Set<Uri>>(
|
||||
context: context,
|
||||
builder: (context) => ContainerSitesScreen(
|
||||
initialSites: assignedSites.value?.toSet() ?? {},
|
||||
),
|
||||
);
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.web),
|
||||
title: const Text('Assigned Sites'),
|
||||
subtitle: assignedSiteCount > 0
|
||||
? Text(
|
||||
'$assignedSiteCount ${assignedSiteCount == 1 ? 'rule' : 'rules'} configured',
|
||||
)
|
||||
: const Text(
|
||||
'Route matching origins into this container',
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
final result = await showDialog<Set<Uri>>(
|
||||
context: context,
|
||||
builder: (context) => ContainerSitesScreen(
|
||||
initialSites:
|
||||
assignedSites.value?.toSet() ?? {},
|
||||
),
|
||||
);
|
||||
|
||||
if (result == null || result.isEmpty) {
|
||||
assignedSites.value = null;
|
||||
} else {
|
||||
assignedSites.value = result.toList();
|
||||
}
|
||||
},
|
||||
if (result == null || result.isEmpty) {
|
||||
assignedSites.value = null;
|
||||
} else {
|
||||
assignedSites.value = result.toList();
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(height: 1, indent: 56),
|
||||
SwitchListTile.adaptive(
|
||||
value:
|
||||
contextualIdentity.value != null &&
|
||||
strictMode.value,
|
||||
title: const Text('Strict Mode'),
|
||||
subtitle: Text(
|
||||
contextualIdentity.value != null
|
||||
? 'Only allow assigned sites to load; block everything else'
|
||||
: 'Requires cookie isolation to be enabled',
|
||||
),
|
||||
secondary: const Icon(MdiIcons.shieldLockOutline),
|
||||
onChanged: (contextualIdentity.value != null)
|
||||
? (value) {
|
||||
strictMode.value = value;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -119,6 +119,10 @@ class ContainerProxyRepository extends _$ContainerProxyRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setStrictContexts(Map<String, List<String>> contexts) {
|
||||
return _runLocked(body: () => _service.setStrictContexts(contexts));
|
||||
}
|
||||
|
||||
/// Serialises [body] behind the service lock after the underlying Gecko
|
||||
/// proxy service reports healthy. Replaces the per-method boilerplate that
|
||||
/// each public mutator used to repeat.
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ContainerProxyRepositoryProvider
|
||||
}
|
||||
|
||||
String _$containerProxyRepositoryHash() =>
|
||||
r'9201bb0cdda570d38adc5fc16d1c7719f3d719a2';
|
||||
r'9d9190edcb06eb1335318172183e354a66626f57';
|
||||
|
||||
abstract class _$ContainerProxyRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -103,7 +103,9 @@ class MainApp extends HookConsumerWidget {
|
||||
child: _SyncEventListener(
|
||||
child: _SandboxCaptureErrorListener(
|
||||
child: _DownloadStoppedListener(
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
child: _StrictContainerBlockListener(
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -206,6 +208,42 @@ class _DownloadStoppedListener extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Surfaces a snackbar when the container-proxy extension cancels a navigation
|
||||
/// because the active tab's container is in strict mode (only assigned sites
|
||||
/// may load). Strict blocks carry no destination container — the load was
|
||||
/// already cancelled natively — so this listener only notifies the user.
|
||||
class _StrictContainerBlockListener extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const _StrictContainerBlockListener({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final siteAssignmentEvents = ref.watch(
|
||||
eventServiceProvider.select((service) => service.siteAssignementEvent),
|
||||
);
|
||||
|
||||
useOnStreamChange(
|
||||
siteAssignmentEvents,
|
||||
onData: (event) {
|
||||
if (!event.strict) {
|
||||
return;
|
||||
}
|
||||
|
||||
final host = Uri.tryParse(event.url)?.host;
|
||||
ui_helper.showInfoMessage(
|
||||
context,
|
||||
host != null && host.isNotEmpty
|
||||
? '$host is not assigned to this container'
|
||||
: 'This site is not assigned to this container',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
MediaQueryData applyAppMediaQueryOverrides({
|
||||
required MediaQueryData mediaQuery,
|
||||
required double uiScaleFactor,
|
||||
|
||||
Reference in New Issue
Block a user