add permissions badge
This commit is contained in:
@@ -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)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
|
||||||
|
|||||||
+110
@@ -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;
|
||||||
|
}
|
||||||
+51
@@ -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';
|
||||||
+23
-1
@@ -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(
|
||||||
|
|||||||
+3
-5
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user