add initial sync of extensions

This commit is contained in:
Fabian Freund
2026-02-09 04:59:43 +01:00
parent fec33aa694
commit e0cce93ffb
9 changed files with 180 additions and 18 deletions
@@ -21,7 +21,6 @@ import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:nullability/nullability.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/logger.dart';
import 'package:weblibre/domain/entities/equatable_image.dart'; import 'package:weblibre/domain/entities/equatable_image.dart';
@@ -61,12 +60,12 @@ class WebExtensionsState extends _$WebExtensionsState {
title: data.title, title: data.title,
enabled: data.enabled ?? current.enabled, enabled: data.enabled ?? current.enabled,
badgeText: data.badgeText, badgeText: data.badgeText,
badgeTextColor: data.badgeTextColor.mapNotNull( badgeTextColor: data.badgeTextColor != null
(color) => Color(color), ? Color(data.badgeTextColor!)
), : null,
badgeBackgroundColor: data.badgeBackgroundColor.mapNotNull( badgeBackgroundColor: data.badgeBackgroundColor != null
(color) => Color(color), ? Color(data.badgeBackgroundColor!)
), : null,
); );
} else { } else {
if (state.containsKey(extensionId)) { if (state.containsKey(extensionId)) {
@@ -152,6 +151,40 @@ class WebExtensionsState extends _$WebExtensionsState {
], ],
}; };
// Sync extension events after engine is ready and listeners are set up
// This ensures we get the current state even if we missed initial events
ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
try {
await GeckoTabService().syncEvents(
onBrowserExtensionsChange:
actionType == WebExtensionActionType.browser,
onPageExtensionsChange: actionType == WebExtensionActionType.page,
onBrowserExtensionIcons:
actionType == WebExtensionActionType.browser,
onPageExtensionIcons: actionType == WebExtensionActionType.page,
);
} catch (e, s) {
logger.w(
'Failed to sync extension events for $actionType',
error: e,
stackTrace: s,
);
}
}
},
onError: (error, stackTrace) {
logger.e(
'Error listening to engineReadyStateProvider',
error: error,
stackTrace: stackTrace,
);
},
);
ref.onDispose(() async { ref.onDispose(() async {
// Dispose all cached images // Dispose all cached images
_imageCache.clear(); _imageCache.clear();
@@ -62,7 +62,7 @@ final class WebExtensionsStateProvider
} }
String _$webExtensionsStateHash() => String _$webExtensionsStateHash() =>
r'28cd9c99bc167fa1a051eccf3d23917a417e54ea'; r'13cbbea409b5b643eb6766689f6b23a4d1701204';
final class WebExtensionsStateFamily extends $Family final class WebExtensionsStateFamily extends $Family
with with
@@ -37,11 +37,7 @@ class ExtensionBadgeIcon extends StatelessWidget {
textColor: state.badgeTextColor, textColor: state.badgeTextColor,
backgroundColor: state.badgeBackgroundColor, backgroundColor: state.badgeBackgroundColor,
child: RepaintBoundary( child: RepaintBoundary(
child: SafeRawImage( child: SafeRawImage(image: state.icon, width: 24, height: 24),
image: state.icon,
width: 24,
height: 24,
),
), ),
); );
} }
@@ -38,7 +38,7 @@ class Components(val profileApplicationContext: ProfileContext,
val logLevel: Log.Priority, val logLevel: Log.Priority,
val contentBlocking: ContentBlocking, val contentBlocking: ContentBlocking,
val addonCollection: AddonCollection?, val addonCollection: AddonCollection?,
private val addonEvents: GeckoAddonEvents, val addonEvents: GeckoAddonEvents,
private val tabContentEvents: GeckoTabContentEvents, private val tabContentEvents: GeckoTabContentEvents,
private val extensionEvents: BrowserExtensionEvents private val extensionEvents: BrowserExtensionEvents
) { ) {
@@ -14,6 +14,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.RecoverableBrowserState as
import eu.weblibre.flutter_mozilla_components.pigeons.RestoreLocation as PigeonRestoreLocation import eu.weblibre.flutter_mozilla_components.pigeons.RestoreLocation as PigeonRestoreLocation
import eu.weblibre.flutter_mozilla_components.pigeons.RecoverableTab as PigeonRecoverableTab import eu.weblibre.flutter_mozilla_components.pigeons.RecoverableTab as PigeonRecoverableTab
import eu.weblibre.flutter_mozilla_components.pigeons.SourceValue import eu.weblibre.flutter_mozilla_components.pigeons.SourceValue
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionActionType
import eu.weblibre.flutter_mozilla_components.pigeons.WebExtensionData
import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes import eu.weblibre.flutter_mozilla_components.ext.toWebPBytes
import eu.weblibre.flutter_mozilla_components.pigeons.FindResultState import eu.weblibre.flutter_mozilla_components.pigeons.FindResultState
@@ -178,6 +180,10 @@ class GeckoTabsApiImpl : GeckoTabsApi {
onHistoryStateChange: Boolean, onHistoryStateChange: Boolean,
onFindResults: Boolean, onFindResults: Boolean,
onThumbnailChange: Boolean, onThumbnailChange: Boolean,
onBrowserExtensionsChange: Boolean,
onPageExtensionsChange: Boolean,
onBrowserExtensionIcons: Boolean,
onPageExtensionIcons: Boolean,
) { ) {
try { try {
val tabs = components.core.store.state.tabs.map { it.copy() } val tabs = components.core.store.state.tabs.map { it.copy() }
@@ -274,11 +280,122 @@ class GeckoTabsApiImpl : GeckoTabsApi {
coroutineScope.launch { handleThumbnailChange(tab) } coroutineScope.launch { handleThumbnailChange(tab) }
} }
} }
// Sync extension events
if (onBrowserExtensionsChange || onPageExtensionsChange ||
onBrowserExtensionIcons || onPageExtensionIcons) {
syncExtensionEvents(
onBrowserExtensionsChange,
onPageExtensionsChange,
onBrowserExtensionIcons,
onPageExtensionIcons
)
}
} catch (e: Exception) { } catch (e: Exception) {
logger.error("$TAG: Failed to sync events", e) logger.error("$TAG: Failed to sync events", e)
} }
} }
private fun syncExtensionEvents(
onBrowserExtensionsChange: Boolean,
onPageExtensionsChange: Boolean,
onBrowserExtensionIcons: Boolean,
onPageExtensionIcons: Boolean
) {
try {
val extensions = components.core.store.state.extensions.values.filter { it.enabled }
extensions.forEach { extension ->
val browserAction = extension.browserAction
val pageAction = extension.pageAction
// Sync browser action
if (browserAction != null) {
if (onBrowserExtensionsChange) {
val data = WebExtensionData(
extensionId = extension.id,
title = browserAction.title,
enabled = browserAction.enabled,
badgeText = browserAction.badgeText,
badgeTextColor = browserAction.badgeTextColor?.toLong(),
badgeBackgroundColor = browserAction.badgeBackgroundColor?.toLong(),
)
components.addonEvents.onUpsertWebExtensionAction(
System.currentTimeMillis(),
extension.id,
WebExtensionActionType.BROWSER,
data
) { }
}
if (onBrowserExtensionIcons) {
coroutineScope.launch {
try {
val icon = browserAction.loadIcon?.invoke(128)
icon?.let {
val imageBytes = icon.toWebPBytes()
withContext(Dispatchers.Main) {
components.addonEvents.onUpdateWebExtensionIcon(
System.currentTimeMillis(),
extension.id,
WebExtensionActionType.BROWSER,
imageBytes
) { }
}
}
} catch (e: Exception) {
logger.error("$TAG: Failed to load browser action icon for ${extension.id}", e)
}
}
}
}
// Sync page action
if (pageAction != null && pageAction.enabled == true) {
if (onPageExtensionsChange) {
val data = WebExtensionData(
extensionId = extension.id,
title = pageAction.title,
enabled = pageAction.enabled,
badgeText = pageAction.badgeText,
badgeTextColor = pageAction.badgeTextColor?.toLong(),
badgeBackgroundColor = pageAction.badgeBackgroundColor?.toLong(),
)
components.addonEvents.onUpsertWebExtensionAction(
System.currentTimeMillis(),
extension.id,
WebExtensionActionType.PAGE,
data
) { }
}
if (onPageExtensionIcons) {
coroutineScope.launch {
try {
val icon = pageAction.loadIcon?.invoke(128)
icon?.let {
val imageBytes = icon.toWebPBytes()
withContext(Dispatchers.Main) {
components.addonEvents.onUpdateWebExtensionIcon(
System.currentTimeMillis(),
extension.id,
WebExtensionActionType.PAGE,
imageBytes
) { }
}
}
} catch (e: Exception) {
logger.error("$TAG: Failed to load page action icon for ${extension.id}", e)
}
}
}
}
}
} catch (e: Exception) {
logger.error("$TAG: Failed to sync extension events", e)
}
}
override fun selectTab(tabId: String) { override fun selectTab(tabId: String) {
try { try {
components.useCases.tabsUseCases.selectTab(tabId = tabId) components.useCases.tabsUseCases.selectTab(tabId = tabId)
@@ -4344,7 +4344,7 @@ interface GeckoSessionApi {
} }
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoTabsApi { interface GeckoTabsApi {
fun syncEvents(onSelectedTabChange: Boolean, onTabListChange: Boolean, onTabContentStateChange: Boolean, onIconChange: Boolean, onSecurityInfoStateChange: Boolean, onReaderableStateChange: Boolean, onHistoryStateChange: Boolean, onFindResults: Boolean, onThumbnailChange: Boolean) fun syncEvents(onSelectedTabChange: Boolean, onTabListChange: Boolean, onTabContentStateChange: Boolean, onIconChange: Boolean, onSecurityInfoStateChange: Boolean, onReaderableStateChange: Boolean, onHistoryStateChange: Boolean, onFindResults: Boolean, onThumbnailChange: Boolean, onBrowserExtensionsChange: Boolean, onPageExtensionsChange: Boolean, onBrowserExtensionIcons: Boolean, onPageExtensionIcons: Boolean)
fun selectTab(tabId: String) fun selectTab(tabId: String)
fun removeTab(tabId: String) fun removeTab(tabId: String)
fun addTab(url: String, selectTab: Boolean, startLoading: Boolean, parentId: String?, flags: LoadUrlFlagsValue, contextId: String?, source: SourceValue, private: Boolean, historyMetadata: HistoryMetadataKey?, additionalHeaders: Map<String, String>?): String fun addTab(url: String, selectTab: Boolean, startLoading: Boolean, parentId: String?, flags: LoadUrlFlagsValue, contextId: String?, source: SourceValue, private: Boolean, historyMetadata: HistoryMetadataKey?, additionalHeaders: Map<String, String>?): String
@@ -4390,8 +4390,12 @@ interface GeckoTabsApi {
val onHistoryStateChangeArg = args[6] as Boolean val onHistoryStateChangeArg = args[6] as Boolean
val onFindResultsArg = args[7] as Boolean val onFindResultsArg = args[7] as Boolean
val onThumbnailChangeArg = args[8] as Boolean val onThumbnailChangeArg = args[8] as Boolean
val onBrowserExtensionsChangeArg = args[9] as Boolean
val onPageExtensionsChangeArg = args[10] as Boolean
val onBrowserExtensionIconsArg = args[11] as Boolean
val onPageExtensionIconsArg = args[12] as Boolean
val wrapped: List<Any?> = try { val wrapped: List<Any?> = try {
api.syncEvents(onSelectedTabChangeArg, onTabListChangeArg, onTabContentStateChangeArg, onIconChangeArg, onSecurityInfoStateChangeArg, onReaderableStateChangeArg, onHistoryStateChangeArg, onFindResultsArg, onThumbnailChangeArg) api.syncEvents(onSelectedTabChangeArg, onTabListChangeArg, onTabContentStateChangeArg, onIconChangeArg, onSecurityInfoStateChangeArg, onReaderableStateChangeArg, onHistoryStateChangeArg, onFindResultsArg, onThumbnailChangeArg, onBrowserExtensionsChangeArg, onPageExtensionsChangeArg, onBrowserExtensionIconsArg, onPageExtensionIconsArg)
listOf(null) listOf(null)
} catch (exception: Throwable) { } catch (exception: Throwable) {
GeckoPigeonUtils.wrapError(exception) GeckoPigeonUtils.wrapError(exception)
@@ -25,6 +25,10 @@ class GeckoTabService {
bool onHistoryStateChange = false, bool onHistoryStateChange = false,
bool onFindResults = false, bool onFindResults = false,
bool onThumbnailChange = false, bool onThumbnailChange = false,
bool onBrowserExtensionsChange = false,
bool onPageExtensionsChange = false,
bool onBrowserExtensionIcons = false,
bool onPageExtensionIcons = false,
}) { }) {
return _api.syncEvents( return _api.syncEvents(
onSelectedTabChange: onSelectedTabChange, onSelectedTabChange: onSelectedTabChange,
@@ -36,6 +40,10 @@ class GeckoTabService {
onFindResults: onFindResults, onFindResults: onFindResults,
onThumbnailChange: onThumbnailChange, onThumbnailChange: onThumbnailChange,
onReaderableStateChange: false, onReaderableStateChange: false,
onBrowserExtensionsChange: onBrowserExtensionsChange,
onPageExtensionsChange: onPageExtensionsChange,
onBrowserExtensionIcons: onBrowserExtensionIcons,
onPageExtensionIcons: onPageExtensionIcons,
); );
} }
@@ -4935,14 +4935,14 @@ class GeckoTabsApi {
final String pigeonVar_messageChannelSuffix; final String pigeonVar_messageChannelSuffix;
Future<void> syncEvents({required bool onSelectedTabChange, required bool onTabListChange, required bool onTabContentStateChange, required bool onIconChange, required bool onSecurityInfoStateChange, required bool onReaderableStateChange, required bool onHistoryStateChange, required bool onFindResults, required bool onThumbnailChange, }) async { Future<void> syncEvents({required bool onSelectedTabChange, required bool onTabListChange, required bool onTabContentStateChange, required bool onIconChange, required bool onSecurityInfoStateChange, required bool onReaderableStateChange, required bool onHistoryStateChange, required bool onFindResults, required bool onThumbnailChange, required bool onBrowserExtensionsChange, required bool onPageExtensionsChange, required bool onBrowserExtensionIcons, required bool onPageExtensionIcons, }) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.syncEvents$pigeonVar_messageChannelSuffix'; final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.syncEvents$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>( final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName, pigeonVar_channelName,
pigeonChannelCodec, pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger, binaryMessenger: pigeonVar_binaryMessenger,
); );
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[onSelectedTabChange, onTabListChange, onTabContentStateChange, onIconChange, onSecurityInfoStateChange, onReaderableStateChange, onHistoryStateChange, onFindResults, onThumbnailChange]); final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[onSelectedTabChange, onTabListChange, onTabContentStateChange, onIconChange, onSecurityInfoStateChange, onReaderableStateChange, onHistoryStateChange, onFindResults, onThumbnailChange, onBrowserExtensionsChange, onPageExtensionsChange, onBrowserExtensionIcons, onPageExtensionIcons]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?; final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) { if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName); throw _createConnectionError(pigeonVar_channelName);
@@ -1113,6 +1113,10 @@ abstract class GeckoTabsApi {
required bool onHistoryStateChange, required bool onHistoryStateChange,
required bool onFindResults, required bool onFindResults,
required bool onThumbnailChange, required bool onThumbnailChange,
required bool onBrowserExtensionsChange,
required bool onPageExtensionsChange,
required bool onBrowserExtensionIcons,
required bool onPageExtensionIcons,
}); });
void selectTab({required String tabId}); void selectTab({required String tabId});