initial
This commit is contained in:
+236
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
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';
|
||||
|
||||
/// Section widget for clearing site data
|
||||
class ClearSiteDataSection extends HookConsumerWidget {
|
||||
final Uri url;
|
||||
|
||||
const ClearSiteDataSection({
|
||||
required this.url,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isExpanded = useState(false);
|
||||
final isClearing = useState(false);
|
||||
final selectedTypes = ref.watch(selectedClearDataTypesProvider);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_sweep),
|
||||
title: const Text('Clear Site Data'),
|
||||
subtitle: Text(
|
||||
isExpanded.value
|
||||
? 'Select data types to clear'
|
||||
: 'Cookies, cache, and site data',
|
||||
),
|
||||
trailing: Icon(
|
||||
isExpanded.value ? Icons.expand_less : Icons.expand_more,
|
||||
),
|
||||
onTap: () => isExpanded.value = !isExpanded.value,
|
||||
),
|
||||
if (isExpanded.value) ...[
|
||||
_DataTypeCheckbox(
|
||||
label: 'Cookies',
|
||||
subtitle: 'Login tokens, preferences, tracking data',
|
||||
type: ClearDataType.cookies,
|
||||
isSelected: selectedTypes.contains(ClearDataType.cookies),
|
||||
onChanged: (selected) {
|
||||
ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.cookies);
|
||||
},
|
||||
),
|
||||
_DataTypeCheckbox(
|
||||
label: 'Cached Files',
|
||||
subtitle: 'Images, scripts, stylesheets',
|
||||
type: ClearDataType.allCaches,
|
||||
isSelected: selectedTypes.contains(ClearDataType.allCaches),
|
||||
onChanged: (selected) {
|
||||
ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.allCaches);
|
||||
},
|
||||
),
|
||||
_DataTypeCheckbox(
|
||||
label: 'Site Data',
|
||||
subtitle: 'Offline storage, databases, local files',
|
||||
type: ClearDataType.allSiteData,
|
||||
isSelected: selectedTypes.contains(ClearDataType.allSiteData),
|
||||
onChanged: (selected) {
|
||||
ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.allSiteData);
|
||||
},
|
||||
),
|
||||
_DataTypeCheckbox(
|
||||
label: 'Auth Sessions',
|
||||
subtitle: 'Saved logins, active sessions',
|
||||
type: ClearDataType.authSessions,
|
||||
isSelected: selectedTypes.contains(ClearDataType.authSessions),
|
||||
onChanged: (selected) {
|
||||
ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.authSessions);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: isClearing.value || selectedTypes.isEmpty
|
||||
? null
|
||||
: () => _showConfirmationAndClear(context, ref, isClearing),
|
||||
icon: isClearing.value
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.delete),
|
||||
label: Text(isClearing.value ? 'Clearing...' : 'Clear Now'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showConfirmationAndClear(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ValueNotifier<bool> isClearing,
|
||||
) async {
|
||||
final selectedTypes = ref.read(selectedClearDataTypesProvider);
|
||||
if (selectedTypes.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Select at least one data type')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
icon: const Icon(Icons.warning),
|
||||
title: const Text('Clear Site Data'),
|
||||
content: Text(
|
||||
'This will clear ${_formatTypes(selectedTypes)} for ${url.host}.\n\n'
|
||||
'You may need to log in again.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Clear'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true && context.mounted) {
|
||||
isClearing.value = true;
|
||||
try {
|
||||
await _clearData(ref, selectedTypes);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Site data cleared')),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to clear site data: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
isClearing.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTypes(Set<ClearDataType> types) {
|
||||
final labels = types.map((t) {
|
||||
switch (t) {
|
||||
case ClearDataType.cookies:
|
||||
return 'cookies';
|
||||
case ClearDataType.allCaches:
|
||||
return 'cached files';
|
||||
case ClearDataType.allSiteData:
|
||||
return 'site data';
|
||||
case ClearDataType.authSessions:
|
||||
return 'auth sessions';
|
||||
}
|
||||
}).toList();
|
||||
|
||||
if (labels.length == 1) return labels.first;
|
||||
if (labels.length == 2) return '${labels[0]} and ${labels[1]}';
|
||||
return '${labels.sublist(0, labels.length - 1).join(', ')}, and ${labels.last}';
|
||||
}
|
||||
|
||||
Future<void> _clearData(WidgetRef ref, Set<ClearDataType> selectedTypes) async {
|
||||
final host = url.host;
|
||||
|
||||
// Get base domain using PSL API (falls back to host on error)
|
||||
final pslApi = GeckoPublicSuffixListApi();
|
||||
final baseDomain = await pslApi.getPublicSuffixPlusOne(host);
|
||||
|
||||
// Clear data via API
|
||||
final clearApi = GeckoDeleteBrowsingDataController();
|
||||
await clearApi.clearDataForHost(baseDomain, selectedTypes.toList());
|
||||
|
||||
// Reload tab
|
||||
await ref.read(selectedTabSessionProvider).reload();
|
||||
}
|
||||
}
|
||||
|
||||
class _DataTypeCheckbox extends StatelessWidget {
|
||||
final String label;
|
||||
final String subtitle;
|
||||
final ClearDataType type;
|
||||
final bool isSelected;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
const _DataTypeCheckbox({
|
||||
required this.label,
|
||||
required this.subtitle,
|
||||
required this.type,
|
||||
required this.isSelected,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CheckboxListTile(
|
||||
title: Text(label),
|
||||
subtitle: Text(subtitle),
|
||||
value: isSelected,
|
||||
onChanged: (value) => onChanged(value ?? false),
|
||||
controlAffinity: ListTileControlAffinity.trailing,
|
||||
);
|
||||
}
|
||||
}
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
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';
|
||||
|
||||
/// Section widget displaying site permissions with toggles
|
||||
class PermissionsSection extends HookConsumerWidget {
|
||||
final String origin;
|
||||
final bool isPrivate;
|
||||
|
||||
const PermissionsSection({
|
||||
required this.origin,
|
||||
required this.isPrivate,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final permissionsAsync = ref.watch(
|
||||
sitePermissionsProvider(origin, isPrivate),
|
||||
);
|
||||
|
||||
return permissionsAsync.when(
|
||||
data: (permissions) => _PermissionsList(
|
||||
origin: origin,
|
||||
isPrivate: isPrivate,
|
||||
permissions: permissions,
|
||||
),
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (error, stack) => Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text('Error loading permissions: $error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PermissionsList extends HookConsumerWidget {
|
||||
final String origin;
|
||||
final bool isPrivate;
|
||||
final SitePermissions? permissions;
|
||||
|
||||
const _PermissionsList({
|
||||
required this.origin,
|
||||
required this.isPrivate,
|
||||
required this.permissions,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final showAll = useState(false);
|
||||
|
||||
// Build list of permission entries with their current status
|
||||
final allPermissions = [
|
||||
_PermissionEntry(
|
||||
icon: Icons.videocam,
|
||||
label: 'Camera',
|
||||
status: permissions?.camera,
|
||||
onChanged: (status) => _updatePermission(ref, (p) => _copyWithCamera(p, status)),
|
||||
),
|
||||
_PermissionEntry(
|
||||
icon: Icons.mic,
|
||||
label: 'Microphone',
|
||||
status: permissions?.microphone,
|
||||
onChanged: (status) => _updatePermission(ref, (p) => _copyWithMicrophone(p, status)),
|
||||
),
|
||||
_PermissionEntry(
|
||||
icon: Icons.location_on,
|
||||
label: 'Location',
|
||||
status: permissions?.location,
|
||||
onChanged: (status) => _updatePermission(ref, (p) => _copyWithLocation(p, status)),
|
||||
),
|
||||
_PermissionEntry(
|
||||
icon: Icons.notifications,
|
||||
label: 'Notifications',
|
||||
status: permissions?.notification,
|
||||
onChanged: (status) => _updatePermission(ref, (p) => _copyWithNotification(p, status)),
|
||||
),
|
||||
_PermissionEntry(
|
||||
icon: Icons.storage,
|
||||
label: 'Persistent Storage',
|
||||
status: permissions?.persistentStorage,
|
||||
onChanged: (status) => _updatePermission(ref, (p) => _copyWithPersistentStorage(p, status)),
|
||||
),
|
||||
_PermissionEntry(
|
||||
icon: Icons.cookie,
|
||||
label: 'Cross-Origin Storage',
|
||||
status: permissions?.crossOriginStorageAccess,
|
||||
onChanged: (status) => _updatePermission(ref, (p) => _copyWithCrossOriginStorage(p, status)),
|
||||
),
|
||||
_PermissionEntry(
|
||||
icon: Icons.key,
|
||||
label: 'Media Key System (DRM)',
|
||||
status: permissions?.mediaKeySystemAccess,
|
||||
onChanged: (status) => _updatePermission(ref, (p) => _copyWithMediaKeySystem(p, 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 permissionsToShow = showAll.value ? allPermissions : setPermissions;
|
||||
final hiddenCount = allPermissions.length - setPermissions.length;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'Permissions',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (hiddenCount > 0)
|
||||
TextButton(
|
||||
onPressed: () => showAll.value = !showAll.value,
|
||||
child: Text(showAll.value ? 'Show less' : 'Show all'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...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),
|
||||
child: Text(
|
||||
'No permissions set for this site',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
_AutoplayTile(
|
||||
audibleStatus: permissions?.autoplayAudible,
|
||||
inaudibleStatus: permissions?.autoplayInaudible,
|
||||
onChanged: (audible, inaudible) => _updateAutoplay(ref, audible, inaudible),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updatePermission(
|
||||
WidgetRef ref,
|
||||
SitePermissions Function(SitePermissions) 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));
|
||||
|
||||
// Reload the tab to apply changes
|
||||
await ref.read(selectedTabSessionProvider).reload();
|
||||
}
|
||||
|
||||
Future<void> _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 {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final SitePermissionStatus? status;
|
||||
final ValueChanged<SitePermissionStatus> onChanged;
|
||||
|
||||
_PermissionEntry({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.status,
|
||||
required this.onChanged,
|
||||
});
|
||||
}
|
||||
|
||||
class _PermissionTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final SitePermissionStatus? status;
|
||||
final ValueChanged<SitePermissionStatus> onChanged;
|
||||
|
||||
const _PermissionTile({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.status,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentStatus = status ?? SitePermissionStatus.noDecision;
|
||||
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
trailing: DropdownButton<SitePermissionStatus>(
|
||||
value: currentStatus,
|
||||
underline: const SizedBox(),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: SitePermissionStatus.noDecision,
|
||||
child: Text('Ask'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: SitePermissionStatus.allowed,
|
||||
child: Text('Allow'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: SitePermissionStatus.blocked,
|
||||
child: Text('Block'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
onChanged(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoplayTile extends StatelessWidget {
|
||||
final AutoplayStatus? audibleStatus;
|
||||
final AutoplayStatus? inaudibleStatus;
|
||||
final void Function(AutoplayStatus?, AutoplayStatus?) onChanged;
|
||||
|
||||
const _AutoplayTile({
|
||||
required this.audibleStatus,
|
||||
required this.inaudibleStatus,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Determine the combined autoplay setting
|
||||
final combinedStatus = _getCombinedStatus();
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.play_circle),
|
||||
title: const Text('Autoplay'),
|
||||
trailing: DropdownButton<_AutoplayCombined>(
|
||||
value: combinedStatus,
|
||||
underline: const SizedBox(),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: _AutoplayCombined.allowAll,
|
||||
child: Text('Allow All'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: _AutoplayCombined.blockAudible,
|
||||
child: Text('Block Audible'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: _AutoplayCombined.blockAll,
|
||||
child: Text('Block All'),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
switch (value) {
|
||||
case _AutoplayCombined.allowAll:
|
||||
onChanged(AutoplayStatus.allowed, AutoplayStatus.allowed);
|
||||
case _AutoplayCombined.blockAudible:
|
||||
onChanged(AutoplayStatus.blocked, AutoplayStatus.allowed);
|
||||
case _AutoplayCombined.blockAll:
|
||||
onChanged(AutoplayStatus.blocked, AutoplayStatus.blocked);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_AutoplayCombined _getCombinedStatus() {
|
||||
final audible = audibleStatus ?? AutoplayStatus.blocked;
|
||||
final inaudible = inaudibleStatus ?? AutoplayStatus.allowed;
|
||||
|
||||
if (audible == AutoplayStatus.allowed && inaudible == AutoplayStatus.allowed) {
|
||||
return _AutoplayCombined.allowAll;
|
||||
} else if (audible == AutoplayStatus.blocked && inaudible == AutoplayStatus.allowed) {
|
||||
return _AutoplayCombined.blockAudible;
|
||||
} else {
|
||||
return _AutoplayCombined.blockAll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum _AutoplayCombined {
|
||||
allowAll,
|
||||
blockAudible,
|
||||
blockAll,
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
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?> sitePermissions(
|
||||
Ref ref,
|
||||
String origin,
|
||||
bool isPrivate,
|
||||
) async {
|
||||
final api = GeckoSitePermissionsApi();
|
||||
return api.getSitePermissions(origin, isPrivate);
|
||||
}
|
||||
|
||||
/// Provider to get the public suffix plus one (eTLD+1) for a host
|
||||
@Riverpod()
|
||||
Future<String> publicSuffixPlusOne(
|
||||
Ref ref,
|
||||
String host,
|
||||
) async {
|
||||
final api = GeckoPublicSuffixListApi();
|
||||
return api.getPublicSuffixPlusOne(host);
|
||||
}
|
||||
|
||||
/// Notifier for managing selected clear data types
|
||||
@Riverpod()
|
||||
class SelectedClearDataTypes extends _$SelectedClearDataTypes {
|
||||
@override
|
||||
Set<ClearDataType> build() {
|
||||
// Default to clearing all site data and auth sessions (most common use case)
|
||||
return {
|
||||
ClearDataType.allSiteData,
|
||||
ClearDataType.authSessions,
|
||||
};
|
||||
}
|
||||
|
||||
void toggle(ClearDataType type) {
|
||||
if (state.contains(type)) {
|
||||
state = Set.from(state)..remove(type);
|
||||
} else {
|
||||
state = Set.from(state)..add(type);
|
||||
}
|
||||
}
|
||||
|
||||
void selectAll() {
|
||||
state = Set.from(ClearDataType.values);
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
state = {};
|
||||
}
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'site_permissions_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// 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?>,
|
||||
SitePermissions?,
|
||||
FutureOr<SitePermissions?>
|
||||
>
|
||||
with $FutureModifier<SitePermissions?>, $FutureProvider<SitePermissions?> {
|
||||
/// 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<SitePermissions?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<SitePermissions?> 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'c605e9a1acb261ec46659431b5bb1e105b8aa2a1';
|
||||
|
||||
/// Provider to fetch site permissions for a given origin
|
||||
|
||||
final class SitePermissionsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<SitePermissions?>, (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)
|
||||
final publicSuffixPlusOneProvider = PublicSuffixPlusOneFamily._();
|
||||
|
||||
/// Provider to get the public suffix plus one (eTLD+1) for a host
|
||||
|
||||
final class PublicSuffixPlusOneProvider
|
||||
extends $FunctionalProvider<AsyncValue<String>, String, FutureOr<String>>
|
||||
with $FutureModifier<String>, $FutureProvider<String> {
|
||||
/// Provider to get the public suffix plus one (eTLD+1) for a host
|
||||
PublicSuffixPlusOneProvider._({
|
||||
required PublicSuffixPlusOneFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'publicSuffixPlusOneProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$publicSuffixPlusOneHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'publicSuffixPlusOneProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<String> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<String> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return publicSuffixPlusOne(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PublicSuffixPlusOneProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$publicSuffixPlusOneHash() =>
|
||||
r'e259f66e3ebf278468223d8e25618eb9f67a3504';
|
||||
|
||||
/// Provider to get the public suffix plus one (eTLD+1) for a host
|
||||
|
||||
final class PublicSuffixPlusOneFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<String>, String> {
|
||||
PublicSuffixPlusOneFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'publicSuffixPlusOneProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider to get the public suffix plus one (eTLD+1) for a host
|
||||
|
||||
PublicSuffixPlusOneProvider call(String host) =>
|
||||
PublicSuffixPlusOneProvider._(argument: host, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'publicSuffixPlusOneProvider';
|
||||
}
|
||||
|
||||
/// Notifier for managing selected clear data types
|
||||
|
||||
@ProviderFor(SelectedClearDataTypes)
|
||||
final selectedClearDataTypesProvider = SelectedClearDataTypesProvider._();
|
||||
|
||||
/// Notifier for managing selected clear data types
|
||||
final class SelectedClearDataTypesProvider
|
||||
extends $NotifierProvider<SelectedClearDataTypes, Set<ClearDataType>> {
|
||||
/// Notifier for managing selected clear data types
|
||||
SelectedClearDataTypesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'selectedClearDataTypesProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$selectedClearDataTypesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
SelectedClearDataTypes create() => SelectedClearDataTypes();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Set<ClearDataType> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Set<ClearDataType>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$selectedClearDataTypesHash() =>
|
||||
r'f010b5dc724bd214add09f7f85c56dd615003915';
|
||||
|
||||
/// Notifier for managing selected clear data types
|
||||
|
||||
abstract class _$SelectedClearDataTypes extends $Notifier<Set<ClearDataType>> {
|
||||
Set<ClearDataType> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<Set<ClearDataType>, Set<ClearDataType>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<Set<ClearDataType>, Set<ClearDataType>>,
|
||||
Set<ClearDataType>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import 'package:weblibre/features/bangs/presentation/widgets/site_search.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart';
|
||||
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
|
||||
|
||||
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
|
||||
@@ -206,6 +208,17 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Permissions Section
|
||||
PermissionsSection(
|
||||
origin: initialTabState.url.origin,
|
||||
isPrivate: initialTabState.isPrivate,
|
||||
),
|
||||
const Divider(),
|
||||
// Clear Site Data Section
|
||||
ClearSiteDataSection(
|
||||
url: initialTabState.url,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user