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';
/// 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.
/// Indicates whether the site settings badge should be shown on the tab icon
/// 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()
Future<bool> showSiteSettingsBadge(Ref ref) async {
Future<SiteSettingsBadgeState> showSiteSettingsBadge(Ref ref) async {
final tabState = ref.watch(selectedTabStateProvider);
if (tabState == null) {
return false;
return SiteSettingsBadgeState.hidden;
}
// Check tracking protection exception
@@ -42,7 +55,7 @@ Future<bool> showSiteSettingsBadge(Ref ref) async {
);
if (!tabState.url.hasScheme || !tabState.url.isHttpOrHttps) {
return false;
return SiteSettingsBadgeState.hidden;
}
// Watch site permissions
@@ -54,27 +67,24 @@ Future<bool> showSiteSettingsBadge(Ref ref) async {
);
if (hasTrackingException) {
return true;
}
// Check for altered permissions
if (_hasAlteredPermissions(permissions)) {
return true;
}
// Check for altered autoplay settings
if (_hasAlteredAutoplay(permissions)) {
return true;
return SiteSettingsBadgeState.weakened;
}
return false;
final badgeState = _evaluateBadgeState(permissions);
return badgeState;
}
/// Checks if any permission has been explicitly set to allowed or blocked
bool _hasAlteredPermissions(SitePermissions? permissions) {
/// Evaluates whether site permissions are default, improved, or weakened.
/// Weakened takes precedence over improved.
SiteSettingsBadgeState _evaluateBadgeState(SitePermissions? permissions) {
if (permissions == null) {
return false;
return SiteSettingsBadgeState.hidden;
}
// Check all permission fields
var hasImproved = false;
var hasWeakened = false;
// Evaluate permission-type fields
final permissionStatuses = [
permissions.camera,
permissions.microphone,
@@ -87,30 +97,40 @@ bool _hasAlteredPermissions(SitePermissions? permissions) {
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;
for (final status in permissionStatuses) {
if (status == null) {
continue;
}
if (status == SitePermissionStatus.blocked) {
hasImproved = true;
} else if (status == SitePermissionStatus.allowed) {
hasWeakened = true;
}
// noDecision is the default -> no effect
}
// Evaluate autoplay audible
final audible = permissions.autoplayAudible;
if (audible != null && audible != AutoplayStatus.blocked) {
hasWeakened = true;
}
// Evaluate autoplay inaudible
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)
final audibleAltered = audible != null && audible != AutoplayStatus.blocked;
if (hasWeakened) {
return SiteSettingsBadgeState.weakened;
}
if (hasImproved) {
return SiteSettingsBadgeState.improved;
}
// Check if inaudible differs from default (null or allowed)
final inaudibleAltered =
inaudible != null && inaudible != AutoplayStatus.allowed;
return audibleAltered || inaudibleAltered;
return SiteSettingsBadgeState.hidden;
}
@@ -8,20 +8,24 @@ part of 'site_settings_badge_provider.dart';
// 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.
/// Provider that determines the site settings badge state on the tab icon.
@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.
/// Provider that determines the site settings badge state on the tab icon.
final class ShowSiteSettingsBadgeProvider
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
with $FutureModifier<bool>, $FutureProvider<bool> {
/// 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.
extends
$FunctionalProvider<
AsyncValue<SiteSettingsBadgeState>,
SiteSettingsBadgeState,
FutureOr<SiteSettingsBadgeState>
>
with
$FutureModifier<SiteSettingsBadgeState>,
$FutureProvider<SiteSettingsBadgeState> {
/// Provider that determines the site settings badge state on the tab icon.
ShowSiteSettingsBadgeProvider._()
: super(
from: null,
@@ -38,14 +42,15 @@ final class ShowSiteSettingsBadgeProvider
@$internal
@override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
$FutureProviderElement<SiteSettingsBadgeState> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
FutureOr<SiteSettingsBadgeState> create(Ref ref) {
return showSiteSettingsBadge(ref);
}
}
String _$showSiteSettingsBadgeHash() =>
r'c68e3b37b6f25e02779123b3f642a58dc98ba3a7';
r'fbfe409cdd6656d5cd26e377a68acde6ff4294fa';
@@ -46,8 +46,10 @@ class CompactAppBarTitle extends ConsumerWidget {
final selectedTabType = ref.watch(selectedTabTypeProvider);
final settings = ref.watch(generalSettingsWithDefaultsProvider);
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
final showSiteSettingsBadge = ref.watch(
showSiteSettingsBadgeProvider.select((value) => value.value == true),
final siteSettingsBadgeState = ref.watch(
showSiteSettingsBadgeProvider.select(
(value) => value.value ?? SiteSettingsBadgeState.hidden,
),
);
if (tabState == null) {
@@ -64,7 +66,7 @@ class CompactAppBarTitle extends ConsumerWidget {
tabState: tabState,
isTabTunneled:
isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
showSiteSettingsBadge: showSiteSettingsBadge,
siteSettingsBadgeState: siteSettingsBadgeState,
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
onSiteSettingsTap: () {
ref
@@ -87,7 +89,7 @@ class CompactAppBarTitleView extends StatelessWidget {
super.key,
required this.tabState,
required this.isTabTunneled,
required this.showSiteSettingsBadge,
required this.siteSettingsBadgeState,
required this.onSiteSettingsTap,
required this.onTitleTap,
this.tabIcon,
@@ -96,7 +98,7 @@ class CompactAppBarTitleView extends StatelessWidget {
final TabState tabState;
final bool isTabTunneled;
final bool showSiteSettingsBadge;
final SiteSettingsBadgeState siteSettingsBadgeState;
final VoidCallback onSiteSettingsTap;
final VoidCallback onTitleTap;
final Widget? tabIcon;
@@ -117,17 +119,22 @@ class CompactAppBarTitleView extends StatelessWidget {
clipBehavior: Clip.none,
children: [
tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
Positioned(
top: -4,
right: -4,
child: Icon(
MdiIcons.shieldHalfFull,
size: 10,
color: showSiteSettingsBadge
? appColors.warningAmber
: Colors.green,
if (siteSettingsBadgeState != SiteSettingsBadgeState.hidden)
Positioned(
top: -4,
right: -4,
child: Icon(
siteSettingsBadgeState == SiteSettingsBadgeState.improved
? MdiIcons.shield
: MdiIcons.shieldAlert,
size: 10,
color:
siteSettingsBadgeState ==
SiteSettingsBadgeState.improved
? Colors.green
: appColors.warningAmber,
),
),
),
],
),
),
@@ -202,8 +209,10 @@ class AppBarTitle extends ConsumerWidget {
final selectedTabType = ref.watch(selectedTabTypeProvider);
final settings = ref.watch(generalSettingsWithDefaultsProvider);
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
final showSiteSettingsBadge = ref.watch(
showSiteSettingsBadgeProvider.select((value) => value.value == true),
final siteSettingsBadgeState = ref.watch(
showSiteSettingsBadgeProvider.select(
(value) => value.value ?? SiteSettingsBadgeState.hidden,
),
);
if (tabState == null) {
@@ -220,7 +229,7 @@ class AppBarTitle extends ConsumerWidget {
tabState: tabState,
isTabTunneled:
isTabTuneledAsync.hasValue && isTabTuneledAsync.value == true,
showSiteSettingsBadge: showSiteSettingsBadge,
siteSettingsBadgeState: siteSettingsBadgeState,
longPressUrlCopy: settings.tabBarLongPressUrlCopy,
onSiteSettingsTap: () {
ref
@@ -243,7 +252,7 @@ class AppBarTitleView extends StatelessWidget {
super.key,
required this.tabState,
required this.isTabTunneled,
required this.showSiteSettingsBadge,
required this.siteSettingsBadgeState,
required this.onSiteSettingsTap,
required this.onTitleTap,
required this.longPressUrlCopy,
@@ -252,7 +261,7 @@ class AppBarTitleView extends StatelessWidget {
final TabState tabState;
final bool isTabTunneled;
final bool showSiteSettingsBadge;
final SiteSettingsBadgeState siteSettingsBadgeState;
final VoidCallback onSiteSettingsTap;
final VoidCallback onTitleTap;
final Widget? tabIcon;
@@ -273,17 +282,22 @@ class AppBarTitleView extends StatelessWidget {
clipBehavior: Clip.none,
children: [
tabIcon ?? TabIcon(tabState: tabState, iconSize: 24),
Positioned(
top: -4,
right: -4,
child: Icon(
MdiIcons.shieldHalfFull,
size: 10,
color: showSiteSettingsBadge
? appColors.warningAmber
: Colors.green,
if (siteSettingsBadgeState != SiteSettingsBadgeState.hidden)
Positioned(
top: -4,
right: -4,
child: Icon(
siteSettingsBadgeState == SiteSettingsBadgeState.improved
? MdiIcons.shield
: MdiIcons.shieldAlert,
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/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/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/bottom_app_bar.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
@@ -351,7 +352,7 @@ class _RegularPreviewTitle extends StatelessWidget {
return AppBarTitleView(
tabState: tabState,
isTabTunneled: false,
showSiteSettingsBadge: false,
siteSettingsBadgeState: SiteSettingsBadgeState.hidden,
onSiteSettingsTap: _noop,
onTitleTap: _noop,
tabIcon: const Icon(MdiIcons.web, size: 24),
@@ -370,7 +371,7 @@ class _CompactPreviewTitle extends StatelessWidget {
return CompactAppBarTitleView(
tabState: tabState,
isTabTunneled: false,
showSiteSettingsBadge: false,
siteSettingsBadgeState: SiteSettingsBadgeState.hidden,
onSiteSettingsTap: _noop,
onTitleTap: _noop,
tabIcon: const Icon(MdiIcons.web, size: 24),