diff --git a/app/lib/features/geckoview/features/browser/domain/entities/site_permissions.dart b/app/lib/features/geckoview/features/browser/domain/entities/site_permissions.dart new file mode 100644 index 00000000..e9422939 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/entities/site_permissions.dart @@ -0,0 +1,41 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; + +part 'site_permissions.g.dart'; + +@CopyWith() +class SitePermissionsWrapper extends SitePermissions { + SitePermissionsWrapper({ + required super.origin, + super.camera, + super.microphone, + super.location, + super.notification, + super.persistentStorage, + super.crossOriginStorageAccess, + super.mediaKeySystemAccess, + super.localDeviceAccess, + super.localNetworkAccess, + super.autoplayAudible, + super.autoplayInaudible, + required super.savedAt, + }); + + factory SitePermissionsWrapper.fromPermission(SitePermissions permissions) { + return SitePermissionsWrapper( + origin: permissions.origin, + camera: permissions.camera, + microphone: permissions.microphone, + location: permissions.location, + notification: permissions.notification, + persistentStorage: permissions.persistentStorage, + crossOriginStorageAccess: permissions.crossOriginStorageAccess, + mediaKeySystemAccess: permissions.mediaKeySystemAccess, + localDeviceAccess: permissions.localDeviceAccess, + localNetworkAccess: permissions.localNetworkAccess, + autoplayAudible: permissions.autoplayAudible, + autoplayInaudible: permissions.autoplayInaudible, + savedAt: permissions.savedAt, + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/domain/entities/site_permissions.g.dart b/app/lib/features/geckoview/features/browser/domain/entities/site_permissions.g.dart new file mode 100644 index 00000000..9639dc05 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/entities/site_permissions.g.dart @@ -0,0 +1,220 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'site_permissions.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$SitePermissionsWrapperCWProxy { + SitePermissionsWrapper origin(String origin); + + SitePermissionsWrapper camera(SitePermissionStatus? camera); + + SitePermissionsWrapper microphone(SitePermissionStatus? microphone); + + SitePermissionsWrapper location(SitePermissionStatus? location); + + SitePermissionsWrapper notification(SitePermissionStatus? notification); + + SitePermissionsWrapper persistentStorage( + SitePermissionStatus? persistentStorage, + ); + + SitePermissionsWrapper crossOriginStorageAccess( + SitePermissionStatus? crossOriginStorageAccess, + ); + + SitePermissionsWrapper mediaKeySystemAccess( + SitePermissionStatus? mediaKeySystemAccess, + ); + + SitePermissionsWrapper localDeviceAccess( + SitePermissionStatus? localDeviceAccess, + ); + + SitePermissionsWrapper localNetworkAccess( + SitePermissionStatus? localNetworkAccess, + ); + + SitePermissionsWrapper autoplayAudible(AutoplayStatus? autoplayAudible); + + SitePermissionsWrapper autoplayInaudible(AutoplayStatus? autoplayInaudible); + + SitePermissionsWrapper savedAt(int savedAt); + + /// 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 `SitePermissionsWrapper(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// SitePermissionsWrapper(...).copyWith(id: 12, name: "My name") + /// ``` + SitePermissionsWrapper call({ + String origin, + SitePermissionStatus? camera, + SitePermissionStatus? microphone, + SitePermissionStatus? location, + SitePermissionStatus? notification, + SitePermissionStatus? persistentStorage, + SitePermissionStatus? crossOriginStorageAccess, + SitePermissionStatus? mediaKeySystemAccess, + SitePermissionStatus? localDeviceAccess, + SitePermissionStatus? localNetworkAccess, + AutoplayStatus? autoplayAudible, + AutoplayStatus? autoplayInaudible, + int savedAt, + }); +} + +/// Callable proxy for `copyWith` functionality. +/// Use as `instanceOfSitePermissionsWrapper.copyWith(...)` or call `instanceOfSitePermissionsWrapper.copyWith.fieldName(value)` for a single field. +class _$SitePermissionsWrapperCWProxyImpl + implements _$SitePermissionsWrapperCWProxy { + const _$SitePermissionsWrapperCWProxyImpl(this._value); + + final SitePermissionsWrapper _value; + + @override + SitePermissionsWrapper origin(String origin) => call(origin: origin); + + @override + SitePermissionsWrapper camera(SitePermissionStatus? camera) => + call(camera: camera); + + @override + SitePermissionsWrapper microphone(SitePermissionStatus? microphone) => + call(microphone: microphone); + + @override + SitePermissionsWrapper location(SitePermissionStatus? location) => + call(location: location); + + @override + SitePermissionsWrapper notification(SitePermissionStatus? notification) => + call(notification: notification); + + @override + SitePermissionsWrapper persistentStorage( + SitePermissionStatus? persistentStorage, + ) => call(persistentStorage: persistentStorage); + + @override + SitePermissionsWrapper crossOriginStorageAccess( + SitePermissionStatus? crossOriginStorageAccess, + ) => call(crossOriginStorageAccess: crossOriginStorageAccess); + + @override + SitePermissionsWrapper mediaKeySystemAccess( + SitePermissionStatus? mediaKeySystemAccess, + ) => call(mediaKeySystemAccess: mediaKeySystemAccess); + + @override + SitePermissionsWrapper localDeviceAccess( + SitePermissionStatus? localDeviceAccess, + ) => call(localDeviceAccess: localDeviceAccess); + + @override + SitePermissionsWrapper localNetworkAccess( + SitePermissionStatus? localNetworkAccess, + ) => call(localNetworkAccess: localNetworkAccess); + + @override + SitePermissionsWrapper autoplayAudible(AutoplayStatus? autoplayAudible) => + call(autoplayAudible: autoplayAudible); + + @override + SitePermissionsWrapper autoplayInaudible(AutoplayStatus? autoplayInaudible) => + call(autoplayInaudible: autoplayInaudible); + + @override + SitePermissionsWrapper savedAt(int savedAt) => call(savedAt: savedAt); + + @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 `SitePermissionsWrapper(...).copyWith.fieldName(value)`. + /// + /// Example: + /// ```dart + /// SitePermissionsWrapper(...).copyWith(id: 12, name: "My name") + /// ``` + SitePermissionsWrapper call({ + Object? origin = const $CopyWithPlaceholder(), + Object? camera = const $CopyWithPlaceholder(), + Object? microphone = const $CopyWithPlaceholder(), + Object? location = const $CopyWithPlaceholder(), + Object? notification = const $CopyWithPlaceholder(), + Object? persistentStorage = const $CopyWithPlaceholder(), + Object? crossOriginStorageAccess = const $CopyWithPlaceholder(), + Object? mediaKeySystemAccess = const $CopyWithPlaceholder(), + Object? localDeviceAccess = const $CopyWithPlaceholder(), + Object? localNetworkAccess = const $CopyWithPlaceholder(), + Object? autoplayAudible = const $CopyWithPlaceholder(), + Object? autoplayInaudible = const $CopyWithPlaceholder(), + Object? savedAt = const $CopyWithPlaceholder(), + }) { + return SitePermissionsWrapper( + origin: origin == const $CopyWithPlaceholder() || origin == null + ? _value.origin + // ignore: cast_nullable_to_non_nullable + : origin as String, + camera: camera == const $CopyWithPlaceholder() + ? _value.camera + // ignore: cast_nullable_to_non_nullable + : camera as SitePermissionStatus?, + microphone: microphone == const $CopyWithPlaceholder() + ? _value.microphone + // ignore: cast_nullable_to_non_nullable + : microphone as SitePermissionStatus?, + location: location == const $CopyWithPlaceholder() + ? _value.location + // ignore: cast_nullable_to_non_nullable + : location as SitePermissionStatus?, + notification: notification == const $CopyWithPlaceholder() + ? _value.notification + // ignore: cast_nullable_to_non_nullable + : notification as SitePermissionStatus?, + persistentStorage: persistentStorage == const $CopyWithPlaceholder() + ? _value.persistentStorage + // ignore: cast_nullable_to_non_nullable + : persistentStorage as SitePermissionStatus?, + crossOriginStorageAccess: + crossOriginStorageAccess == const $CopyWithPlaceholder() + ? _value.crossOriginStorageAccess + // ignore: cast_nullable_to_non_nullable + : crossOriginStorageAccess as SitePermissionStatus?, + mediaKeySystemAccess: mediaKeySystemAccess == const $CopyWithPlaceholder() + ? _value.mediaKeySystemAccess + // ignore: cast_nullable_to_non_nullable + : mediaKeySystemAccess as SitePermissionStatus?, + localDeviceAccess: localDeviceAccess == const $CopyWithPlaceholder() + ? _value.localDeviceAccess + // ignore: cast_nullable_to_non_nullable + : localDeviceAccess as SitePermissionStatus?, + localNetworkAccess: localNetworkAccess == const $CopyWithPlaceholder() + ? _value.localNetworkAccess + // ignore: cast_nullable_to_non_nullable + : localNetworkAccess as SitePermissionStatus?, + autoplayAudible: autoplayAudible == const $CopyWithPlaceholder() + ? _value.autoplayAudible + // ignore: cast_nullable_to_non_nullable + : autoplayAudible as AutoplayStatus?, + autoplayInaudible: autoplayInaudible == const $CopyWithPlaceholder() + ? _value.autoplayInaudible + // ignore: cast_nullable_to_non_nullable + : autoplayInaudible as AutoplayStatus?, + savedAt: savedAt == const $CopyWithPlaceholder() || savedAt == null + ? _value.savedAt + // ignore: cast_nullable_to_non_nullable + : savedAt as int, + ); + } +} + +extension $SitePermissionsWrapperCopyWith on SitePermissionsWrapper { + /// Returns a callable class used to build a new instance with modified fields. + /// Example: `instanceOfSitePermissionsWrapper.copyWith(...)` or `instanceOfSitePermissionsWrapper.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$SitePermissionsWrapperCWProxy get copyWith => + _$SitePermissionsWrapperCWProxyImpl(this); +} diff --git a/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.dart b/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.dart new file mode 100644 index 00000000..819e5a75 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.dart @@ -0,0 +1,54 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/entities/site_permissions.dart'; + +part 'site_permissions.g.dart'; + +@Riverpod(keepAlive: true) +class SitePermissionsRepository extends _$SitePermissionsRepository { + final _api = GeckoSitePermissionsApi(); + + Future getPermissions() async { + final permissions = await _api.getSitePermissions(origin, isPrivate); + state = AsyncValue.data(permissions); + + return permissions; + } + + Future updatePermission( + SitePermissions Function(SitePermissionsWrapper) updater, + ) async { + final currentPermissions = + await getPermissions() ?? + SitePermissions( + origin: origin, + savedAt: DateTime.now().millisecondsSinceEpoch, + ); + + await setPermissions( + updater(SitePermissionsWrapper.fromPermission(currentPermissions)), + ); + } + + Future setPermissions(SitePermissions permissions) async { + if (permissions.origin != origin) { + throw Exception('Origin does not match'); + } + + await _api.setSitePermissions(permissions, isPrivate); + ref.invalidateSelf(); + } + + Future deletePermissions() async { + await _api.deleteSitePermissions(origin, isPrivate); + ref.invalidateSelf(); + } + + @override + Future build({ + required String origin, + required bool isPrivate, + }) { + return _api.getSitePermissions(origin, isPrivate); + } +} diff --git a/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.g.dart b/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.g.dart new file mode 100644 index 00000000..713640ab --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.g.dart @@ -0,0 +1,116 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'site_permissions.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(SitePermissionsRepository) +final sitePermissionsRepositoryProvider = SitePermissionsRepositoryFamily._(); + +final class SitePermissionsRepositoryProvider + extends + $AsyncNotifierProvider { + SitePermissionsRepositoryProvider._({ + required SitePermissionsRepositoryFamily super.from, + required ({String origin, bool isPrivate}) super.argument, + }) : super( + retry: null, + name: r'sitePermissionsRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$sitePermissionsRepositoryHash(); + + @override + String toString() { + return r'sitePermissionsRepositoryProvider' + '' + '$argument'; + } + + @$internal + @override + SitePermissionsRepository create() => SitePermissionsRepository(); + + @override + bool operator ==(Object other) { + return other is SitePermissionsRepositoryProvider && + other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$sitePermissionsRepositoryHash() => + r'83033a26cd7c9473a7ac9a23f91787af8ed4ce86'; + +final class SitePermissionsRepositoryFamily extends $Family + with + $ClassFamilyOverride< + SitePermissionsRepository, + AsyncValue, + SitePermissions?, + FutureOr, + ({String origin, bool isPrivate}) + > { + SitePermissionsRepositoryFamily._() + : super( + retry: null, + name: r'sitePermissionsRepositoryProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + SitePermissionsRepositoryProvider call({ + required String origin, + required bool isPrivate, + }) => SitePermissionsRepositoryProvider._( + argument: (origin: origin, isPrivate: isPrivate), + from: this, + ); + + @override + String toString() => r'sitePermissionsRepositoryProvider'; +} + +abstract class _$SitePermissionsRepository + extends $AsyncNotifier { + late final _$args = ref.$arg as ({String origin, bool isPrivate}); + String get origin => _$args.origin; + bool get isPrivate => _$args.isPrivate; + + FutureOr build({ + required String origin, + required bool isPrivate, + }); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref as $Ref, SitePermissions?>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, SitePermissions?>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate( + ref, + () => build(origin: _$args.origin, isPrivate: _$args.isPrivate), + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart b/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart index da740b18..6f793851 100644 --- a/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart @@ -51,7 +51,7 @@ final class TrackingProtectionRepositoryProvider } String _$trackingProtectionRepositoryHash() => - r'6d4407034b2c67317d0a0e106002f1785fd86a34'; + r'f4b09ed513da6bd5a9f4d01833a2dbba990f59de'; /// Repository for managing per-site Enhanced Tracking Protection exceptions /// diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart index af455db7..e5f45721 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart @@ -23,15 +23,13 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart'; +import 'package:weblibre/utils/ui_helper.dart'; /// Section widget for clearing site data class ClearSiteDataSection extends HookConsumerWidget { final Uri url; - const ClearSiteDataSection({ - required this.url, - super.key, - }); + const ClearSiteDataSection({required this.url, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -63,7 +61,9 @@ class ClearSiteDataSection extends HookConsumerWidget { type: ClearDataType.cookies, isSelected: selectedTypes.contains(ClearDataType.cookies), onChanged: (selected) { - ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.cookies); + ref + .read(selectedClearDataTypesProvider.notifier) + .toggle(ClearDataType.cookies); }, ), _DataTypeCheckbox( @@ -72,7 +72,9 @@ class ClearSiteDataSection extends HookConsumerWidget { type: ClearDataType.allCaches, isSelected: selectedTypes.contains(ClearDataType.allCaches), onChanged: (selected) { - ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.allCaches); + ref + .read(selectedClearDataTypesProvider.notifier) + .toggle(ClearDataType.allCaches); }, ), _DataTypeCheckbox( @@ -81,7 +83,9 @@ class ClearSiteDataSection extends HookConsumerWidget { type: ClearDataType.allSiteData, isSelected: selectedTypes.contains(ClearDataType.allSiteData), onChanged: (selected) { - ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.allSiteData); + ref + .read(selectedClearDataTypesProvider.notifier) + .toggle(ClearDataType.allSiteData); }, ), _DataTypeCheckbox( @@ -90,11 +94,16 @@ class ClearSiteDataSection extends HookConsumerWidget { type: ClearDataType.authSessions, isSelected: selectedTypes.contains(ClearDataType.authSessions), onChanged: (selected) { - ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.authSessions); + ref + .read(selectedClearDataTypesProvider.notifier) + .toggle(ClearDataType.authSessions); }, ), Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), child: SizedBox( width: double.infinity, child: FilledButton.icon( @@ -124,9 +133,7 @@ class ClearSiteDataSection extends HookConsumerWidget { ) async { final selectedTypes = ref.read(selectedClearDataTypesProvider); if (selectedTypes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Select at least one data type')), - ); + showErrorMessage(context, 'Select at least one data type'); return; } @@ -157,15 +164,11 @@ class ClearSiteDataSection extends HookConsumerWidget { try { await _clearData(ref, selectedTypes); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Site data cleared')), - ); + showInfoMessage(context, 'Site data cleared'); } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to clear site data: $e')), - ); + showErrorMessage(context, 'Failed to clear site data: $e'); } } finally { isClearing.value = false; @@ -192,7 +195,10 @@ class ClearSiteDataSection extends HookConsumerWidget { return '${labels.sublist(0, labels.length - 1).join(', ')}, and ${labels.last}'; } - Future _clearData(WidgetRef ref, Set selectedTypes) async { + Future _clearData( + WidgetRef ref, + Set selectedTypes, + ) async { final host = url.host; // Get base domain using PSL API (falls back to host on error) diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart index fba8b235..1cffdaaf 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart @@ -22,7 +22,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; -import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/entities/site_permissions.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/repositories/site_permissions.dart'; /// Section widget displaying site permissions with toggles class PermissionsSection extends HookConsumerWidget { @@ -38,7 +39,7 @@ class PermissionsSection extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final permissionsAsync = ref.watch( - sitePermissionsProvider(origin, isPrivate), + sitePermissionsRepositoryProvider(origin: origin, isPrivate: isPrivate), ); return permissionsAsync.when( @@ -80,50 +81,75 @@ class _PermissionsList extends HookConsumerWidget { icon: Icons.videocam, label: 'Camera', status: permissions?.camera, - onChanged: (status) => _updatePermission(ref, (p) => _copyWithCamera(p, status)), + onChanged: (status) => _updatePermission( + ref, + (permissions) => permissions.copyWith.camera(status), + ), ), _PermissionEntry( icon: Icons.mic, label: 'Microphone', status: permissions?.microphone, - onChanged: (status) => _updatePermission(ref, (p) => _copyWithMicrophone(p, status)), + onChanged: (status) => _updatePermission( + ref, + (permissions) => permissions.copyWith.microphone(status), + ), ), _PermissionEntry( icon: Icons.location_on, label: 'Location', status: permissions?.location, - onChanged: (status) => _updatePermission(ref, (p) => _copyWithLocation(p, status)), + onChanged: (status) => _updatePermission( + ref, + (permissions) => permissions.copyWith.location(status), + ), ), _PermissionEntry( icon: Icons.notifications, label: 'Notifications', status: permissions?.notification, - onChanged: (status) => _updatePermission(ref, (p) => _copyWithNotification(p, status)), + onChanged: (status) => _updatePermission( + ref, + (permissions) => permissions.copyWith.notification(status), + ), ), _PermissionEntry( icon: Icons.storage, label: 'Persistent Storage', status: permissions?.persistentStorage, - onChanged: (status) => _updatePermission(ref, (p) => _copyWithPersistentStorage(p, status)), + onChanged: (status) => _updatePermission( + ref, + (permissions) => permissions.copyWith.persistentStorage(status), + ), ), _PermissionEntry( icon: Icons.cookie, label: 'Cross-Origin Storage', status: permissions?.crossOriginStorageAccess, - onChanged: (status) => _updatePermission(ref, (p) => _copyWithCrossOriginStorage(p, status)), + onChanged: (status) => _updatePermission( + ref, + (permissions) => + permissions.copyWith.crossOriginStorageAccess(status), + ), ), _PermissionEntry( icon: Icons.key, label: 'Media Key System (DRM)', status: permissions?.mediaKeySystemAccess, - onChanged: (status) => _updatePermission(ref, (p) => _copyWithMediaKeySystem(p, status)), + onChanged: (status) => _updatePermission( + ref, + (permissions) => permissions.copyWith.mediaKeySystemAccess(status), + ), ), ]; // Filter to only show permissions that have been explicitly set (not noDecision) - final setPermissions = allPermissions.where( - (p) => p.status != null && p.status != SitePermissionStatus.noDecision, - ).toList(); + final setPermissions = allPermissions + .where( + (p) => + p.status != null && p.status != SitePermissionStatus.noDecision, + ) + .toList(); final permissionsToShow = showAll.value ? allPermissions : setPermissions; final hiddenCount = allPermissions.length - setPermissions.length; @@ -151,15 +177,20 @@ class _PermissionsList extends HookConsumerWidget { ], ), ), - ...permissionsToShow.map((entry) => _PermissionTile( - icon: entry.icon, - label: entry.label, - status: entry.status, - onChanged: entry.onChanged, - )), + ...permissionsToShow.map( + (entry) => _PermissionTile( + icon: entry.icon, + label: entry.label, + status: entry.status, + onChanged: entry.onChanged, + ), + ), if (permissionsToShow.isEmpty && !showAll.value) Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), child: Text( 'No permissions set for this site', style: Theme.of(context).textTheme.bodyMedium?.copyWith( @@ -171,7 +202,13 @@ class _PermissionsList extends HookConsumerWidget { _AutoplayTile( audibleStatus: permissions?.autoplayAudible, inaudibleStatus: permissions?.autoplayInaudible, - onChanged: (audible, inaudible) => _updateAutoplay(ref, audible, inaudible), + onChanged: (audible, inaudible) => _updatePermission( + ref, + (permissions) => permissions.copyWith( + autoplayAudible: audible, + autoplayInaudible: inaudible, + ), + ), ), ], ); @@ -179,184 +216,20 @@ class _PermissionsList extends HookConsumerWidget { Future _updatePermission( WidgetRef ref, - SitePermissions Function(SitePermissions) updater, + SitePermissions Function(SitePermissionsWrapper) updater, ) async { - final currentPermissions = permissions ?? SitePermissions( - origin: origin, - savedAt: DateTime.now().millisecondsSinceEpoch, - ); - - final updatedPermissions = updater(currentPermissions); - - final api = GeckoSitePermissionsApi(); - await api.setSitePermissions(updatedPermissions, isPrivate); - - // Invalidate the provider to refetch - ref.invalidate(sitePermissionsProvider(origin, isPrivate)); + await ref + .read( + sitePermissionsRepositoryProvider( + origin: origin, + isPrivate: isPrivate, + ).notifier, + ) + .updatePermission(updater); // Reload the tab to apply changes await ref.read(selectedTabSessionProvider).reload(); } - - Future _updateAutoplay( - WidgetRef ref, - AutoplayStatus? audible, - AutoplayStatus? inaudible, - ) async { - final currentPermissions = permissions ?? SitePermissions( - origin: origin, - savedAt: DateTime.now().millisecondsSinceEpoch, - ); - - final updatedPermissions = SitePermissions( - origin: currentPermissions.origin, - camera: currentPermissions.camera, - microphone: currentPermissions.microphone, - location: currentPermissions.location, - notification: currentPermissions.notification, - persistentStorage: currentPermissions.persistentStorage, - crossOriginStorageAccess: currentPermissions.crossOriginStorageAccess, - mediaKeySystemAccess: currentPermissions.mediaKeySystemAccess, - localDeviceAccess: currentPermissions.localDeviceAccess, - localNetworkAccess: currentPermissions.localNetworkAccess, - autoplayAudible: audible ?? currentPermissions.autoplayAudible, - autoplayInaudible: inaudible ?? currentPermissions.autoplayInaudible, - savedAt: currentPermissions.savedAt, - ); - - final api = GeckoSitePermissionsApi(); - await api.setSitePermissions(updatedPermissions, isPrivate); - - ref.invalidate(sitePermissionsProvider(origin, isPrivate)); - await ref.read(selectedTabSessionProvider).reload(); - } - - // Copy helper methods since Pigeon doesn't generate copyWith - SitePermissions _copyWithCamera(SitePermissions p, SitePermissionStatus status) { - return SitePermissions( - origin: p.origin, - camera: status, - microphone: p.microphone, - location: p.location, - notification: p.notification, - persistentStorage: p.persistentStorage, - crossOriginStorageAccess: p.crossOriginStorageAccess, - mediaKeySystemAccess: p.mediaKeySystemAccess, - localDeviceAccess: p.localDeviceAccess, - localNetworkAccess: p.localNetworkAccess, - autoplayAudible: p.autoplayAudible, - autoplayInaudible: p.autoplayInaudible, - savedAt: p.savedAt, - ); - } - - SitePermissions _copyWithMicrophone(SitePermissions p, SitePermissionStatus status) { - return SitePermissions( - origin: p.origin, - camera: p.camera, - microphone: status, - location: p.location, - notification: p.notification, - persistentStorage: p.persistentStorage, - crossOriginStorageAccess: p.crossOriginStorageAccess, - mediaKeySystemAccess: p.mediaKeySystemAccess, - localDeviceAccess: p.localDeviceAccess, - localNetworkAccess: p.localNetworkAccess, - autoplayAudible: p.autoplayAudible, - autoplayInaudible: p.autoplayInaudible, - savedAt: p.savedAt, - ); - } - - SitePermissions _copyWithLocation(SitePermissions p, SitePermissionStatus status) { - return SitePermissions( - origin: p.origin, - camera: p.camera, - microphone: p.microphone, - location: status, - notification: p.notification, - persistentStorage: p.persistentStorage, - crossOriginStorageAccess: p.crossOriginStorageAccess, - mediaKeySystemAccess: p.mediaKeySystemAccess, - localDeviceAccess: p.localDeviceAccess, - localNetworkAccess: p.localNetworkAccess, - autoplayAudible: p.autoplayAudible, - autoplayInaudible: p.autoplayInaudible, - savedAt: p.savedAt, - ); - } - - SitePermissions _copyWithNotification(SitePermissions p, SitePermissionStatus status) { - return SitePermissions( - origin: p.origin, - camera: p.camera, - microphone: p.microphone, - location: p.location, - notification: status, - persistentStorage: p.persistentStorage, - crossOriginStorageAccess: p.crossOriginStorageAccess, - mediaKeySystemAccess: p.mediaKeySystemAccess, - localDeviceAccess: p.localDeviceAccess, - localNetworkAccess: p.localNetworkAccess, - autoplayAudible: p.autoplayAudible, - autoplayInaudible: p.autoplayInaudible, - savedAt: p.savedAt, - ); - } - - SitePermissions _copyWithPersistentStorage(SitePermissions p, SitePermissionStatus status) { - return SitePermissions( - origin: p.origin, - camera: p.camera, - microphone: p.microphone, - location: p.location, - notification: p.notification, - persistentStorage: status, - crossOriginStorageAccess: p.crossOriginStorageAccess, - mediaKeySystemAccess: p.mediaKeySystemAccess, - localDeviceAccess: p.localDeviceAccess, - localNetworkAccess: p.localNetworkAccess, - autoplayAudible: p.autoplayAudible, - autoplayInaudible: p.autoplayInaudible, - savedAt: p.savedAt, - ); - } - - SitePermissions _copyWithCrossOriginStorage(SitePermissions p, SitePermissionStatus status) { - return SitePermissions( - origin: p.origin, - camera: p.camera, - microphone: p.microphone, - location: p.location, - notification: p.notification, - persistentStorage: p.persistentStorage, - crossOriginStorageAccess: status, - mediaKeySystemAccess: p.mediaKeySystemAccess, - localDeviceAccess: p.localDeviceAccess, - localNetworkAccess: p.localNetworkAccess, - autoplayAudible: p.autoplayAudible, - autoplayInaudible: p.autoplayInaudible, - savedAt: p.savedAt, - ); - } - - SitePermissions _copyWithMediaKeySystem(SitePermissions p, SitePermissionStatus status) { - return SitePermissions( - origin: p.origin, - camera: p.camera, - microphone: p.microphone, - location: p.location, - notification: p.notification, - persistentStorage: p.persistentStorage, - crossOriginStorageAccess: p.crossOriginStorageAccess, - mediaKeySystemAccess: status, - localDeviceAccess: p.localDeviceAccess, - localNetworkAccess: p.localNetworkAccess, - autoplayAudible: p.autoplayAudible, - autoplayInaudible: p.autoplayInaudible, - savedAt: p.savedAt, - ); - } } class _PermissionEntry { @@ -475,9 +348,11 @@ class _AutoplayTile extends StatelessWidget { final audible = audibleStatus ?? AutoplayStatus.blocked; final inaudible = inaudibleStatus ?? AutoplayStatus.allowed; - if (audible == AutoplayStatus.allowed && inaudible == AutoplayStatus.allowed) { + if (audible == AutoplayStatus.allowed && + inaudible == AutoplayStatus.allowed) { return _AutoplayCombined.allowAll; - } else if (audible == AutoplayStatus.blocked && inaudible == AutoplayStatus.allowed) { + } else if (audible == AutoplayStatus.blocked && + inaudible == AutoplayStatus.allowed) { return _AutoplayCombined.blockAudible; } else { return _AutoplayCombined.blockAll; @@ -485,8 +360,4 @@ class _AutoplayTile extends StatelessWidget { } } -enum _AutoplayCombined { - allowAll, - blockAudible, - blockAll, -} +enum _AutoplayCombined { allowAll, blockAudible, blockAll } diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart index 851380fe..e82fd81a 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart @@ -22,17 +22,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'site_permissions_provider.g.dart'; -/// Provider to fetch site permissions for a given origin -@Riverpod() -Future sitePermissions( - Ref ref, - String origin, - bool isPrivate, -) { - final api = GeckoSitePermissionsApi(); - return api.getSitePermissions(origin, isPrivate); -} - /// Provider to get the public suffix plus one (eTLD+1) for a host @Riverpod() Future publicSuffixPlusOne(Ref ref, String host) { diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.g.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.g.dart index 858d481b..9849b3cd 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.g.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.g.dart @@ -8,90 +8,6 @@ part of 'site_permissions_provider.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -/// Provider to fetch site permissions for a given origin - -@ProviderFor(sitePermissions) -final sitePermissionsProvider = SitePermissionsFamily._(); - -/// Provider to fetch site permissions for a given origin - -final class SitePermissionsProvider - extends - $FunctionalProvider< - AsyncValue, - SitePermissions?, - FutureOr - > - with $FutureModifier, $FutureProvider { - /// Provider to fetch site permissions for a given origin - SitePermissionsProvider._({ - required SitePermissionsFamily super.from, - required (String, bool) super.argument, - }) : super( - retry: null, - name: r'sitePermissionsProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$sitePermissionsHash(); - - @override - String toString() { - return r'sitePermissionsProvider' - '' - '$argument'; - } - - @$internal - @override - $FutureProviderElement $createElement( - $ProviderPointer pointer, - ) => $FutureProviderElement(pointer); - - @override - FutureOr create(Ref ref) { - final argument = this.argument as (String, bool); - return sitePermissions(ref, argument.$1, argument.$2); - } - - @override - bool operator ==(Object other) { - return other is SitePermissionsProvider && other.argument == argument; - } - - @override - int get hashCode { - return argument.hashCode; - } -} - -String _$sitePermissionsHash() => r'785af0f938bfe53799fc8b6ee8be71015afbc86c'; - -/// Provider to fetch site permissions for a given origin - -final class SitePermissionsFamily extends $Family - with $FunctionalFamilyOverride, (String, bool)> { - SitePermissionsFamily._() - : super( - retry: null, - name: r'sitePermissionsProvider', - dependencies: null, - $allTransitiveDependencies: null, - isAutoDispose: true, - ); - - /// Provider to fetch site permissions for a given origin - - SitePermissionsProvider call(String origin, bool isPrivate) => - SitePermissionsProvider._(argument: (origin, isPrivate), from: this); - - @override - String toString() => r'sitePermissionsProvider'; -} - /// Provider to get the public suffix plus one (eTLD+1) for a host @ProviderFor(publicSuffixPlusOne)