diff --git a/app/lib/features/geckoview/domain/providers/web_extensions_state.dart b/app/lib/features/geckoview/domain/providers/web_extensions_state.dart index 7233680b..6032cb0f 100644 --- a/app/lib/features/geckoview/domain/providers/web_extensions_state.dart +++ b/app/lib/features/geckoview/domain/providers/web_extensions_state.dart @@ -21,7 +21,6 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; -import 'package:nullability/nullability.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/domain/entities/equatable_image.dart'; @@ -61,12 +60,12 @@ class WebExtensionsState extends _$WebExtensionsState { title: data.title, enabled: data.enabled ?? current.enabled, badgeText: data.badgeText, - badgeTextColor: data.badgeTextColor.mapNotNull( - (color) => Color(color), - ), - badgeBackgroundColor: data.badgeBackgroundColor.mapNotNull( - (color) => Color(color), - ), + badgeTextColor: data.badgeTextColor != null + ? Color(data.badgeTextColor!) + : null, + badgeBackgroundColor: data.badgeBackgroundColor != null + ? Color(data.badgeBackgroundColor!) + : null, ); } else { 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 { // Dispose all cached images _imageCache.clear(); diff --git a/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart b/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart index 53c46102..7c390817 100644 --- a/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart @@ -62,7 +62,7 @@ final class WebExtensionsStateProvider } String _$webExtensionsStateHash() => - r'28cd9c99bc167fa1a051eccf3d23917a417e54ea'; + r'13cbbea409b5b643eb6766689f6b23a4d1701204'; final class WebExtensionsStateFamily extends $Family with diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart index 43d20a59..b958116a 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart @@ -37,11 +37,7 @@ class ExtensionBadgeIcon extends StatelessWidget { textColor: state.badgeTextColor, backgroundColor: state.badgeBackgroundColor, child: RepaintBoundary( - child: SafeRawImage( - image: state.icon, - width: 24, - height: 24, - ), + child: SafeRawImage(image: state.icon, width: 24, height: 24), ), ); } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt index f8aa3f9c..b3bdbbfc 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/Components.kt @@ -38,7 +38,7 @@ class Components(val profileApplicationContext: ProfileContext, val logLevel: Log.Priority, val contentBlocking: ContentBlocking, val addonCollection: AddonCollection?, - private val addonEvents: GeckoAddonEvents, + val addonEvents: GeckoAddonEvents, private val tabContentEvents: GeckoTabContentEvents, private val extensionEvents: BrowserExtensionEvents ) { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt index fb3ed60e..a7222477 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTabsApiImpl.kt @@ -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.RecoverableTab as PigeonRecoverableTab 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.ext.toWebPBytes import eu.weblibre.flutter_mozilla_components.pigeons.FindResultState @@ -178,6 +180,10 @@ class GeckoTabsApiImpl : GeckoTabsApi { onHistoryStateChange: Boolean, onFindResults: Boolean, onThumbnailChange: Boolean, + onBrowserExtensionsChange: Boolean, + onPageExtensionsChange: Boolean, + onBrowserExtensionIcons: Boolean, + onPageExtensionIcons: Boolean, ) { try { val tabs = components.core.store.state.tabs.map { it.copy() } @@ -274,11 +280,122 @@ class GeckoTabsApiImpl : GeckoTabsApi { coroutineScope.launch { handleThumbnailChange(tab) } } } + + // Sync extension events + if (onBrowserExtensionsChange || onPageExtensionsChange || + onBrowserExtensionIcons || onPageExtensionIcons) { + syncExtensionEvents( + onBrowserExtensionsChange, + onPageExtensionsChange, + onBrowserExtensionIcons, + onPageExtensionIcons + ) + } } catch (e: Exception) { 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) { try { components.useCases.tabsUseCases.selectTab(tabId = tabId) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 5443d4c3..53265a40 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -4344,7 +4344,7 @@ interface GeckoSessionApi { } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ 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 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 @@ -4390,8 +4390,12 @@ interface GeckoTabsApi { val onHistoryStateChangeArg = args[6] as Boolean val onFindResultsArg = args[7] 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 = 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) } catch (exception: Throwable) { GeckoPigeonUtils.wrapError(exception) diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart index 1ec65369..c3ecf35d 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart @@ -25,6 +25,10 @@ class GeckoTabService { bool onHistoryStateChange = false, bool onFindResults = false, bool onThumbnailChange = false, + bool onBrowserExtensionsChange = false, + bool onPageExtensionsChange = false, + bool onBrowserExtensionIcons = false, + bool onPageExtensionIcons = false, }) { return _api.syncEvents( onSelectedTabChange: onSelectedTabChange, @@ -36,6 +40,10 @@ class GeckoTabService { onFindResults: onFindResults, onThumbnailChange: onThumbnailChange, onReaderableStateChange: false, + onBrowserExtensionsChange: onBrowserExtensionsChange, + onPageExtensionsChange: onPageExtensionsChange, + onBrowserExtensionIcons: onBrowserExtensionIcons, + onPageExtensionIcons: onPageExtensionIcons, ); } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 8673e583..3fdee041 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -4935,14 +4935,14 @@ class GeckoTabsApi { final String pigeonVar_messageChannelSuffix; - Future 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 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_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([onSelectedTabChange, onTabListChange, onTabContentStateChange, onIconChange, onSecurityInfoStateChange, onReaderableStateChange, onHistoryStateChange, onFindResults, onThumbnailChange]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([onSelectedTabChange, onTabListChange, onTabContentStateChange, onIconChange, onSecurityInfoStateChange, onReaderableStateChange, onHistoryStateChange, onFindResults, onThumbnailChange, onBrowserExtensionsChange, onPageExtensionsChange, onBrowserExtensionIcons, onPageExtensionIcons]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index c4dca1e1..335d79a2 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1113,6 +1113,10 @@ abstract class GeckoTabsApi { required bool onHistoryStateChange, required bool onFindResults, required bool onThumbnailChange, + required bool onBrowserExtensionsChange, + required bool onPageExtensionsChange, + required bool onBrowserExtensionIcons, + required bool onPageExtensionIcons, }); void selectTab({required String tabId});