implement tracking protection per site
This commit is contained in:
@@ -64,6 +64,7 @@ import 'package:weblibre/features/settings/presentation/screens/privacy_security
|
||||
import 'package:weblibre/features/settings/presentation/screens/search_content_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart';
|
||||
import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart';
|
||||
import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart';
|
||||
|
||||
@@ -146,6 +146,11 @@ RouteBase get $settingsRoute => GoRouteData.$route(
|
||||
name: 'AddonCollectionRoute',
|
||||
factory: $AddonCollectionRoute._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'tracking_protection_exceptions',
|
||||
name: 'TrackingProtectionExceptionsRoute',
|
||||
factory: $TrackingProtectionExceptionsRoute._fromState,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -446,6 +451,28 @@ mixin $AddonCollectionRoute on GoRouteData {
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
mixin $TrackingProtectionExceptionsRoute on GoRouteData {
|
||||
static TrackingProtectionExceptionsRoute _fromState(GoRouterState state) =>
|
||||
TrackingProtectionExceptionsRoute();
|
||||
|
||||
@override
|
||||
String get location =>
|
||||
GoRouteData.$location('/settings/tracking_protection_exceptions');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
@override
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
@override
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
|
||||
@override
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
RouteBase get $browserRoute => GoRouteData.$route(
|
||||
path: '/browser',
|
||||
name: 'BrowserRoute',
|
||||
|
||||
@@ -71,6 +71,10 @@ part of 'routes.dart';
|
||||
name: 'AddonCollectionRoute',
|
||||
path: 'addon_collection',
|
||||
),
|
||||
TypedGoRoute<TrackingProtectionExceptionsRoute>(
|
||||
name: 'TrackingProtectionExceptionsRoute',
|
||||
path: 'tracking_protection_exceptions',
|
||||
),
|
||||
],
|
||||
)
|
||||
class SettingsRoute extends GoRouteData with $SettingsRoute {
|
||||
@@ -182,3 +186,11 @@ class WebEngineHardeningGroupRoute extends GoRouteData
|
||||
return WebEngineHardeningGroupScreen(groupName: group);
|
||||
}
|
||||
}
|
||||
|
||||
class TrackingProtectionExceptionsRoute extends GoRouteData
|
||||
with $TrackingProtectionExceptionsRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const TrackingProtectionExceptionsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 'tracking_protection.g.dart';
|
||||
|
||||
/// Repository for managing per-site Enhanced Tracking Protection exceptions
|
||||
///
|
||||
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
|
||||
/// automatically after mutations.
|
||||
@Riverpod(keepAlive: true)
|
||||
class TrackingProtectionRepository extends _$TrackingProtectionRepository {
|
||||
final _api = GeckoTrackingProtectionApi();
|
||||
|
||||
/// Check if a tab has a tracking protection exception
|
||||
///
|
||||
/// Returns true if ETP is disabled for this site
|
||||
Future<bool> containsException(String tabId) {
|
||||
return _api.containsException(tabId);
|
||||
}
|
||||
|
||||
/// Add tracking protection exception for a tab (disable ETP for this site)
|
||||
Future<void> addException(String tabId) async {
|
||||
await _api.addException(tabId);
|
||||
await _invalidateWithDelay();
|
||||
}
|
||||
|
||||
/// Remove tracking protection exception for a tab (enable ETP for this site)
|
||||
Future<void> removeException(String tabId) async {
|
||||
await _api.removeException(tabId);
|
||||
await _invalidateWithDelay();
|
||||
}
|
||||
|
||||
/// Remove a specific exception by URL and refresh the exceptions list
|
||||
Future<void> removeExceptionByUrl(String url) async {
|
||||
await _api.removeExceptionByUrl(url);
|
||||
await _invalidateWithDelay();
|
||||
}
|
||||
|
||||
/// Remove all tracking protection exceptions
|
||||
Future<void> removeAllExceptions() async {
|
||||
await _api.removeAllExceptions();
|
||||
await _invalidateWithDelay();
|
||||
}
|
||||
|
||||
/// Helper method to invalidate repository state after mutations with delay
|
||||
Future<void> _invalidateWithDelay() async {
|
||||
await Future.delayed(const Duration(milliseconds: 100)).whenComplete(() {
|
||||
if (ref.mounted) {
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TrackingProtectionException>> build() {
|
||||
return _api.fetchExceptions();
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tracking_protection.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Repository for managing per-site Enhanced Tracking Protection exceptions
|
||||
///
|
||||
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
|
||||
/// automatically after mutations.
|
||||
|
||||
@ProviderFor(TrackingProtectionRepository)
|
||||
final trackingProtectionRepositoryProvider =
|
||||
TrackingProtectionRepositoryProvider._();
|
||||
|
||||
/// Repository for managing per-site Enhanced Tracking Protection exceptions
|
||||
///
|
||||
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
|
||||
/// automatically after mutations.
|
||||
final class TrackingProtectionRepositoryProvider
|
||||
extends
|
||||
$AsyncNotifierProvider<
|
||||
TrackingProtectionRepository,
|
||||
List<TrackingProtectionException>
|
||||
> {
|
||||
/// Repository for managing per-site Enhanced Tracking Protection exceptions
|
||||
///
|
||||
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
|
||||
/// automatically after mutations.
|
||||
TrackingProtectionRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'trackingProtectionRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$trackingProtectionRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
TrackingProtectionRepository create() => TrackingProtectionRepository();
|
||||
}
|
||||
|
||||
String _$trackingProtectionRepositoryHash() =>
|
||||
r'6d4407034b2c67317d0a0e106002f1785fd86a34';
|
||||
|
||||
/// Repository for managing per-site Enhanced Tracking Protection exceptions
|
||||
///
|
||||
/// Wraps the GeckoTrackingProtectionApi and handles state invalidation
|
||||
/// automatically after mutations.
|
||||
|
||||
abstract class _$TrackingProtectionRepository
|
||||
extends $AsyncNotifier<List<TrackingProtectionException>> {
|
||||
FutureOr<List<TrackingProtectionException>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<
|
||||
AsyncValue<List<TrackingProtectionException>>,
|
||||
List<TrackingProtectionException>
|
||||
>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<List<TrackingProtectionException>>,
|
||||
List<TrackingProtectionException>
|
||||
>,
|
||||
AsyncValue<List<TrackingProtectionException>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
+3
-9
@@ -28,17 +28,14 @@ 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 {
|
||||
Future<String> publicSuffixPlusOne(Ref ref, String host) {
|
||||
final api = GeckoPublicSuffixListApi();
|
||||
return api.getPublicSuffixPlusOne(host);
|
||||
}
|
||||
@@ -49,10 +46,7 @@ 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,
|
||||
};
|
||||
return {ClearDataType.allSiteData, ClearDataType.authSessions};
|
||||
}
|
||||
|
||||
void toggle(ClearDataType type) {
|
||||
|
||||
+2
-2
@@ -68,7 +68,7 @@ final class SitePermissionsProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$sitePermissionsHash() => r'c605e9a1acb261ec46659431b5bb1e105b8aa2a1';
|
||||
String _$sitePermissionsHash() => r'785af0f938bfe53799fc8b6ee8be71015afbc86c';
|
||||
|
||||
/// Provider to fetch site permissions for a given origin
|
||||
|
||||
@@ -147,7 +147,7 @@ final class PublicSuffixPlusOneProvider
|
||||
}
|
||||
|
||||
String _$publicSuffixPlusOneHash() =>
|
||||
r'e259f66e3ebf278468223d8e25618eb9f67a3504';
|
||||
r'2871740bee033e634047b1374ae8dfad508f2cb2';
|
||||
|
||||
/// Provider to get the public suffix plus one (eTLD+1) for a host
|
||||
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
|
||||
|
||||
part 'tracking_protection_provider.g.dart';
|
||||
|
||||
/// Provider that checks if a specific tab has a tracking protection exception
|
||||
/// Automatically rebuilds when repository changes after mutations
|
||||
@Riverpod()
|
||||
Future<bool> hasTrackingProtectionException(Ref ref, String tabId) {
|
||||
ref.watch(trackingProtectionRepositoryProvider);
|
||||
|
||||
return ref
|
||||
.read(trackingProtectionRepositoryProvider.notifier)
|
||||
.containsException(tabId);
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tracking_protection_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provider that checks if a specific tab has a tracking protection exception
|
||||
/// Automatically rebuilds when repository changes after mutations
|
||||
|
||||
@ProviderFor(hasTrackingProtectionException)
|
||||
final hasTrackingProtectionExceptionProvider =
|
||||
HasTrackingProtectionExceptionFamily._();
|
||||
|
||||
/// Provider that checks if a specific tab has a tracking protection exception
|
||||
/// Automatically rebuilds when repository changes after mutations
|
||||
|
||||
final class HasTrackingProtectionExceptionProvider
|
||||
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
|
||||
with $FutureModifier<bool>, $FutureProvider<bool> {
|
||||
/// Provider that checks if a specific tab has a tracking protection exception
|
||||
/// Automatically rebuilds when repository changes after mutations
|
||||
HasTrackingProtectionExceptionProvider._({
|
||||
required HasTrackingProtectionExceptionFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'hasTrackingProtectionExceptionProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$hasTrackingProtectionExceptionHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'hasTrackingProtectionExceptionProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<bool> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return hasTrackingProtectionException(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is HasTrackingProtectionExceptionProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$hasTrackingProtectionExceptionHash() =>
|
||||
r'3c6c2d4a2c75d52c33211bc8da8191185fb6c542';
|
||||
|
||||
/// Provider that checks if a specific tab has a tracking protection exception
|
||||
/// Automatically rebuilds when repository changes after mutations
|
||||
|
||||
final class HasTrackingProtectionExceptionFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<bool>, String> {
|
||||
HasTrackingProtectionExceptionFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'hasTrackingProtectionExceptionProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider that checks if a specific tab has a tracking protection exception
|
||||
/// Automatically rebuilds when repository changes after mutations
|
||||
|
||||
HasTrackingProtectionExceptionProvider call(String tabId) =>
|
||||
HasTrackingProtectionExceptionProvider._(argument: tabId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'hasTrackingProtectionExceptionProvider';
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart';
|
||||
|
||||
/// Section widget displaying Enhanced Tracking Protection toggle
|
||||
class TrackingProtectionSection extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
|
||||
const TrackingProtectionSection({required this.tabId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hasExceptionAsync = ref.watch(
|
||||
hasTrackingProtectionExceptionProvider(tabId),
|
||||
);
|
||||
|
||||
return hasExceptionAsync.when(
|
||||
data: (hasException) => _TrackingProtectionTile(
|
||||
tabId: tabId,
|
||||
isEnabled: !hasException, // ETP enabled when NOT in exceptions
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (error, stack) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackingProtectionTile extends ConsumerWidget {
|
||||
final String tabId;
|
||||
final bool isEnabled;
|
||||
|
||||
const _TrackingProtectionTile({required this.tabId, required this.isEnabled});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return SwitchListTile.adaptive(
|
||||
value: isEnabled,
|
||||
onChanged: (enabled) => _toggleProtection(context, ref, enabled),
|
||||
title: const Text('Enhanced Tracking Protection'),
|
||||
subtitle: Text(
|
||||
isEnabled
|
||||
? 'Trackers on this site are being blocked'
|
||||
: 'Trackers on this site are allowed',
|
||||
),
|
||||
secondary: Icon(
|
||||
isEnabled ? Icons.shield : Icons.shield_outlined,
|
||||
color: isEnabled
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggleProtection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
bool enabled,
|
||||
) async {
|
||||
try {
|
||||
if (enabled) {
|
||||
// Remove exception to enable ETP
|
||||
await ref
|
||||
.read(trackingProtectionRepositoryProvider.notifier)
|
||||
.removeException(tabId);
|
||||
} else {
|
||||
// Add exception to disable ETP
|
||||
await ref
|
||||
.read(trackingProtectionRepositoryProvider.notifier)
|
||||
.addException(tabId);
|
||||
}
|
||||
|
||||
// Reload tab to apply changes
|
||||
await ref.read(selectedTabSessionProvider).reload();
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
showErrorMessage(context, 'Failed to toggle tracking protection: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
|
||||
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/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart';
|
||||
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
|
||||
|
||||
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
|
||||
@@ -208,6 +209,11 @@ class ViewTabSheetWidget extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Tracking Protection Section
|
||||
TrackingProtectionSection(
|
||||
tabId: initialTabState.id,
|
||||
),
|
||||
const Divider(),
|
||||
// Permissions Section
|
||||
PermissionsSection(
|
||||
origin: initialTabState.url.origin,
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
@@ -87,11 +88,27 @@ class _TrackingProtectionSection extends StatelessWidget {
|
||||
_EnhancedTrackingProtectionSection(),
|
||||
_BounceTrackingProtectionTile(),
|
||||
_QueryParameterStrippingSection(),
|
||||
_TrackingProtectionExceptionsTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TrackingProtectionExceptionsTile extends StatelessWidget {
|
||||
const _TrackingProtectionExceptionsTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: const Icon(MdiIcons.shieldOffOutline),
|
||||
title: const Text('Tracking Protection Exceptions'),
|
||||
subtitle: const Text('Sites where tracking protection is disabled'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => context.push('/settings/tracking_protection_exceptions'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConnectionSecuritySection extends StatelessWidget {
|
||||
const _ConnectionSecuritySection();
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart';
|
||||
|
||||
/// Screen to view and manage tracking protection exceptions
|
||||
///
|
||||
/// Shows list of all sites where ETP is disabled, with options
|
||||
/// to remove individual exceptions or remove all exceptions.
|
||||
class TrackingProtectionExceptionsScreen extends HookConsumerWidget {
|
||||
const TrackingProtectionExceptionsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final exceptionsAsync = ref.watch(trackingProtectionRepositoryProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Tracking Protection Exceptions'),
|
||||
actions: [
|
||||
exceptionsAsync.maybeWhen(
|
||||
data: (exceptions) => exceptions.isNotEmpty
|
||||
? MenuAnchor(
|
||||
builder: (context, controller, child) => IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
),
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.delete_sweep),
|
||||
onPressed: () => _showDeleteAllDialog(context, ref),
|
||||
child: const Text('Delete All'),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: exceptionsAsync.when(
|
||||
data: (exceptions) {
|
||||
if (exceptions.isEmpty) {
|
||||
return const _EmptyState();
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: exceptions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final exception = exceptions[index];
|
||||
return _ExceptionTile(
|
||||
exception: exception,
|
||||
onDelete: () => _deleteException(context, ref, exception),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => _ErrorState(error: error.toString()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showDeleteAllDialog(BuildContext context, WidgetRef ref) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete All Exceptions?'),
|
||||
content: const Text(
|
||||
'This will re-enable tracking protection for all exception sites.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
try {
|
||||
await ref
|
||||
.read(trackingProtectionRepositoryProvider.notifier)
|
||||
.removeAllExceptions();
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
showErrorMessage(context, 'Failed to delete exceptions: $e');
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _deleteException(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
TrackingProtectionException exception,
|
||||
) async {
|
||||
try {
|
||||
await ref
|
||||
.read(trackingProtectionRepositoryProvider.notifier)
|
||||
.removeExceptionByUrl(exception.url);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
showErrorMessage(context, 'Failed to remove exception: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ExceptionTile extends StatelessWidget {
|
||||
final TrackingProtectionException exception;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const _ExceptionTile({required this.exception, required this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final uri = Uri.tryParse(exception.url);
|
||||
|
||||
return ListTile(
|
||||
leading: uri != null
|
||||
? UrlIcon([uri], iconSize: 24)
|
||||
: const Icon(MdiIcons.shieldOutline),
|
||||
title: Text(exception.url),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: onDelete,
|
||||
tooltip: 'Remove exception',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
const _EmptyState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.shield_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text('No exceptions', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Sites added to exceptions will appear here',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorState extends StatelessWidget {
|
||||
final String error;
|
||||
|
||||
const _ErrorState({required this.error});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 64),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Error loading exceptions',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user