new extension management

This commit is contained in:
Fabian Freund
2026-04-17 19:09:27 +02:00
parent 8217472f59
commit f9bcd076f2
49 changed files with 7827 additions and 4533 deletions
@@ -0,0 +1,294 @@
/*
* 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:convert';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod/experimental/persist.dart';
import 'package:riverpod_annotation/experimental/persist.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/user/data/providers.dart';
part 'providers.g.dart';
sealed class AddonUpdateOutcome {
const AddonUpdateOutcome();
}
class AddonUpdateOutcomeAvailable extends AddonUpdateOutcome {
final AddonInfo addon;
final String availableVersion;
const AddonUpdateOutcomeAvailable({
required this.addon,
required this.availableVersion,
});
}
class AddonUpdateOutcomeUpToDate extends AddonUpdateOutcome {
const AddonUpdateOutcomeUpToDate();
}
class AddonUpdateOutcomeMissing extends AddonUpdateOutcome {
const AddonUpdateOutcomeMissing();
}
sealed class AddonUpdateRunResult {
const AddonUpdateRunResult();
}
class AddonUpdateRunDone extends AddonUpdateRunResult {
final String? message;
const AddonUpdateRunDone(this.message);
}
class AddonUpdateRunNoRemoteSource extends AddonUpdateRunResult {
const AddonUpdateRunNoRemoteSource();
}
class AddonUpdateRunFailed extends AddonUpdateRunResult {
const AddonUpdateRunFailed();
}
String _resolveAvailableVersion(AddonInfo addon, AddonStoreInfo? storeInfo) {
final latest = storeInfo?.latestVersion.trim();
return (latest != null && latest.isNotEmpty) ? latest : addon.version;
}
@Riverpod()
class AddonDetails extends _$AddonDetails {
GeckoAddonService get _service => ref.read(addonServiceProvider);
Future<void> _run(Future<AddonInfo?> Function() action) async {
state = const AsyncLoading<AddonInfo?>();
state = await AsyncValue.guard(action);
ref.invalidate(addonListProvider);
}
Future<void> refresh() async {
ref.invalidateSelf();
await future;
}
Future<void> install() async {
final current = state.value;
if (current == null) return;
await _run(() async {
await _service.installAddon(Uri.parse(current.downloadUrl));
return _service.getAddonById(addonId);
});
}
Future<void> uninstall() async {
await _run(() async {
await _service.uninstallAddon(addonId);
return null;
});
}
Future<void> setEnabled({required bool enabled}) async {
await _run(
() => enabled
? _service.enableAddon(addonId)
: _service.disableAddon(addonId),
);
}
Future<void> setAllowedInPrivateBrowsing({required bool allowed}) async {
await _run(
() => _service.setAddonAllowedInPrivateBrowsing(addonId, allowed),
);
}
Future<void> setAutoUpdateEnabled({required bool enabled}) async {
await _run(
() => _service.setAddonAutoUpdateEnabledForAddon(addonId, enabled),
);
}
@override
Future<AddonInfo?> build(String addonId) {
return _service.getAddonById(addonId);
}
}
@Riverpod()
Future<AddonStoreInfo?> addonStoreInfo(Ref ref, String addonId) {
return ref.read(addonServiceProvider).getAddonStoreInfo(addonId);
}
@Riverpod()
Future<AddonUpdateAttemptInfo?> lastAddonUpdateAttempt(
Ref ref,
String addonId,
) {
return ref.read(addonServiceProvider).getLastAddonUpdateAttempt(addonId);
}
@Riverpod()
class AddonUpdateCheck extends _$AddonUpdateCheck {
/// Refreshes store info and returns whether an update is available.
Future<AddonUpdateOutcome> resolveAvailableUpdate() async {
final storeInfo = await ref
.read(addonServiceProvider)
.getAddonStoreInfo(addonId);
final fresh = await ref
.read(addonServiceProvider)
.getAddonById(addonId, allowCache: false);
if (fresh == null) return const AddonUpdateOutcomeMissing();
final available = _resolveAvailableVersion(fresh, storeInfo);
final hasUpdate =
fresh.installedVersion != null &&
available.isNotEmpty &&
fresh.installedVersion != available;
return hasUpdate
? AddonUpdateOutcomeAvailable(addon: fresh, availableVersion: available)
: const AddonUpdateOutcomeUpToDate();
}
/// Triggers a remote update and awaits completion. Invalidates dependent
/// providers on completion.
Future<AddonUpdateRunResult> triggerAndAwait() async {
state = const AsyncLoading();
final result = await AsyncValue.guard<AddonUpdateRunResult>(() async {
final AddonUpdateAttemptInfo? attempt;
try {
attempt = await ref
.read(addonServiceProvider)
.triggerAddonUpdate(addonId);
} catch (error) {
final noRemote = error.toString().contains(
'No remote update source is available for this locally installed extension.',
);
return noRemote
? const AddonUpdateRunNoRemoteSource()
: const AddonUpdateRunFailed();
}
return attempt?.status == AddonUpdateStatus.error
? const AddonUpdateRunFailed()
: AddonUpdateRunDone(attempt?.message);
});
state = result;
ref.invalidate(addonDetailsProvider(addonId));
ref.invalidate(lastAddonUpdateAttemptProvider(addonId));
return result.value ?? const AddonUpdateRunFailed();
}
@override
AsyncValue<AddonUpdateRunResult> build(String addonId) =>
const AsyncData(AddonUpdateRunDone(null));
}
@Riverpod()
class AddonList extends _$AddonList {
GeckoAddonService get _service => ref.read(addonServiceProvider);
Future<void> refresh() async {
ref.invalidateSelf();
await future;
}
Future<void> install(AddonInfo addon) async {
ref.read(addonBusyIdsProvider.notifier).add(addon.id);
try {
await _service.installAddon(Uri.parse(addon.downloadUrl));
ref.invalidate(addonDetailsProvider(addon.id));
ref.invalidateSelf();
await future;
} finally {
ref.read(addonBusyIdsProvider.notifier).remove(addon.id);
}
}
Future<void> uninstall(AddonInfo addon) async {
ref.read(addonBusyIdsProvider.notifier).add(addon.id);
try {
await _service.uninstallAddon(addon.id);
ref.invalidate(addonDetailsProvider(addon.id));
ref.invalidateSelf();
await future;
} finally {
ref.read(addonBusyIdsProvider.notifier).remove(addon.id);
}
}
@override
Future<List<AddonInfo>> build() {
return _service.getAddons();
}
}
@Riverpod()
class AddonBusyIds extends _$AddonBusyIds {
void add(String id) => state = {...state, id};
void remove(String id) => state = {...state}..remove(id);
@override
Set<String> build() => const {};
}
@Riverpod(keepAlive: true)
class PinnedAddonIds extends _$PinnedAddonIds {
void setPinned(String addonId, {required bool pinned}) {
if (pinned) {
if (!state.contains(addonId)) {
state = {...state, addonId};
}
} else if (state.contains(addonId)) {
state = {...state}..remove(addonId);
}
}
@override
Set<String> build() {
persist(
ref.watch(riverpodDatabaseStorageProvider),
key: 'PinnedAddonIds',
encode: (state) => jsonEncode(state.toList()),
decode: (encoded) =>
(jsonDecode(encoded) as List<dynamic>).cast<String>().toSet(),
);
return stateOrNull ?? const {};
}
}
@Riverpod()
class BulkAddonUpdate extends _$BulkAddonUpdate {
Future<void> triggerAll() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await ref.read(addonServiceProvider).triggerAllAddonUpdates();
});
}
@override
AsyncValue<void> build() => const AsyncData(null);
}
@@ -0,0 +1,562 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(AddonDetails)
final addonDetailsProvider = AddonDetailsFamily._();
final class AddonDetailsProvider
extends $AsyncNotifierProvider<AddonDetails, AddonInfo?> {
AddonDetailsProvider._({
required AddonDetailsFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'addonDetailsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addonDetailsHash();
@override
String toString() {
return r'addonDetailsProvider'
''
'($argument)';
}
@$internal
@override
AddonDetails create() => AddonDetails();
@override
bool operator ==(Object other) {
return other is AddonDetailsProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$addonDetailsHash() => r'26b4a33e9d17aced1d3fb5c6ff28921f611ca5b0';
final class AddonDetailsFamily extends $Family
with
$ClassFamilyOverride<
AddonDetails,
AsyncValue<AddonInfo?>,
AddonInfo?,
FutureOr<AddonInfo?>,
String
> {
AddonDetailsFamily._()
: super(
retry: null,
name: r'addonDetailsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AddonDetailsProvider call(String addonId) =>
AddonDetailsProvider._(argument: addonId, from: this);
@override
String toString() => r'addonDetailsProvider';
}
abstract class _$AddonDetails extends $AsyncNotifier<AddonInfo?> {
late final _$args = ref.$arg as String;
String get addonId => _$args;
FutureOr<AddonInfo?> build(String addonId);
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<AddonInfo?>, AddonInfo?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<AddonInfo?>, AddonInfo?>,
AsyncValue<AddonInfo?>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(addonStoreInfo)
final addonStoreInfoProvider = AddonStoreInfoFamily._();
final class AddonStoreInfoProvider
extends
$FunctionalProvider<
AsyncValue<AddonStoreInfo?>,
AddonStoreInfo?,
FutureOr<AddonStoreInfo?>
>
with $FutureModifier<AddonStoreInfo?>, $FutureProvider<AddonStoreInfo?> {
AddonStoreInfoProvider._({
required AddonStoreInfoFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'addonStoreInfoProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addonStoreInfoHash();
@override
String toString() {
return r'addonStoreInfoProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<AddonStoreInfo?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<AddonStoreInfo?> create(Ref ref) {
final argument = this.argument as String;
return addonStoreInfo(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AddonStoreInfoProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$addonStoreInfoHash() => r'0024a540ecf6f1d55243d9dc963e04fbc212275e';
final class AddonStoreInfoFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<AddonStoreInfo?>, String> {
AddonStoreInfoFamily._()
: super(
retry: null,
name: r'addonStoreInfoProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AddonStoreInfoProvider call(String addonId) =>
AddonStoreInfoProvider._(argument: addonId, from: this);
@override
String toString() => r'addonStoreInfoProvider';
}
@ProviderFor(lastAddonUpdateAttempt)
final lastAddonUpdateAttemptProvider = LastAddonUpdateAttemptFamily._();
final class LastAddonUpdateAttemptProvider
extends
$FunctionalProvider<
AsyncValue<AddonUpdateAttemptInfo?>,
AddonUpdateAttemptInfo?,
FutureOr<AddonUpdateAttemptInfo?>
>
with
$FutureModifier<AddonUpdateAttemptInfo?>,
$FutureProvider<AddonUpdateAttemptInfo?> {
LastAddonUpdateAttemptProvider._({
required LastAddonUpdateAttemptFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'lastAddonUpdateAttemptProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$lastAddonUpdateAttemptHash();
@override
String toString() {
return r'lastAddonUpdateAttemptProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<AddonUpdateAttemptInfo?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<AddonUpdateAttemptInfo?> create(Ref ref) {
final argument = this.argument as String;
return lastAddonUpdateAttempt(ref, argument);
}
@override
bool operator ==(Object other) {
return other is LastAddonUpdateAttemptProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$lastAddonUpdateAttemptHash() =>
r'79847d9f5720fea1f742d57c3b17272ffd980cc1';
final class LastAddonUpdateAttemptFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<AddonUpdateAttemptInfo?>, String> {
LastAddonUpdateAttemptFamily._()
: super(
retry: null,
name: r'lastAddonUpdateAttemptProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
LastAddonUpdateAttemptProvider call(String addonId) =>
LastAddonUpdateAttemptProvider._(argument: addonId, from: this);
@override
String toString() => r'lastAddonUpdateAttemptProvider';
}
@ProviderFor(AddonUpdateCheck)
final addonUpdateCheckProvider = AddonUpdateCheckFamily._();
final class AddonUpdateCheckProvider
extends
$NotifierProvider<AddonUpdateCheck, AsyncValue<AddonUpdateRunResult>> {
AddonUpdateCheckProvider._({
required AddonUpdateCheckFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'addonUpdateCheckProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addonUpdateCheckHash();
@override
String toString() {
return r'addonUpdateCheckProvider'
''
'($argument)';
}
@$internal
@override
AddonUpdateCheck create() => AddonUpdateCheck();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AsyncValue<AddonUpdateRunResult> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AsyncValue<AddonUpdateRunResult>>(
value,
),
);
}
@override
bool operator ==(Object other) {
return other is AddonUpdateCheckProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$addonUpdateCheckHash() => r'4ef375b5cd9b0eb89fbbdaff3f93a35482191af5';
final class AddonUpdateCheckFamily extends $Family
with
$ClassFamilyOverride<
AddonUpdateCheck,
AsyncValue<AddonUpdateRunResult>,
AsyncValue<AddonUpdateRunResult>,
AsyncValue<AddonUpdateRunResult>,
String
> {
AddonUpdateCheckFamily._()
: super(
retry: null,
name: r'addonUpdateCheckProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
AddonUpdateCheckProvider call(String addonId) =>
AddonUpdateCheckProvider._(argument: addonId, from: this);
@override
String toString() => r'addonUpdateCheckProvider';
}
abstract class _$AddonUpdateCheck
extends $Notifier<AsyncValue<AddonUpdateRunResult>> {
late final _$args = ref.$arg as String;
String get addonId => _$args;
AsyncValue<AddonUpdateRunResult> build(String addonId);
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
AsyncValue<AddonUpdateRunResult>,
AsyncValue<AddonUpdateRunResult>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<AddonUpdateRunResult>,
AsyncValue<AddonUpdateRunResult>
>,
AsyncValue<AddonUpdateRunResult>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(AddonList)
final addonListProvider = AddonListProvider._();
final class AddonListProvider
extends $AsyncNotifierProvider<AddonList, List<AddonInfo>> {
AddonListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'addonListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addonListHash();
@$internal
@override
AddonList create() => AddonList();
}
String _$addonListHash() => r'7625b69a433d073e186571453e39b557d8b48a5d';
abstract class _$AddonList extends $AsyncNotifier<List<AddonInfo>> {
FutureOr<List<AddonInfo>> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<List<AddonInfo>>, List<AddonInfo>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<AddonInfo>>, List<AddonInfo>>,
AsyncValue<List<AddonInfo>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(AddonBusyIds)
final addonBusyIdsProvider = AddonBusyIdsProvider._();
final class AddonBusyIdsProvider
extends $NotifierProvider<AddonBusyIds, Set<String>> {
AddonBusyIdsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'addonBusyIdsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addonBusyIdsHash();
@$internal
@override
AddonBusyIds create() => AddonBusyIds();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<String> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Set<String>>(value),
);
}
}
String _$addonBusyIdsHash() => r'6f9761320d42b2b5132936797617fdb13b718dd3';
abstract class _$AddonBusyIds extends $Notifier<Set<String>> {
Set<String> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<Set<String>, Set<String>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Set<String>, Set<String>>,
Set<String>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(PinnedAddonIds)
final pinnedAddonIdsProvider = PinnedAddonIdsProvider._();
final class PinnedAddonIdsProvider
extends $NotifierProvider<PinnedAddonIds, Set<String>> {
PinnedAddonIdsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'pinnedAddonIdsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$pinnedAddonIdsHash();
@$internal
@override
PinnedAddonIds create() => PinnedAddonIds();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<String> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Set<String>>(value),
);
}
}
String _$pinnedAddonIdsHash() => r'4f46cd69d4817e6e19e4d04f4fdcf83d5e2efb8c';
abstract class _$PinnedAddonIds extends $Notifier<Set<String>> {
Set<String> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<Set<String>, Set<String>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Set<String>, Set<String>>,
Set<String>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(BulkAddonUpdate)
final bulkAddonUpdateProvider = BulkAddonUpdateProvider._();
final class BulkAddonUpdateProvider
extends $NotifierProvider<BulkAddonUpdate, AsyncValue<void>> {
BulkAddonUpdateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'bulkAddonUpdateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bulkAddonUpdateHash();
@$internal
@override
BulkAddonUpdate create() => BulkAddonUpdate();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AsyncValue<void> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AsyncValue<void>>(value),
);
}
}
String _$bulkAddonUpdateHash() => r'2605711734f9eb6af70e721a4337556a7ea85512';
abstract class _$BulkAddonUpdate extends $Notifier<AsyncValue<void>> {
AsyncValue<void> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<void>, AsyncValue<void>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<void>, AsyncValue<void>>,
AsyncValue<void>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}