update permission shield behaviour

This commit is contained in:
Fabian Freund
2026-05-05 08:54:09 +02:00
parent 01c2cea856
commit e037ca0a9b
4 changed files with 123 additions and 83 deletions
@@ -27,13 +27,26 @@ import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode
part 'site_settings_badge_provider.g.dart'; part 'site_settings_badge_provider.g.dart';
/// Provider that determines whether to show the site settings badge on the tab icon. /// Indicates whether the site settings badge should be shown on the tab icon
/// Returns true if any site-specific setting has been altered from defaults. /// and, if so, which direction settings have been changed.
enum SiteSettingsBadgeState {
/// No per-site settings differ from defaults.
hidden,
/// At least one per-site setting is stricter than the default.
improved,
/// At least one per-site setting is looser than the default.
/// Tracking-protection exceptions also count as weakened.
weakened,
}
/// Provider that determines the site settings badge state on the tab icon.
@Riverpod() @Riverpod()
Future<bool> showSiteSettingsBadge(Ref ref) async { Future<SiteSettingsBadgeState> showSiteSettingsBadge(Ref ref) async {
final tabState = ref.watch(selectedTabStateProvider); final tabState = ref.watch(selectedTabStateProvider);
if (tabState == null) { if (tabState == null) {
return false; return SiteSettingsBadgeState.hidden;
} }
// Check tracking protection exception // Check tracking protection exception
@@ -42,7 +55,7 @@ Future<bool> showSiteSettingsBadge(Ref ref) async {
); );
if (!tabState.url.hasScheme || !tabState.url.isHttpOrHttps) { if (!tabState.url.hasScheme || !tabState.url.isHttpOrHttps) {
return false; return SiteSettingsBadgeState.hidden;
} }
// Watch site permissions // Watch site permissions
@@ -54,27 +67,24 @@ Future<bool> showSiteSettingsBadge(Ref ref) async {
); );
if (hasTrackingException) { if (hasTrackingException) {
return true; return SiteSettingsBadgeState.weakened;
}
// Check for altered permissions
if (_hasAlteredPermissions(permissions)) {
return true;
}
// Check for altered autoplay settings
if (_hasAlteredAutoplay(permissions)) {
return true;
} }
return false; final badgeState = _evaluateBadgeState(permissions);
return badgeState;
} }
/// Checks if any permission has been explicitly set to allowed or blocked /// Evaluates whether site permissions are default, improved, or weakened.
bool _hasAlteredPermissions(SitePermissions? permissions) { /// Weakened takes precedence over improved.
SiteSettingsBadgeState _evaluateBadgeState(SitePermissions? permissions) {
if (permissions == null) { if (permissions == null) {
return false; return SiteSettingsBadgeState.hidden;
} }
// Check all permission fields var hasImproved = false;
var hasWeakened = false;
// Evaluate permission-type fields
final permissionStatuses = [ final permissionStatuses = [
permissions.camera, permissions.camera,
permissions.microphone, permissions.microphone,
@@ -87,30 +97,40 @@ bool _hasAlteredPermissions(SitePermissions? permissions) {
permissions.localNetworkAccess, permissions.localNetworkAccess,
]; ];
return permissionStatuses.any( for (final status in permissionStatuses) {
(status) => if (status == null) {
status != null && continue;
(status == SitePermissionStatus.allowed || }
status == SitePermissionStatus.blocked), if (status == SitePermissionStatus.blocked) {
); hasImproved = true;
} } else if (status == SitePermissionStatus.allowed) {
hasWeakened = true;
/// Checks if autoplay settings differ from defaults }
/// Default: autoplayAudible = blocked (null), autoplayInaudible = allowed (null) // noDecision is the default -> no effect
bool _hasAlteredAutoplay(SitePermissions? permissions) {
if (permissions == null) {
return false;
} }
// Evaluate autoplay audible
final audible = permissions.autoplayAudible; final audible = permissions.autoplayAudible;
if (audible != null && audible != AutoplayStatus.blocked) {
hasWeakened = true;
}
// Evaluate autoplay inaudible
final inaudible = permissions.autoplayInaudible; final inaudible = permissions.autoplayInaudible;
if (inaudible != null) {
if (inaudible == AutoplayStatus.blocked) {
hasImproved = true;
} else if (inaudible != AutoplayStatus.allowed) {
hasWeakened = true;
}
}
// Check if audible differs from default (null or blocked) if (hasWeakened) {
final audibleAltered = audible != null && audible != AutoplayStatus.blocked; return SiteSettingsBadgeState.weakened;
}
if (hasImproved) {
return SiteSettingsBadgeState.improved;
}
// Check if inaudible differs from default (null or allowed) return SiteSettingsBadgeState.hidden;
final inaudibleAltered =
inaudible != null && inaudible != AutoplayStatus.allowed;
return audibleAltered || inaudibleAltered;
} }
@@ -8,20 +8,24 @@ part of 'site_settings_badge_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning // ignore_for_file: type=lint, type=warning
/// Provider that determines whether to show the site settings badge on the tab icon. /// Provider that determines the site settings badge state on the tab icon.
/// Returns true if any site-specific setting has been altered from defaults.
@ProviderFor(showSiteSettingsBadge) @ProviderFor(showSiteSettingsBadge)
final showSiteSettingsBadgeProvider = ShowSiteSettingsBadgeProvider._(); final showSiteSettingsBadgeProvider = ShowSiteSettingsBadgeProvider._();
/// Provider that determines whether to show the site settings badge on the tab icon. /// Provider that determines the site settings badge state on the tab icon.
/// Returns true if any site-specific setting has been altered from defaults.
final class ShowSiteSettingsBadgeProvider final class ShowSiteSettingsBadgeProvider
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>> extends
with $FutureModifier<bool>, $FutureProvider<bool> { $FunctionalProvider<
/// Provider that determines whether to show the site settings badge on the tab icon. AsyncValue<SiteSettingsBadgeState>,
/// Returns true if any site-specific setting has been altered from defaults. SiteSettingsBadgeState,
FutureOr<SiteSettingsBadgeState>
>
with
$FutureModifier<SiteSettingsBadgeState>,
$FutureProvider<SiteSettingsBadgeState> {
/// Provider that determines the site settings badge state on the tab icon.
ShowSiteSettingsBadgeProvider._() ShowSiteSettingsBadgeProvider._()
: super( : super(
from: null, from: null,
@@ -38,14 +42,15 @@ final class ShowSiteSettingsBadgeProvider
@$internal @$internal
@override @override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) => $FutureProviderElement<SiteSettingsBadgeState> $createElement(
$FutureProviderElement(pointer); $ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override @override
FutureOr<bool> create(Ref ref) { FutureOr<SiteSettingsBadgeState> create(Ref ref) {
return showSiteSettingsBadge(ref); return showSiteSettingsBadge(ref);
} }
} }
String _$showSiteSettingsBadgeHash() => String _$showSiteSettingsBadgeHash() =>
r'c68e3b37b6f25e02779123b3f642a58dc98ba3a7'; r'fbfe409cdd6656d5cd26e377a68acde6ff4294fa';
@@ -46,8 +46,10 @@ class CompactAppBarTitle extends ConsumerWidget {
final selectedTabType = ref.watch(selectedTabTypeProvider); final selectedTabType = ref.watch(selectedTabTypeProvider);
final settings = ref.watch(generalSettingsWithDefaultsProvider); final settings = ref.watch(generalSettingsWithDefaultsProvider);
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id)); final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
final showSiteSettingsBadge = ref.watch( final siteSettingsBadgeState = ref.watch(
showSiteSettingsBadgeProvider.select((value) => value.value == true), showSiteSettingsBadgeProvider.select(
(value) => value.value ?? SiteSettingsBadgeState.hidden,
),
); );
if (tabState == null) { if (tabState == null) {
@@ -64,7 +66,7 @@ class CompactAppBarTitle extends ConsumerWidget {
tabState: tabState, tabState: tabState,
isTabTunneled: isTabTunneled:
isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true, isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
showSiteSettingsBadge: showSiteSettingsBadge, siteSettingsBadgeState: siteSettingsBadgeState,
longPressUrlCopy: settings.tabBarLongPressUrlCopy, longPressUrlCopy: settings.tabBarLongPressUrlCopy,
onSiteSettingsTap: () { onSiteSettingsTap: () {
ref ref
@@ -87,7 +89,7 @@ class CompactAppBarTitleView extends StatelessWidget {
super.key, super.key,
required this.tabState, required this.tabState,
required this.isTabTunneled, required this.isTabTunneled,
required this.showSiteSettingsBadge, required this.siteSettingsBadgeState,
required this.onSiteSettingsTap, required this.onSiteSettingsTap,
required this.onTitleTap, required this.onTitleTap,
this.tabIcon, this.tabIcon,
@@ -96,7 +98,7 @@ class CompactAppBarTitleView extends StatelessWidget {
final TabState tabState; final TabState tabState;
final bool isTabTunneled; final bool isTabTunneled;
final bool showSiteSettingsBadge; final SiteSettingsBadgeState siteSettingsBadgeState;
final VoidCallback onSiteSettingsTap; final VoidCallback onSiteSettingsTap;
final VoidCallback onTitleTap; final VoidCallback onTitleTap;
final Widget? tabIcon; final Widget? tabIcon;
@@ -117,17 +119,22 @@ class CompactAppBarTitleView extends StatelessWidget {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
tabIcon ?? TabIcon(tabState: tabState, iconSize: 24), tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
Positioned( if (siteSettingsBadgeState != SiteSettingsBadgeState.hidden)
top: -4, Positioned(
right: -4, top: -4,
child: Icon( right: -4,
MdiIcons.shieldHalfFull, child: Icon(
size: 10, siteSettingsBadgeState == SiteSettingsBadgeState.improved
color: showSiteSettingsBadge ? MdiIcons.shield
? appColors.warningAmber : MdiIcons.shieldAlert,
: Colors.green, size: 10,
color:
siteSettingsBadgeState ==
SiteSettingsBadgeState.improved
? Colors.green
: appColors.warningAmber,
),
), ),
),
], ],
), ),
), ),
@@ -202,8 +209,10 @@ class AppBarTitle extends ConsumerWidget {
final selectedTabType = ref.watch(selectedTabTypeProvider); final selectedTabType = ref.watch(selectedTabTypeProvider);
final settings = ref.watch(generalSettingsWithDefaultsProvider); final settings = ref.watch(generalSettingsWithDefaultsProvider);
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id)); final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
final showSiteSettingsBadge = ref.watch( final siteSettingsBadgeState = ref.watch(
showSiteSettingsBadgeProvider.select((value) => value.value == true), showSiteSettingsBadgeProvider.select(
(value) => value.value ?? SiteSettingsBadgeState.hidden,
),
); );
if (tabState == null) { if (tabState == null) {
@@ -220,7 +229,7 @@ class AppBarTitle extends ConsumerWidget {
tabState: tabState, tabState: tabState,
isTabTunneled: isTabTunneled:
isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true, isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
showSiteSettingsBadge: showSiteSettingsBadge, siteSettingsBadgeState: siteSettingsBadgeState,
longPressUrlCopy: settings.tabBarLongPressUrlCopy, longPressUrlCopy: settings.tabBarLongPressUrlCopy,
onSiteSettingsTap: () { onSiteSettingsTap: () {
ref ref
@@ -243,7 +252,7 @@ class AppBarTitleView extends StatelessWidget {
super.key, super.key,
required this.tabState, required this.tabState,
required this.isTabTunneled, required this.isTabTunneled,
required this.showSiteSettingsBadge, required this.siteSettingsBadgeState,
required this.onSiteSettingsTap, required this.onSiteSettingsTap,
required this.onTitleTap, required this.onTitleTap,
required this.longPressUrlCopy, required this.longPressUrlCopy,
@@ -252,7 +261,7 @@ class AppBarTitleView extends StatelessWidget {
final TabState tabState; final TabState tabState;
final bool isTabTunneled; final bool isTabTunneled;
final bool showSiteSettingsBadge; final SiteSettingsBadgeState siteSettingsBadgeState;
final VoidCallback onSiteSettingsTap; final VoidCallback onSiteSettingsTap;
final VoidCallback onTitleTap; final VoidCallback onTitleTap;
final Widget? tabIcon; final Widget? tabIcon;
@@ -273,17 +282,22 @@ class AppBarTitleView extends StatelessWidget {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
tabIcon ?? TabIcon(tabState: tabState, iconSize: 24), tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
Positioned( if (siteSettingsBadgeState != SiteSettingsBadgeState.hidden)
top: -4, Positioned(
right: -4, top: -4,
child: Icon( right: -4,
MdiIcons.shieldHalfFull, child: Icon(
size: 10, siteSettingsBadgeState == SiteSettingsBadgeState.improved
color: showSiteSettingsBadge ? MdiIcons.shield
? appColors.warningAmber : MdiIcons.shieldAlert,
: Colors.green, size: 10,
color:
siteSettingsBadgeState ==
SiteSettingsBadgeState.improved
? Colors.green
: appColors.warningAmber,
),
), ),
),
], ],
), ),
), ),
@@ -24,6 +24,7 @@ import 'package:weblibre/features/geckoview/domain/entities/states/security.dart
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart'; import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_toolbar.dart'; import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_toolbar.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/site_settings_badge_provider.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
@@ -351,7 +352,7 @@ class _RegularPreviewTitle extends StatelessWidget {
return AppBarTitleView( return AppBarTitleView(
tabState: tabState, tabState: tabState,
isTabTunneled: false, isTabTunneled: false,
showSiteSettingsBadge: false, siteSettingsBadgeState: SiteSettingsBadgeState.hidden,
onSiteSettingsTap: _noop, onSiteSettingsTap: _noop,
onTitleTap: _noop, onTitleTap: _noop,
tabIcon: const Icon(MdiIcons.web, size: 24), tabIcon: const Icon(MdiIcons.web, size: 24),
@@ -370,7 +371,7 @@ class _CompactPreviewTitle extends StatelessWidget {
return CompactAppBarTitleView( return CompactAppBarTitleView(
tabState: tabState, tabState: tabState,
isTabTunneled: false, isTabTunneled: false,
showSiteSettingsBadge: false, siteSettingsBadgeState: SiteSettingsBadgeState.hidden,
onSiteSettingsTap: _noop, onSiteSettingsTap: _noop,
onTitleTap: _noop, onTitleTap: _noop,
tabIcon: const Icon(MdiIcons.web, size: 24), tabIcon: const Icon(MdiIcons.web, size: 24),