diff --git a/app/lib/core/design/app_colors.dart b/app/lib/core/design/app_colors.dart index a293213b..61255c76 100644 --- a/app/lib/core/design/app_colors.dart +++ b/app/lib/core/design/app_colors.dart @@ -11,6 +11,7 @@ class AppColors extends ThemeExtension { this.torPurple = const Color(0xFF7D4698), this.torActiveGreen = const Color(0xFF68B030), this.torBackgroundGrey = const Color(0xFF333A41), + this.warningAmber = const Color(0xFFFFA000), }); final Color seedColor; @@ -21,6 +22,7 @@ class AppColors extends ThemeExtension { final Color torPurple; final Color torActiveGreen; final Color torBackgroundGrey; + final Color warningAmber; static const light = AppColors._(); static const dark = AppColors._(); @@ -35,6 +37,7 @@ class AppColors extends ThemeExtension { Color? torPurple, Color? torActiveGreen, Color? torBackgroundGrey, + Color? warningAmber, }) { return AppColors._( seedColor: seedColor ?? this.seedColor, @@ -46,6 +49,7 @@ class AppColors extends ThemeExtension { torPurple: torPurple ?? this.torPurple, torActiveGreen: torActiveGreen ?? this.torActiveGreen, torBackgroundGrey: torBackgroundGrey ?? this.torBackgroundGrey, + warningAmber: warningAmber ?? this.warningAmber, ); } @@ -84,6 +88,7 @@ class AppColors extends ThemeExtension { other.torBackgroundGrey, t, )!, + warningAmber: Color.lerp(warningAmber, other.warningAmber, t)!, ); } 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 index 819e5a75..b2ca35e9 100644 --- a/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.dart +++ b/app/lib/features/geckoview/features/browser/domain/repositories/site_permissions.dart @@ -4,6 +4,8 @@ import 'package:weblibre/features/geckoview/features/browser/domain/entities/sit part 'site_permissions.g.dart'; +typedef PermissionUpdater = SitePermissions Function(SitePermissionsWrapper); + @Riverpod(keepAlive: true) class SitePermissionsRepository extends _$SitePermissionsRepository { final _api = GeckoSitePermissionsApi(); @@ -15,9 +17,7 @@ class SitePermissionsRepository extends _$SitePermissionsRepository { return permissions; } - Future updatePermission( - SitePermissions Function(SitePermissionsWrapper) updater, - ) async { + Future updatePermission(PermissionUpdater updater) async { final currentPermissions = await getPermissions() ?? SitePermissions( @@ -48,7 +48,8 @@ class SitePermissionsRepository extends _$SitePermissionsRepository { Future build({ required String origin, required bool isPrivate, - }) { - return _api.getSitePermissions(origin, isPrivate); + }) async { + final permissions = await _api.getSitePermissions(origin, isPrivate); + return permissions; } } 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 index 713640ab..7e63f300 100644 --- 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 @@ -53,7 +53,7 @@ final class SitePermissionsRepositoryProvider } String _$sitePermissionsRepositoryHash() => - r'83033a26cd7c9473a7ac9a23f91787af8ed4ce86'; + r'b821fbd3a457f5b9cfc6c9d398c93f09c6b30b0b'; final class SitePermissionsRepositoryFamily extends $Family with diff --git a/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart new file mode 100644 index 00000000..4c4f796b --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart @@ -0,0 +1,110 @@ +/* + * 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 . + */ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/repositories/site_permissions.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart'; + +part 'site_settings_badge_provider.g.dart'; + +/// Provider that determines whether to show the site settings badge on the tab icon. +/// Returns true if any site-specific setting has been altered from defaults. +@Riverpod() +Future showSiteSettingsBadge(Ref ref) async { + final tabState = ref.watch(selectedTabStateProvider); + if (tabState == null) { + return false; + } + + // Check tracking protection exception + final hasTrackingException = await ref.watch( + hasTrackingProtectionExceptionProvider(tabState.id).future, + ); + + // Watch site permissions + final permissions = await ref.watch( + sitePermissionsRepositoryProvider( + origin: tabState.url.origin, + isPrivate: tabState.isPrivate, + ).future, + ); + + if (hasTrackingException) { + return true; + } + // Check for altered permissions + if (_hasAlteredPermissions(permissions)) { + return true; + } + // Check for altered autoplay settings + if (_hasAlteredAutoplay(permissions)) { + return true; + } + + return false; +} + +/// Checks if any permission has been explicitly set to allowed or blocked +bool _hasAlteredPermissions(SitePermissions? permissions) { + if (permissions == null) { + return false; + } + + // Check all permission fields + final permissionStatuses = [ + permissions.camera, + permissions.microphone, + permissions.location, + permissions.notification, + permissions.persistentStorage, + permissions.crossOriginStorageAccess, + permissions.mediaKeySystemAccess, + permissions.localDeviceAccess, + permissions.localNetworkAccess, + ]; + + return permissionStatuses.any( + (status) => + status != null && + (status == SitePermissionStatus.allowed || + status == SitePermissionStatus.blocked), + ); +} + +/// Checks if autoplay settings differ from defaults +/// Default: autoplayAudible = blocked (null), autoplayInaudible = allowed (null) +bool _hasAlteredAutoplay(SitePermissions? permissions) { + if (permissions == null) { + return false; + } + + final audible = permissions.autoplayAudible; + final inaudible = permissions.autoplayInaudible; + + // Check if audible differs from default (null or blocked) + final audibleAltered = audible != null && audible != AutoplayStatus.blocked; + + // Check if inaudible differs from default (null or allowed) + final inaudibleAltered = + inaudible != null && inaudible != AutoplayStatus.allowed; + + return audibleAltered || inaudibleAltered; +} diff --git a/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.g.dart b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.g.dart new file mode 100644 index 00000000..a058a8f9 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'site_settings_badge_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Provider that determines whether to show the site settings badge on the tab icon. +/// Returns true if any site-specific setting has been altered from defaults. + +@ProviderFor(showSiteSettingsBadge) +final showSiteSettingsBadgeProvider = ShowSiteSettingsBadgeProvider._(); + +/// Provider that determines whether to show the site settings badge on the tab icon. +/// Returns true if any site-specific setting has been altered from defaults. + +final class ShowSiteSettingsBadgeProvider + extends $FunctionalProvider, bool, FutureOr> + with $FutureModifier, $FutureProvider { + /// Provider that determines whether to show the site settings badge on the tab icon. + /// Returns true if any site-specific setting has been altered from defaults. + ShowSiteSettingsBadgeProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'showSiteSettingsBadgeProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$showSiteSettingsBadgeHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return showSiteSettingsBadge(ref); + } +} + +String _$showSiteSettingsBadgeHash() => + r'8b759a67e5c8b0f49a5d38b793a3357576a2eb87'; diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart index fd206a11..9a2cd73b 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart @@ -28,6 +28,7 @@ import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; @@ -42,6 +43,9 @@ class AppBarTitle extends HookConsumerWidget { final tabState = ref.watch(selectedTabStateProvider); final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id)); + final showSiteSettingsBadge = ref.watch( + showSiteSettingsBadgeProvider.select((value) => value.value == true), + ); if (tabState == null) { return const SizedBox.shrink(); @@ -77,7 +81,25 @@ class AppBarTitle extends HookConsumerWidget { .read(bottomSheetControllerProvider.notifier) .show(SiteSettingsSheet(tabState: tabState)); }, - child: TabIcon(tabState: tabState, iconSize: 24), + child: Padding( + padding: const EdgeInsets.only(right: 4.0), + child: Stack( + clipBehavior: Clip.none, + children: [ + TabIcon(tabState: tabState, iconSize: 24), + if (showSiteSettingsBadge) + Positioned( + top: -4, + right: -4, + child: Icon( + MdiIcons.shieldHalfFull, + size: 10, + color: appColors.warningAmber, + ), + ), + ], + ), + ), ), // Title/URL tap → opens search screen Expanded( 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 478149f0..2deda290 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 @@ -77,9 +77,7 @@ class _PermissionsList extends HookConsumerWidget { final showAll = useState(false); // Memoize the update callback to avoid recreating closures - final updatePermission = useCallback(( - SitePermissions Function(SitePermissionsWrapper) updater, - ) async { + Future updatePermission(PermissionUpdater updater) async { await ref .read( sitePermissionsRepositoryProvider( @@ -89,7 +87,7 @@ class _PermissionsList extends HookConsumerWidget { ) .updatePermission(updater); await ref.read(selectedTabSessionProvider).reload(); - }, [origin, isPrivate]); + } // Build permission entries - only recalculates when permissions change final allPermissions = useMemoized( @@ -104,7 +102,7 @@ class _PermissionsList extends HookConsumerWidget { ), ) .toList(), - [permissions, updatePermission], + [permissions], ); // Filter to only show permissions that have been explicitly set (not noDecision)