add permissions badge

This commit is contained in:
Fabian Freund
2026-01-21 09:01:59 +01:00
parent d6e02f9d02
commit f648c43441
7 changed files with 199 additions and 12 deletions
+5
View File
@@ -11,6 +11,7 @@ class AppColors extends ThemeExtension<AppColors> {
this.torPurple = const Color(0xFF7D4698), this.torPurple = const Color(0xFF7D4698),
this.torActiveGreen = const Color(0xFF68B030), this.torActiveGreen = const Color(0xFF68B030),
this.torBackgroundGrey = const Color(0xFF333A41), this.torBackgroundGrey = const Color(0xFF333A41),
this.warningAmber = const Color(0xFFFFA000),
}); });
final Color seedColor; final Color seedColor;
@@ -21,6 +22,7 @@ class AppColors extends ThemeExtension<AppColors> {
final Color torPurple; final Color torPurple;
final Color torActiveGreen; final Color torActiveGreen;
final Color torBackgroundGrey; final Color torBackgroundGrey;
final Color warningAmber;
static const light = AppColors._(); static const light = AppColors._();
static const dark = AppColors._(); static const dark = AppColors._();
@@ -35,6 +37,7 @@ class AppColors extends ThemeExtension<AppColors> {
Color? torPurple, Color? torPurple,
Color? torActiveGreen, Color? torActiveGreen,
Color? torBackgroundGrey, Color? torBackgroundGrey,
Color? warningAmber,
}) { }) {
return AppColors._( return AppColors._(
seedColor: seedColor ?? this.seedColor, seedColor: seedColor ?? this.seedColor,
@@ -46,6 +49,7 @@ class AppColors extends ThemeExtension<AppColors> {
torPurple: torPurple ?? this.torPurple, torPurple: torPurple ?? this.torPurple,
torActiveGreen: torActiveGreen ?? this.torActiveGreen, torActiveGreen: torActiveGreen ?? this.torActiveGreen,
torBackgroundGrey: torBackgroundGrey ?? this.torBackgroundGrey, torBackgroundGrey: torBackgroundGrey ?? this.torBackgroundGrey,
warningAmber: warningAmber ?? this.warningAmber,
); );
} }
@@ -84,6 +88,7 @@ class AppColors extends ThemeExtension<AppColors> {
other.torBackgroundGrey, other.torBackgroundGrey,
t, t,
)!, )!,
warningAmber: Color.lerp(warningAmber, other.warningAmber, t)!,
); );
} }
@@ -4,6 +4,8 @@ import 'package:weblibre/features/geckoview/features/browser/domain/entities/sit
part 'site_permissions.g.dart'; part 'site_permissions.g.dart';
typedef PermissionUpdater = SitePermissions Function(SitePermissionsWrapper);
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SitePermissionsRepository extends _$SitePermissionsRepository { class SitePermissionsRepository extends _$SitePermissionsRepository {
final _api = GeckoSitePermissionsApi(); final _api = GeckoSitePermissionsApi();
@@ -15,9 +17,7 @@ class SitePermissionsRepository extends _$SitePermissionsRepository {
return permissions; return permissions;
} }
Future<void> updatePermission( Future<void> updatePermission(PermissionUpdater updater) async {
SitePermissions Function(SitePermissionsWrapper) updater,
) async {
final currentPermissions = final currentPermissions =
await getPermissions() ?? await getPermissions() ??
SitePermissions( SitePermissions(
@@ -48,7 +48,8 @@ class SitePermissionsRepository extends _$SitePermissionsRepository {
Future<SitePermissions?> build({ Future<SitePermissions?> build({
required String origin, required String origin,
required bool isPrivate, required bool isPrivate,
}) { }) async {
return _api.getSitePermissions(origin, isPrivate); final permissions = await _api.getSitePermissions(origin, isPrivate);
return permissions;
} }
} }
@@ -53,7 +53,7 @@ final class SitePermissionsRepositoryProvider
} }
String _$sitePermissionsRepositoryHash() => String _$sitePermissionsRepositoryHash() =>
r'83033a26cd7c9473a7ac9a23f91787af8ed4ce86'; r'b821fbd3a457f5b9cfc6c9d398c93f09c6b30b0b';
final class SitePermissionsRepositoryFamily extends $Family final class SitePermissionsRepositoryFamily extends $Family
with with
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<bool> 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;
}
@@ -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<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.
ShowSiteSettingsBadgeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'showSiteSettingsBadgeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$showSiteSettingsBadgeHash();
@$internal
@override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
return showSiteSettingsBadge(ref);
}
}
String _$showSiteSettingsBadgeHash() =>
r'8b759a67e5c8b0f49a5d38b793a3357576a2eb87';
@@ -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/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.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/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/tab_icon.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
@@ -42,6 +43,9 @@ class AppBarTitle extends HookConsumerWidget {
final tabState = ref.watch(selectedTabStateProvider); final tabState = ref.watch(selectedTabStateProvider);
final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id)); final isTabTuneledAsync = ref.watch(isTabTunneledProvider(tabState?.id));
final showSiteSettingsBadge = ref.watch(
showSiteSettingsBadgeProvider.select((value) => value.value == true),
);
if (tabState == null) { if (tabState == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -77,7 +81,25 @@ class AppBarTitle extends HookConsumerWidget {
.read(bottomSheetControllerProvider.notifier) .read(bottomSheetControllerProvider.notifier)
.show(SiteSettingsSheet(tabState: tabState)); .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 // Title/URL tap → opens search screen
Expanded( Expanded(
@@ -77,9 +77,7 @@ class _PermissionsList extends HookConsumerWidget {
final showAll = useState(false); final showAll = useState(false);
// Memoize the update callback to avoid recreating closures // Memoize the update callback to avoid recreating closures
final updatePermission = useCallback(( Future<void> updatePermission(PermissionUpdater updater) async {
SitePermissions Function(SitePermissionsWrapper) updater,
) async {
await ref await ref
.read( .read(
sitePermissionsRepositoryProvider( sitePermissionsRepositoryProvider(
@@ -89,7 +87,7 @@ class _PermissionsList extends HookConsumerWidget {
) )
.updatePermission(updater); .updatePermission(updater);
await ref.read(selectedTabSessionProvider).reload(); await ref.read(selectedTabSessionProvider).reload();
}, [origin, isPrivate]); }
// Build permission entries - only recalculates when permissions change // Build permission entries - only recalculates when permissions change
final allPermissions = useMemoized( final allPermissions = useMemoized(
@@ -104,7 +102,7 @@ class _PermissionsList extends HookConsumerWidget {
), ),
) )
.toList(), .toList(),
[permissions, updatePermission], [permissions],
); );
// Filter to only show permissions that have been explicitly set (not noDecision) // Filter to only show permissions that have been explicitly set (not noDecision)