intent gatekeeper initial
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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/>.
|
||||
*/
|
||||
enum IntentSourcePolicy { allow, block }
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class PendingIntentDecision with FastEquatable {
|
||||
final int id;
|
||||
final String packageName;
|
||||
final String? url;
|
||||
|
||||
PendingIntentDecision({
|
||||
required this.id,
|
||||
required this.packageName,
|
||||
required this.url,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [id, packageName, url];
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/pending_intent_decision.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
part 'intent_gatekeeper.g.dart';
|
||||
|
||||
const _ownPackageName = 'eu.weblibre.gecko';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class IntentGatekeeper extends _$IntentGatekeeper {
|
||||
late StreamController<PendingIntentDecision> _decisionRequests;
|
||||
final _pending = <int, Completer<bool>>{};
|
||||
int _nextId = 0;
|
||||
|
||||
@override
|
||||
Stream<PendingIntentDecision> build() {
|
||||
_decisionRequests = StreamController<PendingIntentDecision>.broadcast();
|
||||
|
||||
ref.onDispose(() async {
|
||||
for (final completer in _pending.values) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(false);
|
||||
}
|
||||
}
|
||||
|
||||
_pending.clear();
|
||||
|
||||
await _decisionRequests.close();
|
||||
});
|
||||
|
||||
return _decisionRequests.stream;
|
||||
}
|
||||
|
||||
/// Resolves whether an intent coming from [fromPackageName] targeting [url]
|
||||
/// should be allowed through. If the user has to decide, this waits for
|
||||
/// [resolve] to be called with the matching decision id.
|
||||
Future<bool> shouldAllow({
|
||||
required String? fromPackageName,
|
||||
required String? url,
|
||||
}) async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
if (!settings.blockExternalAppsEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Internal / unknown callers: no package to gate on — let through.
|
||||
if (fromPackageName == null || fromPackageName == _ownPackageName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final existing = settings.externalAppIntentPolicies[fromPackageName];
|
||||
if (existing == IntentSourcePolicy.allow) {
|
||||
return true;
|
||||
}
|
||||
if (existing == IntentSourcePolicy.block) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final id = _nextId++;
|
||||
final completer = Completer<bool>();
|
||||
_pending[id] = completer;
|
||||
|
||||
_decisionRequests.add(
|
||||
PendingIntentDecision(id: id, packageName: fromPackageName, url: url),
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> resolve({
|
||||
required int id,
|
||||
required IntentSourcePolicy decision,
|
||||
bool persist = false,
|
||||
String? packageName,
|
||||
}) async {
|
||||
final completer = _pending.remove(id);
|
||||
completer?.complete(decision == IntentSourcePolicy.allow);
|
||||
|
||||
if (persist && packageName != null) {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(current) => current.copyWith.externalAppIntentPolicies({
|
||||
...current.externalAppIntentPolicies,
|
||||
packageName: decision,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'intent_gatekeeper.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(IntentGatekeeper)
|
||||
final intentGatekeeperProvider = IntentGatekeeperProvider._();
|
||||
|
||||
final class IntentGatekeeperProvider
|
||||
extends $StreamNotifierProvider<IntentGatekeeper, PendingIntentDecision> {
|
||||
IntentGatekeeperProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'intentGatekeeperProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$intentGatekeeperHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
IntentGatekeeper create() => IntentGatekeeper();
|
||||
}
|
||||
|
||||
String _$intentGatekeeperHash() => r'94df8850478ad6695eb14752e82af1919ea8a077';
|
||||
|
||||
abstract class _$IntentGatekeeper
|
||||
extends $StreamNotifier<PendingIntentDecision> {
|
||||
Stream<PendingIntentDecision> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<AsyncValue<PendingIntentDecision>, PendingIntentDecision>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
AsyncValue<PendingIntentDecision>,
|
||||
PendingIntentDecision
|
||||
>,
|
||||
AsyncValue<PendingIntentDecision>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:simple_intent_receiver/simple_intent_receiver.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
part 'native_gatekeeper_replicator.g.dart';
|
||||
|
||||
/// Mirrors the Flutter-side block list to the native side so the
|
||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||
/// the Flutter gatekeeper dialog.
|
||||
@Riverpod(keepAlive: true)
|
||||
class NativeIntentGatekeeperReplicator
|
||||
extends _$NativeIntentGatekeeperReplicator {
|
||||
final _api = IntentGatekeeperHostApi();
|
||||
|
||||
Future<void> _push(
|
||||
({bool enabled, Map<String, IntentSourcePolicy> policies}) config,
|
||||
) async {
|
||||
final blocked = config.policies.entries
|
||||
.where((entry) => entry.value == IntentSourcePolicy.block)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
try {
|
||||
await _api.setConfig(config.enabled, blocked);
|
||||
} catch (error, stackTrace) {
|
||||
logger.e(
|
||||
'Failed to replicate intent gatekeeper config to native',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {
|
||||
ref.listen(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(settings) => EquatableValue((
|
||||
enabled: settings.blockExternalAppsEnabled,
|
||||
policies: settings.externalAppIntentPolicies,
|
||||
)),
|
||||
),
|
||||
fireImmediately: true,
|
||||
(p, n) {
|
||||
final previous = p?.value;
|
||||
final next = n.value;
|
||||
|
||||
if (previous != null &&
|
||||
previous.enabled == next.enabled &&
|
||||
const DeepCollectionEquality.unordered().equals(
|
||||
previous.policies,
|
||||
next.policies,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
unawaited(_push(next));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'native_gatekeeper_replicator.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Mirrors the Flutter-side block list to the native side so the
|
||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||
/// the Flutter gatekeeper dialog.
|
||||
|
||||
@ProviderFor(NativeIntentGatekeeperReplicator)
|
||||
final nativeIntentGatekeeperReplicatorProvider =
|
||||
NativeIntentGatekeeperReplicatorProvider._();
|
||||
|
||||
/// Mirrors the Flutter-side block list to the native side so the
|
||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||
/// the Flutter gatekeeper dialog.
|
||||
final class NativeIntentGatekeeperReplicatorProvider
|
||||
extends $NotifierProvider<NativeIntentGatekeeperReplicator, void> {
|
||||
/// Mirrors the Flutter-side block list to the native side so the
|
||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||
/// the Flutter gatekeeper dialog.
|
||||
NativeIntentGatekeeperReplicatorProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'nativeIntentGatekeeperReplicatorProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$nativeIntentGatekeeperReplicatorHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
NativeIntentGatekeeperReplicator create() =>
|
||||
NativeIntentGatekeeperReplicator();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$nativeIntentGatekeeperReplicatorHash() =>
|
||||
r'ee97dbd489e4e946e0a98cd640300f939f3b0682';
|
||||
|
||||
/// Mirrors the Flutter-side block list to the native side so the
|
||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||
/// the Flutter gatekeeper dialog.
|
||||
|
||||
abstract class _$NativeIntentGatekeeperReplicator extends $Notifier<void> {
|
||||
void build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<void, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<void, void>,
|
||||
void,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:simple_intent_receiver/simple_intent_receiver.dart';
|
||||
|
||||
part 'package_label_resolver.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<String?> packageLabel(Ref ref, String packageName) {
|
||||
final api = IntentGatekeeperHostApi();
|
||||
return api.resolvePackageLabel(packageName);
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'package_label_resolver.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(packageLabel)
|
||||
final packageLabelProvider = PackageLabelFamily._();
|
||||
|
||||
final class PackageLabelProvider
|
||||
extends $FunctionalProvider<AsyncValue<String?>, String?, FutureOr<String?>>
|
||||
with $FutureModifier<String?>, $FutureProvider<String?> {
|
||||
PackageLabelProvider._({
|
||||
required PackageLabelFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'packageLabelProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$packageLabelHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'packageLabelProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<String?> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return packageLabel(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PackageLabelProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$packageLabelHash() => r'14f966e502c5dde332cc42d52727ab111bf6a2b2';
|
||||
|
||||
final class PackageLabelFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<String?>, String> {
|
||||
PackageLabelFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'packageLabelProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: false,
|
||||
);
|
||||
|
||||
PackageLabelProvider call(String packageName) =>
|
||||
PackageLabelProvider._(argument: packageName, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'packageLabelProvider';
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/pending_intent_decision.dart';
|
||||
import 'package:weblibre/features/intent_gatekeeper/domain/services/package_label_resolver.dart';
|
||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||
|
||||
class DialogOutcome {
|
||||
final IntentSourcePolicy decision;
|
||||
final bool persist;
|
||||
|
||||
const DialogOutcome({required this.decision, this.persist = false});
|
||||
}
|
||||
|
||||
class IntentGatekeeperDialog extends HookConsumerWidget {
|
||||
final PendingIntentDecision request;
|
||||
|
||||
const IntentGatekeeperDialog({super.key, required this.request});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final bold = TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
);
|
||||
|
||||
final label = ref.watch(
|
||||
packageLabelProvider(request.packageName).select((value) => value.value),
|
||||
);
|
||||
|
||||
final displayName = (label != null && label.isNotEmpty)
|
||||
? label
|
||||
: request.packageName;
|
||||
|
||||
final uri = request.url != null ? Uri.tryParse(request.url!) : null;
|
||||
|
||||
return AlertDialog(
|
||||
icon: const Icon(Icons.shield_outlined, size: 32),
|
||||
title: const Text('Open link in WebLibre?'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: displayName, style: bold),
|
||||
const TextSpan(text: ' is trying to open a link in '),
|
||||
TextSpan(text: 'WebLibre', style: bold),
|
||||
const TextSpan(text: '.'),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (uri != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: UriBreadcrumb(
|
||||
uri: uri,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
|
||||
actionsPadding: const EdgeInsets.all(24),
|
||||
actions: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(
|
||||
const DialogOutcome(
|
||||
decision: IntentSourcePolicy.allow,
|
||||
persist: true,
|
||||
),
|
||||
),
|
||||
child: const Text('Always allow'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton.tonal(
|
||||
onPressed: () => Navigator.of(context).pop(
|
||||
const DialogOutcome(decision: IntentSourcePolicy.allow),
|
||||
),
|
||||
child: const Text('Allow once'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => Navigator.of(context).pop(
|
||||
const DialogOutcome(decision: IntentSourcePolicy.block),
|
||||
),
|
||||
child: const Text('Block once'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(
|
||||
const DialogOutcome(
|
||||
decision: IntentSourcePolicy.block,
|
||||
persist: true,
|
||||
),
|
||||
),
|
||||
child: const Text('Always block'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user