app links initial
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'app_link_rule.g.dart';
|
||||
|
||||
enum AppLinkRuleDecision { alwaysOpen, neverOpen }
|
||||
|
||||
/// A remembered per-scope app-link rule (persistence contract, §2.5/§2.9).
|
||||
///
|
||||
/// Stored in `GeneralSettings.appLinkRules` as `Map<String, PersistedAppLinkRule>`
|
||||
/// keyed by [scope] — one canonical rule per scope, upsert/last-write-wins. The
|
||||
/// [scope] is a native-owned canonical key (`host:youtube.com` | `pkg:...`) that
|
||||
/// Dart persists opaquely and never reconstructs.
|
||||
@CopyWith()
|
||||
@JsonSerializable()
|
||||
class PersistedAppLinkRule with FastEquatable {
|
||||
final AppLinkRuleDecision decision;
|
||||
|
||||
/// Canonical scope key: `host:<host>` or `pkg:<package>`.
|
||||
final String scope;
|
||||
|
||||
/// Resolved package name. Required for [AppLinkRuleDecision.alwaysOpen]
|
||||
/// (binds the launch target); null for [AppLinkRuleDecision.neverOpen].
|
||||
final String? packageName;
|
||||
|
||||
PersistedAppLinkRule({
|
||||
required this.decision,
|
||||
required this.scope,
|
||||
this.packageName,
|
||||
});
|
||||
|
||||
factory PersistedAppLinkRule.fromJson(Map<String, dynamic> json) =>
|
||||
_$PersistedAppLinkRuleFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$PersistedAppLinkRuleToJson(this);
|
||||
|
||||
/// Whether this rule is internally consistent: an `alwaysOpen` rule must bind
|
||||
/// a package; the scope must be a recognised canonical key.
|
||||
bool get isValid {
|
||||
if (scope.isEmpty) return false;
|
||||
final hasKnownPrefix =
|
||||
scope.startsWith('host:') || scope.startsWith('pkg:');
|
||||
if (!hasKnownPrefix) return false;
|
||||
if (decision == AppLinkRuleDecision.alwaysOpen &&
|
||||
(packageName == null || packageName!.isEmpty)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [decision, scope, packageName];
|
||||
}
|
||||
|
||||
/// Parse the persisted rule map, dropping malformed rules (with a warning) and
|
||||
/// entries whose map key disagrees with the rule's own scope (§2.9).
|
||||
Map<String, PersistedAppLinkRule> parseAppLinkRules(
|
||||
Map<String, dynamic>? json,
|
||||
) {
|
||||
if (json == null) return const {};
|
||||
final result = <String, PersistedAppLinkRule>{};
|
||||
for (final MapEntry(:key, :value) in json.entries) {
|
||||
if (value is! Map<String, dynamic>) continue;
|
||||
final PersistedAppLinkRule rule;
|
||||
try {
|
||||
rule = PersistedAppLinkRule.fromJson(value);
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
if (rule.scope != key) continue;
|
||||
if (!rule.isValid) continue;
|
||||
result[key] = rule;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_link_rule.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$PersistedAppLinkRuleCWProxy {
|
||||
PersistedAppLinkRule decision(AppLinkRuleDecision decision);
|
||||
|
||||
PersistedAppLinkRule scope(String scope);
|
||||
|
||||
PersistedAppLinkRule packageName(String? packageName);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PersistedAppLinkRule(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// PersistedAppLinkRule(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
PersistedAppLinkRule call({
|
||||
AppLinkRuleDecision decision,
|
||||
String scope,
|
||||
String? packageName,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfPersistedAppLinkRule.copyWith(...)` or call `instanceOfPersistedAppLinkRule.copyWith.fieldName(value)` for a single field.
|
||||
class _$PersistedAppLinkRuleCWProxyImpl
|
||||
implements _$PersistedAppLinkRuleCWProxy {
|
||||
const _$PersistedAppLinkRuleCWProxyImpl(this._value);
|
||||
|
||||
final PersistedAppLinkRule _value;
|
||||
|
||||
@override
|
||||
PersistedAppLinkRule decision(AppLinkRuleDecision decision) =>
|
||||
call(decision: decision);
|
||||
|
||||
@override
|
||||
PersistedAppLinkRule scope(String scope) => call(scope: scope);
|
||||
|
||||
@override
|
||||
PersistedAppLinkRule packageName(String? packageName) =>
|
||||
call(packageName: packageName);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `PersistedAppLinkRule(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// PersistedAppLinkRule(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
PersistedAppLinkRule call({
|
||||
Object? decision = const $CopyWithPlaceholder(),
|
||||
Object? scope = const $CopyWithPlaceholder(),
|
||||
Object? packageName = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return PersistedAppLinkRule(
|
||||
decision: decision == const $CopyWithPlaceholder() || decision == null
|
||||
? _value.decision
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: decision as AppLinkRuleDecision,
|
||||
scope: scope == const $CopyWithPlaceholder() || scope == null
|
||||
? _value.scope
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: scope as String,
|
||||
packageName: packageName == const $CopyWithPlaceholder()
|
||||
? _value.packageName
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: packageName as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $PersistedAppLinkRuleCopyWith on PersistedAppLinkRule {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfPersistedAppLinkRule.copyWith(...)` or `instanceOfPersistedAppLinkRule.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$PersistedAppLinkRuleCWProxy get copyWith =>
|
||||
_$PersistedAppLinkRuleCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
PersistedAppLinkRule _$PersistedAppLinkRuleFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => PersistedAppLinkRule(
|
||||
decision: $enumDecode(_$AppLinkRuleDecisionEnumMap, json['decision']),
|
||||
scope: json['scope'] as String,
|
||||
packageName: json['packageName'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PersistedAppLinkRuleToJson(
|
||||
PersistedAppLinkRule instance,
|
||||
) => <String, dynamic>{
|
||||
'decision': _$AppLinkRuleDecisionEnumMap[instance.decision]!,
|
||||
'scope': instance.scope,
|
||||
'packageName': instance.packageName,
|
||||
};
|
||||
|
||||
const _$AppLinkRuleDecisionEnumMap = {
|
||||
AppLinkRuleDecision.alwaysOpen: 'alwaysOpen',
|
||||
AppLinkRuleDecision.neverOpen: 'neverOpen',
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'
|
||||
show AppLinksMode;
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:weblibre/features/app_links/domain/entities/app_link_rule.dart';
|
||||
|
||||
part 'context_app_link_policy.g.dart';
|
||||
|
||||
/// A container's self-contained app-link policy, used when the container has
|
||||
/// "isolated app link settings" enabled (replace semantics — it fully takes the
|
||||
/// place of the global mode + rules for navigations in that container).
|
||||
///
|
||||
/// Stored in `GeneralSettings.appLinkContextOverrides` keyed by the container's
|
||||
/// Gecko contextId (`contextualIdentity`). Only isolated containers have an
|
||||
/// entry; the snapshot builder synthesises a blank-slate default for a freshly
|
||||
/// isolated container that has not customised anything yet.
|
||||
@CopyWith()
|
||||
@JsonSerializable()
|
||||
class ContextAppLinkPolicy with FastEquatable {
|
||||
/// The container's own global app-links mode (default [AppLinksMode.ask]).
|
||||
final AppLinksMode mode;
|
||||
|
||||
/// The container's own remembered per-scope rules, keyed by canonical scope
|
||||
/// (`host:youtube.com` | `pkg:...`). Same shape/validation as the global
|
||||
/// [GeneralSettings.appLinkRules]; malformed entries are dropped on read.
|
||||
@JsonKey(fromJson: parseAppLinkRules)
|
||||
final Map<String, PersistedAppLinkRule> rules;
|
||||
|
||||
ContextAppLinkPolicy({required this.mode, required this.rules});
|
||||
|
||||
/// The blank-slate policy a container starts from when it is first isolated.
|
||||
ContextAppLinkPolicy.blank() : this(mode: AppLinksMode.ask, rules: const {});
|
||||
|
||||
factory ContextAppLinkPolicy.fromJson(Map<String, dynamic> json) =>
|
||||
_$ContextAppLinkPolicyFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$ContextAppLinkPolicyToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [mode, rules];
|
||||
}
|
||||
|
||||
/// Parse the persisted override map, dropping malformed entries (§2.9 style).
|
||||
/// Keys are contextIds; the interceptor only ever consults entries whose
|
||||
/// contextId belongs to a currently-isolated container, so an orphaned entry
|
||||
/// (container deleted / isolation turned off) is inert.
|
||||
Map<String, ContextAppLinkPolicy> parseAppLinkContextOverrides(
|
||||
Map<String, dynamic>? json,
|
||||
) {
|
||||
if (json == null) return const {};
|
||||
final result = <String, ContextAppLinkPolicy>{};
|
||||
for (final MapEntry(:key, :value) in json.entries) {
|
||||
if (value is! Map<String, dynamic>) continue;
|
||||
try {
|
||||
result[key] = ContextAppLinkPolicy.fromJson(value);
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'context_app_link_policy.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$ContextAppLinkPolicyCWProxy {
|
||||
ContextAppLinkPolicy mode(AppLinksMode mode);
|
||||
|
||||
ContextAppLinkPolicy rules(Map<String, PersistedAppLinkRule> rules);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContextAppLinkPolicy(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ContextAppLinkPolicy(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
ContextAppLinkPolicy call({
|
||||
AppLinksMode mode,
|
||||
Map<String, PersistedAppLinkRule> rules,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfContextAppLinkPolicy.copyWith(...)` or call `instanceOfContextAppLinkPolicy.copyWith.fieldName(value)` for a single field.
|
||||
class _$ContextAppLinkPolicyCWProxyImpl
|
||||
implements _$ContextAppLinkPolicyCWProxy {
|
||||
const _$ContextAppLinkPolicyCWProxyImpl(this._value);
|
||||
|
||||
final ContextAppLinkPolicy _value;
|
||||
|
||||
@override
|
||||
ContextAppLinkPolicy mode(AppLinksMode mode) => call(mode: mode);
|
||||
|
||||
@override
|
||||
ContextAppLinkPolicy rules(Map<String, PersistedAppLinkRule> rules) =>
|
||||
call(rules: rules);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `ContextAppLinkPolicy(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ContextAppLinkPolicy(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
ContextAppLinkPolicy call({
|
||||
Object? mode = const $CopyWithPlaceholder(),
|
||||
Object? rules = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return ContextAppLinkPolicy(
|
||||
mode: mode == const $CopyWithPlaceholder() || mode == null
|
||||
? _value.mode
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: mode as AppLinksMode,
|
||||
rules: rules == const $CopyWithPlaceholder() || rules == null
|
||||
? _value.rules
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: rules as Map<String, PersistedAppLinkRule>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $ContextAppLinkPolicyCopyWith on ContextAppLinkPolicy {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfContextAppLinkPolicy.copyWith(...)` or `instanceOfContextAppLinkPolicy.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$ContextAppLinkPolicyCWProxy get copyWith =>
|
||||
_$ContextAppLinkPolicyCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
ContextAppLinkPolicy _$ContextAppLinkPolicyFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => ContextAppLinkPolicy(
|
||||
mode: $enumDecode(_$AppLinksModeEnumMap, json['mode']),
|
||||
rules: parseAppLinkRules(json['rules'] as Map<String, dynamic>?),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ContextAppLinkPolicyToJson(
|
||||
ContextAppLinkPolicy instance,
|
||||
) => <String, dynamic>{
|
||||
'mode': _$AppLinksModeEnumMap[instance.mode]!,
|
||||
'rules': instance.rules.map((k, e) => MapEntry(k, e.toJson())),
|
||||
};
|
||||
|
||||
const _$AppLinksModeEnumMap = {
|
||||
AppLinksMode.always: 'always',
|
||||
AppLinksMode.ask: 'ask',
|
||||
AppLinksMode.never: 'never',
|
||||
};
|
||||
Reference in New Issue
Block a user