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
@@ -47,8 +47,7 @@ class AllowUnsignedExtensions extends _$AllowUnsignedExtensions {
@override
FutureOr<bool> build() async {
final prefs =
await GeckoPrefService().getPrefs([_signatureRequiredPref]);
final prefs = await GeckoPrefService().getPrefs([_signatureRequiredPref]);
final pref = prefs[_signatureRequiredPref];
final allowUnsigned = pref?.value == false;
@@ -63,6 +62,21 @@ class AllowUnsignedExtensions extends _$AllowUnsignedExtensions {
}
}
@Riverpod(keepAlive: true)
class AddonAutoUpdate extends _$AddonAutoUpdate {
Future<void> setEnabled({required bool enabled}) async {
final service = ref.read(addonServiceProvider);
await service.setAddonAutoUpdateEnabled(enabled: enabled);
state = AsyncData(enabled);
}
@override
FutureOr<bool> build() {
return ref.read(addonServiceProvider).isAddonAutoUpdateEnabled();
}
}
@Riverpod(keepAlive: true)
class BrowserAddonService extends _$BrowserAddonService {
Future<Uri> getAddonXpiUrl(String guid) async {
@@ -54,6 +54,50 @@ abstract class _$AllowUnsignedExtensions extends $AsyncNotifier<bool> {
}
}
@ProviderFor(AddonAutoUpdate)
final addonAutoUpdateProvider = AddonAutoUpdateProvider._();
final class AddonAutoUpdateProvider
extends $AsyncNotifierProvider<AddonAutoUpdate, bool> {
AddonAutoUpdateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'addonAutoUpdateProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$addonAutoUpdateHash();
@$internal
@override
AddonAutoUpdate create() => AddonAutoUpdate();
}
String _$addonAutoUpdateHash() => r'89791e8b771da715b068bbdbe5c3c24a3dad4194';
abstract class _$AddonAutoUpdate extends $AsyncNotifier<bool> {
FutureOr<bool> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<bool>, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<bool>, bool>,
AsyncValue<bool>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(BrowserAddonService)
final browserAddonServiceProvider = BrowserAddonServiceProvider._();
@@ -76,7 +76,10 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
.installFromFile(selectedFile.value!);
if (context.mounted) {
showInfoMessage(context, 'Extension installed successfully');
showInfoMessage(
context,
'Extension installed. Automatic updates are disabled for this local version.',
);
context.pop(true);
}
} catch (e) {
@@ -149,6 +152,11 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
],
),
),
const SizedBox(height: 12),
Text(
'Extensions installed from a local XPI stay pinned to that version and will not update automatically.',
style: Theme.of(context).textTheme.bodySmall,
),
if (errorMessage.value != null) ...[
const SizedBox(height: 8),
Container(
@@ -44,6 +44,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/contro
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/browser_viewport_toolbar_insets.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/addon_popup_bottom_sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
@@ -305,6 +306,18 @@ class BrowserScreen extends HookConsumerWidget {
},
);
final addonService = ref.watch(addonServiceProvider);
useOnStreamChange(
addonService.popupStream,
onData: (event) async {
await showAddonPopupBottomSheet(
context,
extensionId: event.extensionId,
extensionName: event.extensionName,
);
},
);
useOnAppLifecycleStateChange((previous, current) {
switch (current) {
case AppLifecycleState.resumed:
@@ -0,0 +1,121 @@
/*
* 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:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
const _viewType = 'eu.weblibre/addon_popup';
Future<void> showAddonPopupBottomSheet(
BuildContext context, {
required String extensionId,
required String extensionName,
}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (context) => _AddonPopupSheet(
extensionId: extensionId,
extensionName: extensionName,
),
);
}
class _AddonPopupSheet extends StatelessWidget {
final String extensionId;
final String extensionName;
const _AddonPopupSheet({
required this.extensionId,
required this.extensionName,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return DraggableScrollableSheet(
initialChildSize: 0.65,
minChildSize: 0.3,
maxChildSize: 0.95,
expand: false,
builder: (context, scrollController) {
return Column(
children: [
Container(
margin: const EdgeInsets.only(top: 12, bottom: 8),
height: 4,
width: 40,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
const Divider(height: 1),
Expanded(child: _AddonPopupPlatformView(extensionId: extensionId)),
],
);
},
);
}
}
class _AddonPopupPlatformView extends StatelessWidget {
final String extensionId;
const _AddonPopupPlatformView({required this.extensionId});
@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: _viewType,
surfaceFactory: (context, controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{
Factory<EagerGestureRecognizer>(EagerGestureRecognizer.new),
},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (params) {
final controller = PlatformViewsService.initExpensiveAndroidView(
id: params.id,
viewType: _viewType,
layoutDirection: TextDirection.ltr,
creationParams: <String, Object?>{'extensionId': extensionId},
creationParamsCodec: const StandardMessageCodec(),
);
controller.addOnPlatformViewCreatedListener(
params.onPlatformViewCreated,
);
unawaited(controller.create());
return controller;
},
);
}
}
@@ -36,6 +36,7 @@ import 'package:skeletonizer/skeletonizer.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/providers/persisted_bool.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/addons/presentation/screens/addon_internal_settings.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.dart';
@@ -1706,9 +1707,10 @@ class _ExtensionsCard extends HookConsumerWidget {
WebExtensionActionType.browser,
).select((value) => value.values.toList()),
);
final rootContext = Navigator.of(context, rootNavigator: true).context;
Future<void> openExtensionSettings(String extensionId) async {
Navigator.pop(context);
await addonService.startAddonSettingsActivity(extensionId);
await openAddonSettingsFlowById(rootContext, ref, extensionId);
}
return _buildMenuCard(
@@ -1804,7 +1806,7 @@ class _ExtensionsCard extends HookConsumerWidget {
icon: MdiIcons.puzzleEdit,
onTap: () async {
Navigator.pop(context);
await addonService.startAddonManagerActivity();
await const AddonManagerRoute().push<void>(rootContext);
},
),
_buildSubTile(
@@ -27,6 +27,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/features/addons/presentation/widgets/pinned_addon_bar.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
@@ -252,6 +253,7 @@ class BrowserTabBar extends HookConsumerWidget {
: const AppBarTitle()
: null,
actions: [
const PinnedAddonBar(),
if (isSmallWebMode)
ReaderButton(
buttonBuilder: (isLoading, readerActive, icon) => ToolbarButton(