new extension management
This commit is contained in:
+16
-2
@@ -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 {
|
||||
|
||||
+44
@@ -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._();
|
||||
|
||||
|
||||
+9
-1
@@ -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(
|
||||
|
||||
+13
@@ -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:
|
||||
|
||||
+121
@@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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(
|
||||
|
||||
+2
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user