intermediate

This commit is contained in:
Fabian Freund
2024-12-12 09:44:33 +01:00
parent 0710116feb
commit 6b2ae28828
108 changed files with 5531 additions and 2265 deletions
@@ -3,7 +3,7 @@ version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "1.9.22"
ext.mozillaComponentsVersion = '133.0.20241024211615'
ext.mozillaComponentsVersion = '134.0.20241030214633'
repositories {
google()
@@ -74,48 +74,6 @@ android {
viewBinding true
}
dependencies {
implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion"
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.12.0'
//https://stackoverflow.com/questions/73782320/onbackinvokedcallback-is-not-enabled-for-the-application-in-set-androidenableo
implementation 'androidx.activity:activity:1.9.3'
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.1.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
@@ -129,6 +87,66 @@ android {
}
}
dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
configurations.configureEach {
exclude group: "org.mozilla.telemetry", module: "glean-native"
}
// Select the Glean from GeckoView.
// `service-sync-logins` requires Glean, which pulls in glean-native,
// but that's also provided by geckoview-omni, so now we need to select which one to use.
project.configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
if (toBeSelected != null) {
select(toBeSelected)
}
because 'use GeckoView Glean instead of standalone Glean'
}
}
dependencies {
implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-toolbar:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-awesomebar:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-storage-sync:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-awesomebar:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion"
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.google.android.material:material:1.12.0'
//https://stackoverflow.com/questions/73782320/onbackinvokedcallback-is-not-enabled-for-the-application-in-set-androidenableo
implementation 'androidx.activity:activity-ktx:1.9.3'
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.2.0")
}
@@ -0,0 +1,47 @@
const port = browser.runtime.connectNative("mozacReaderExtract");
let domChanged = false;
let lastContent = '';
function parseContent() {
const article = window.parseReaderable(document);
if (article && lastContent !== article.textContent) {
port.postMessage(article);
lastContent = article.textContent;
}
domChanged = false;
}
port.onMessage.addListener((message) => {
if (message.action === "parseContent") {
if (domChanged) {
parseContent();
}
}
});
const observer = new MutationObserver(() => {
domChanged = true;
});
const observerConfig = {
childList: true,
subtree: true,
characterData: true,
attributes: false
};
// Wait for page to be fully loaded
if (document.readyState === 'complete') {
parseContent();
observer.observe(document.body, observerConfig);
} else {
window.addEventListener('load', parseContent);
}
window.addEventListener('unload', () => {
observer.disconnect();
});
@@ -0,0 +1,29 @@
{
"manifest_version": 2,
"browser_specific_settings": {
"gecko": {
"id": "readability-extract@movenext.me"
}
},
"name": "Extract text of Websites",
"version": "1.0",
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": [
"readability.min.js",
"content.js"
]
}
],
"permissions": [
"mozillaAddons",
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"storage",
"tabs",
"webNavigation",
"<all_urls>"
]
}
@@ -0,0 +1 @@
../../../../../../javascript/readability/dist/readability.min.js
@@ -15,7 +15,9 @@ import androidx.fragment.app.Fragment
import eu.lensai.flutter_mozilla_components.addons.WebExtensionPromptFeature
import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
import eu.lensai.flutter_mozilla_components.pip.PictureInPictureIntegration
import eu.lensai.flutter_mozilla_components.services.DownloadService
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.app.links.AppLinksFeature
import mozilla.components.feature.downloads.DownloadsFeature
@@ -51,6 +53,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
private val pictureInPictureIntegration = ViewBoundFeatureWrapper<PictureInPictureIntegration>()
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
@@ -251,16 +254,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
fullScreenFeature.set(
feature = FullScreenFeature(
components.core.store,
components.useCases.sessionUseCases,
sessionId,
) { inFullScreen ->
if (inFullScreen) {
activity?.enterImmersiveMode()
} else {
activity?.exitImmersiveMode()
}
},
store = components.core.store,
sessionUseCases = components.useCases.sessionUseCases,
tabId = sessionId,
fullScreenChanged = ::fullScreenChanged,
),
owner = this,
view = binding.root,
)
@@ -275,6 +273,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
view = binding.root,
)
pictureInPictureIntegration.set(
feature = PictureInPictureIntegration(
components.core.store,
requireActivity(),
sessionId,
),
owner = this,
view = view,
)
secureWindowFeature.set(
feature = SecureWindowFeature(
window = requireActivity().window,
@@ -312,11 +320,34 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
return binding.root
}
private fun fullScreenChanged(enabled: Boolean) {
if (enabled) {
activity?.enterImmersiveMode()
} else {
activity?.exitImmersiveMode()
}
}
@CallSuper
override fun onBackPressed(): Boolean {
return backButtonHandler.any { it.onBackPressed() }
}
final override fun onPause() {
pictureInPictureIntegration.get()?.onHomePressed() ?: false
super.onPause()
}
final override fun onPictureInPictureModeChanged(enabled: Boolean) {
val session = components.core.store.state.selectedTab
val fullScreenMode = session?.content?.fullScreen ?: false
// If we're exiting PIP mode and we're in fullscreen mode, then we should exit fullscreen mode as well.
if (!enabled && fullScreenMode) {
onBackPressed()
fullScreenChanged(false)
}
}
@CallSuper
override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) }
@@ -6,6 +6,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
import eu.lensai.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.lensai.flutter_mozilla_components.integration.ReaderViewIntegration
import mozilla.components.browser.state.state.WebExtensionState
@@ -19,15 +20,16 @@ import mozilla.components.support.webextensions.WebExtensionPopupObserver
/**
* Fragment used for browsing the web within the main app.
*/
class BrowserFragment(private val context: Context) : BaseBrowserFragment(), UserInteractionHandler {
class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
private val readabilityExtractFeature = ViewBoundFeatureWrapper<ReadabilityExtractFeature>()
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
override fun createEngine(components: Components): EngineView {
return components.core.engine.createView(context).apply {
return components.core.engine.createView(requireContext()).apply {
selectionActionDelegate = components.selectionAction
}
}
@@ -49,6 +51,12 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
view = view,
)
readabilityExtractFeature.set(
feature = components.features.readabilityExtractFeature,
owner = this,
view = view,
)
windowFeature.set(
feature = WindowFeature(components.core.store, components.useCases.tabsUseCases),
owner = this,
@@ -72,6 +80,8 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
owner = this,
view = view,
)
components.core.historyStorage.registerStorageMaintenanceWorker()
}
private fun openPopup(webExtensionState: WebExtensionState) {
@@ -86,7 +96,7 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
readerViewFeature.onBackPressed() || super.onBackPressed()
companion object {
fun create(context: Context, sessionId: String? = null) = BrowserFragment(context).apply {
fun create(sessionId: String? = null) = BrowserFragment().apply {
arguments = Bundle().apply {
putSessionId(sessionId)
}
@@ -5,10 +5,13 @@ import androidx.core.app.NotificationManagerCompat
import eu.lensai.flutter_mozilla_components.components.Core
import eu.lensai.flutter_mozilla_components.components.Events
import eu.lensai.flutter_mozilla_components.components.Features
import eu.lensai.flutter_mozilla_components.components.Search
import eu.lensai.flutter_mozilla_components.components.Services
import eu.lensai.flutter_mozilla_components.components.UseCases
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.selection.SelectionActionDelegate
@@ -18,13 +21,15 @@ class Components(private val context: Context,
val flutterEvents: GeckoStateEvents,
val readerViewController: ReaderViewController,
val selectionAction: SelectionActionDelegate,
val addonEvents: GeckoAddonEvents
val addonEvents: GeckoAddonEvents,
val tabContentEvents: GeckoTabContentEvents
) {
val core by lazy { Core(context, this, flutterEvents) }
val events by lazy { Events(flutterEvents) }
val useCases by lazy { UseCases(context, core.engine, core.store) }
val services by lazy { Services(context, useCases.tabsUseCases) }
val features by lazy { Features(core.store, addonEvents) }
val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) }
val search by lazy { Search(context, core, useCases) }
var engineView: EngineView? = null
var engineReportedInitialized = false
@@ -32,6 +32,8 @@ object EngineProvider {
// About config it's no longer enabled by default
builder.aboutConfigEnabled(true)
builder.extensionsWebAPIEnabled(true)
builder.consoleOutput(true)
runtime = GeckoRuntime.create(context, builder.build())
}
@@ -12,6 +12,7 @@ import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSuggestionApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
@@ -25,6 +26,9 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionControll
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
@@ -63,13 +67,18 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
ReaderViewController(_flutterPluginBinding.binaryMessenger)
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger)
GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents))
GlobalComponents.setUp(
flutterPluginBinding.applicationContext,
_flutterEvents,
readerViewController,
selectionActionDelegate,
addonEvents
addonEvents,
tabContentEvents,
)
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
@@ -104,7 +113,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
}
// Replace this with your actual Fragment
val nativeFragment = BrowserFragment.create(activity!!)
val nativeFragment = BrowserFragment.create()
val fm = (activity as FragmentActivity).supportFragmentManager
fm.beginTransaction()
@@ -3,11 +3,14 @@ package eu.lensai.flutter_mozilla_components
import android.content.Context
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
import mozilla.components.support.base.facts.Facts
@@ -40,7 +43,8 @@ object GlobalComponents {
flutterEvents: GeckoStateEvents,
readerViewController: ReaderViewController,
selectionAction: SelectionActionDelegate,
addonEvents: GeckoAddonEvents
addonEvents: GeckoAddonEvents,
tabContentEvents: GeckoTabContentEvents
) {
Logger.debug("Creating new components")
@@ -50,6 +54,7 @@ object GlobalComponents {
readerViewController,
selectionAction,
addonEvents,
tabContentEvents
)
//newComponents.crashReporter.install(applicationContext)
@@ -65,6 +70,8 @@ object GlobalComponents {
//newComponents.useCases.downloadsUseCases.restoreDownloads()
try {
GlobalPlacesDependencyProvider.initialize(newComponents.core.historyStorage)
GlobalAddonDependencyProvider.initialize(
newComponents.core.addonManager,
newComponents.core.addonUpdater,
@@ -22,7 +22,6 @@ import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as Pigeon
class GeckoSessionApiImpl : GeckoSessionApi {
companion object {
private const val TAG = "GeckoSessionApi"
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
}
private val components by lazy {
@@ -0,0 +1,57 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestion
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
class GeckoSuggestionApiImpl(
private val suggestionEvents: GeckoSuggestionEvents
) : GeckoSuggestionApi {
companion object {
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
}
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
override fun onInputChanged(text: String, providers: List<GeckoSuggestionType>) {
for(provider in providers) {
coroutineScope.launch {
val results = when(provider) {
GeckoSuggestionType.SESSION -> components.search.sessionSuggestions.onInputChanged(text)
GeckoSuggestionType.CLIPBOARD -> components.search.clipboardSuggestions.onInputChanged(text)
GeckoSuggestionType.HISTORY -> components.search.historySuggestions.onInputChanged(text)
}
val mappedResults = results.map { result ->
GeckoSuggestion(
id = result.id,
title = result.title,
score = result.score.toLong(),
description = result.description,
type = provider,
editSuggestion = result.editSuggestion,
icon = result.icon?.toWebPBytes()
)
}
runOnUiThread {
suggestionEvents.onSuggestionResult(
System.currentTimeMillis(),
provider,
mappedResults
) { }
}
}
}
}
}
@@ -20,6 +20,7 @@ import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.state.engine.EngineMiddleware
import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
import mozilla.components.concept.engine.DefaultSettings
@@ -35,6 +36,7 @@ import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
import mozilla.components.feature.readerview.ReaderViewMiddleware
import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.feature.session.middleware.LastAccessMiddleware
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
import mozilla.components.feature.webnotifications.WebNotificationFeature
@@ -51,12 +53,13 @@ class Core(private val context: Context,
PreferenceManager.getDefaultSharedPreferences(context)
}
val engineSettings by lazy {
private val engineSettings by lazy {
DefaultSettings().apply {
//historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
requestInterceptor = AppRequestInterceptor(context)
remoteDebuggingEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_remote_debugging), false)
testingModeEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_testing_mode), false)
historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
trackingProtectionPolicy = createTrackingProtectionPolicy(prefs)
httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
globalPrivacyControlEnabled = prefs.getBoolean(
@@ -159,6 +162,17 @@ class Core(private val context: Context,
SessionStorage(context, engine)
}
/**
* The storage component to persist browsing history (with the exception of
* private sessions).
*/
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) }
/**
* A convenience accessor to the [PlacesHistoryStorage].
*/
val historyStorage by lazy { lazyHistoryStorage.value }
/**
* Constructs a [TrackingProtectionPolicy] based on current preferences.
*
@@ -1,12 +1,17 @@
package eu.lensai.flutter_mozilla_components.components
import eu.lensai.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
class Features(
private val engine: Engine,
private val store: BrowserStore,
private val addonEvents: GeckoAddonEvents
private val addonEvents: GeckoAddonEvents,
private var tabContentEvents: GeckoTabContentEvents
) {
val webExtensionToolbarFeature by lazy {
WebExtensionToolbarFeature(
@@ -14,4 +19,12 @@ class Features(
addonEvents
)
}
val readabilityExtractFeature by lazy {
ReadabilityExtractFeature(
engine,
store,
tabContentEvents
)
}
}
@@ -0,0 +1,36 @@
package eu.lensai.flutter_mozilla_components.components
import android.content.Context
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
class Search(
private val context: Context,
private val core: Core,
private val useCases: UseCases,
) {
val sessionSuggestions by lazy {
SessionSuggestionProvider(
context.resources,
core.store,
useCases.tabsUseCases.selectTab,
)
}
val clipboardSuggestions by lazy {
ClipboardSuggestionProvider(
context,
useCases.sessionUseCases.loadUrl,
)
}
val historySuggestions by lazy {
HistoryStorageSuggestionProvider(
core.historyStorage,
useCases.sessionUseCases.loadUrl,
core.icons,
core.engine,
)
}
}
@@ -0,0 +1,123 @@
package eu.lensai.flutter_mozilla_components.feature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.TabContent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.org.json.tryGetString
import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.webextensions.WebExtensionController
import org.json.JSONObject
class ReadabilityExtractFeature(
private var engine: Engine,
private var store: BrowserStore,
private var tabContentEvents: GeckoTabContentEvents
): LifecycleAwareFeature {
companion object {
private val logger = Logger("ReadabilityExtract")
internal const val READER_EXTRACT_EXTENSION_ID = "readability-extract@movenext.me"
internal const val READER_EXTRACT_CONTENT_PORT = "mozacReaderExtract"
internal const val READER_EXTRACT_EXTENSION_URL =
"resource://android/assets/extensions/readability_extract/"
}
private class ContentMessageHandler(
private var tabContentEvents: GeckoTabContentEvents,
private var tabId: String
) : MessageHandler {
override fun onPortConnected(port: Port) {
logger.debug("Port connected: ${port.name()}")
}
override fun onPortMessage(message: Any, port: Port) {
if (message is JSONObject) {
tabContentEvents.onContentUpdate(
System.currentTimeMillis(),
contentArg = TabContent(
tabId = tabId,
fullContentMarkdown = message.tryGetString("fullContentMarkdown"),
fullContentPlain = message.tryGetString("fullContentPlain"),
extractedContentMarkdown = message.tryGetString("extractedContentMarkdown"),
extractedContentPlain = message.tryGetString("extractedContentPlain")
)
) {}
}
}
}
private var sessionScope: CoroutineScope? = null
private var historyScope: CoroutineScope? = null
private var extensionController = WebExtensionController(
READER_EXTRACT_EXTENSION_ID,
READER_EXTRACT_EXTENSION_URL,
READER_EXTRACT_CONTENT_PORT,
)
override fun start() {
ensureExtensionInstalled()
}
override fun stop() {
sessionScope?.cancel()
historyScope?.cancel()
}
private fun ensureExtensionInstalled() {
extensionController.install(
engine,
onSuccess = { extension ->
sessionScope = store.flowScoped { flow ->
flow.map { it.tabs }
.filterChanged { it.engineState.engineSession }
.collect { state ->
val engineSession = state.engineState.engineSession ?: return@collect
if (extension.hasContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT)) {
return@collect
}
val handler = ContentMessageHandler(tabContentEvents, state.id)
extension.registerContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT, handler)
}
}
historyScope = store.flowScoped { flow ->
flow.map { it.tabs }
.filterChanged { it.content.history }
.ifAnyChanged { arrayOf(it.content.history.currentIndex) }
.collect { state ->
val engineSession = state.engineState.engineSession ?: return@collect
if(state.content.loading) {
return@collect
}
if (extension.hasContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT)) {
if(extensionController.portConnected(engineSession)) {
extensionController.sendContentMessage(
JSONObject().put("action", "parseContent"),
engineSession
)
}
}
}
}
},
onError = { throwable ->
Logger.error("Could not install $READER_EXTRACT_EXTENSION_ID extension", throwable)
},
)
}
}
@@ -119,7 +119,7 @@ class WebExtensionToolbarFeature(
extension.browserAction?.let { browserAction ->
addOrUpdateAction(
extension = extension,
defaultAction = browserAction,
globalAction = browserAction,
tabAction = tab?.extensionState?.get(extension.id)?.browserAction,
)
}
@@ -132,7 +132,7 @@ class WebExtensionToolbarFeature(
if (pageAction.copyWithOverride(tabPageAction).enabled == true) {
addOrUpdateAction(
extension = extension,
defaultAction = pageAction,
globalAction = pageAction,
tabAction = tabPageAction,
isPageAction = true,
)
@@ -156,62 +156,60 @@ class WebExtensionToolbarFeature(
private fun addOrUpdateAction(
extension: WebExtensionState,
defaultAction: Action,
globalAction: Action,
tabAction: Action?,
isPageAction: Boolean = false,
) {
var action = defaultAction
val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
// Add the global page/browser action if it doesn't exist
var toolbarAction = actionMap.getOrPut(extension.id) {
CoroutineScope(iconJobDispatcher).launch {
try {
//TODO: make variable size
val icon = globalAction.loadIcon?.invoke(128)
icon?.let {
val imageBytes = icon.toWebPBytes()
runOnUiThread {
addonEvents.onUpdateWebExtensionIcon(
timestampArg = System.currentTimeMillis(),
extensionIdArg = extension.id,
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
iconArg = imageBytes
) { }
}
}
} catch (throwable: Throwable) {
Log.log(
Log.Priority.ERROR,
"mozac-webextensions",
throwable,
"Failed to load browser action icon, falling back to default.",
)
}
}
globalAction
}
// Apply tab-specific override of browser/page action
tabAction?.let {
action = action.copyWithOverride(it)
toolbarAction = toolbarAction.copyWithOverride(it)
}
CoroutineScope(iconJobDispatcher).launch {
try {
//TODO: make variable size
val icon = action.loadIcon?.invoke(128)
icon?.let {
val imageBytes = icon.toWebPBytes()
runOnUiThread {
addonEvents.onUpdateWebExtensionIcon(
timestampArg = System.currentTimeMillis(),
extensionIdArg = extension.id,
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
iconArg = imageBytes
) { }
}
}
} catch (throwable: Throwable) {
Log.log(
Log.Priority.ERROR,
"mozac-webextensions",
throwable,
"Failed to load browser action icon, falling back to default.",
)
}
}
val data = WebExtensionData(
extensionId = extension.id,
title = toolbarAction.title,
enabled = toolbarAction.enabled,
badgeText = toolbarAction.badgeText,
badgeTextColor = toolbarAction.badgeTextColor?.toLong(),
badgeBackgroundColor = toolbarAction.badgeBackgroundColor?.toLong(),
)
val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
// Add the global page/browser action if it doesn't exist
val toolbarAction = actionMap.getOrPut(extension.id) {
val toolbarAction = WebExtensionData(
extensionId = extension.id,
title = action.title,
enabled = action.enabled,
badgeText = action.badgeText,
badgeTextColor = action.badgeTextColor?.toLong(),
badgeBackgroundColor = action.badgeBackgroundColor?.toLong(),
)
addonEvents.onUpsertWebExtensionAction(
timestampArg = System.currentTimeMillis(),
extensionIdArg = extension.id,
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
extensionDataArg = toolbarAction
) { }
action
}
addonEvents.onUpsertWebExtensionAction(
timestampArg = System.currentTimeMillis(),
extensionIdArg = extension.id,
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
extensionDataArg = data
) { }
}
}
@@ -30,12 +30,12 @@ class ReaderViewIntegration(
if (enabled) {
feature.showReaderView()
readerViewController.appearanceButtonVisibility(true) { _ -> };
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),true) { _ -> };
} else {
feature.hideReaderView()
feature.hideControls()
readerViewController.appearanceButtonVisibility(false) { _ -> };
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),false) { _ -> };
}
}
@@ -154,6 +154,18 @@ enum class WebExtensionActionType(val raw: Int) {
}
}
enum class GeckoSuggestionType(val raw: Int) {
SESSION(0),
CLIPBOARD(1),
HISTORY(2);
companion object {
fun ofRaw(raw: Int): GeckoSuggestionType? {
return values().firstOrNull { it.raw == raw }
}
}
}
/**
* Translation options that map to the Gecko Translations Options.
*
@@ -968,6 +980,72 @@ data class WebExtensionData (
)
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class GeckoSuggestion (
val id: String,
val type: GeckoSuggestionType,
val score: Long,
val title: String? = null,
val description: String? = null,
val editSuggestion: String? = null,
val icon: ByteArray? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): GeckoSuggestion {
val id = pigeonVar_list[0] as String
val type = pigeonVar_list[1] as GeckoSuggestionType
val score = pigeonVar_list[2] as Long
val title = pigeonVar_list[3] as String?
val description = pigeonVar_list[4] as String?
val editSuggestion = pigeonVar_list[5] as String?
val icon = pigeonVar_list[6] as ByteArray?
return GeckoSuggestion(id, type, score, title, description, editSuggestion, icon)
}
}
fun toList(): List<Any?> {
return listOf(
id,
type,
score,
title,
description,
editSuggestion,
icon,
)
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class TabContent (
val tabId: String,
val fullContentMarkdown: String? = null,
val fullContentPlain: String? = null,
val extractedContentMarkdown: String? = null,
val extractedContentPlain: String? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): TabContent {
val tabId = pigeonVar_list[0] as String
val fullContentMarkdown = pigeonVar_list[1] as String?
val fullContentPlain = pigeonVar_list[2] as String?
val extractedContentMarkdown = pigeonVar_list[3] as String?
val extractedContentPlain = pigeonVar_list[4] as String?
return TabContent(tabId, fullContentMarkdown, fullContentPlain, extractedContentMarkdown, extractedContentPlain)
}
}
fun toList(): List<Any?> {
return listOf(
tabId,
fullContentMarkdown,
fullContentPlain,
extractedContentMarkdown,
extractedContentPlain,
)
}
}
private open class GeckoPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
@@ -1007,130 +1085,145 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
}
}
136.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TranslationOptions.fromList(it)
return (readValue(buffer) as Long?)?.let {
GeckoSuggestionType.ofRaw(it.toInt())
}
}
137.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ReaderState.fromList(it)
TranslationOptions.fromList(it)
}
}
138.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
LastMediaAccessState.fromList(it)
ReaderState.fromList(it)
}
}
139.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HistoryMetadataKey.fromList(it)
LastMediaAccessState.fromList(it)
}
}
140.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PackageCategoryValue.fromList(it)
HistoryMetadataKey.fromList(it)
}
}
141.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ExternalPackage.fromList(it)
PackageCategoryValue.fromList(it)
}
}
142.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
LoadUrlFlagsValue.fromList(it)
ExternalPackage.fromList(it)
}
}
143.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SourceValue.fromList(it)
LoadUrlFlagsValue.fromList(it)
}
}
144.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TabState.fromList(it)
SourceValue.fromList(it)
}
}
145.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
RecoverableTab.fromList(it)
TabState.fromList(it)
}
}
146.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
RecoverableBrowserState.fromList(it)
RecoverableTab.fromList(it)
}
}
147.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
IconRequest.fromList(it)
RecoverableBrowserState.fromList(it)
}
}
148.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ResourceSize.fromList(it)
IconRequest.fromList(it)
}
}
149.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
Resource.fromList(it)
ResourceSize.fromList(it)
}
}
150.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
IconResult.fromList(it)
Resource.fromList(it)
}
}
151.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CookiePartitionKey.fromList(it)
IconResult.fromList(it)
}
}
152.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
Cookie.fromList(it)
CookiePartitionKey.fromList(it)
}
}
153.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HistoryItem.fromList(it)
Cookie.fromList(it)
}
}
154.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HistoryState.fromList(it)
HistoryItem.fromList(it)
}
}
155.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ReaderableState.fromList(it)
HistoryState.fromList(it)
}
}
156.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SecurityInfoState.fromList(it)
ReaderableState.fromList(it)
}
}
157.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TabContentState.fromList(it)
SecurityInfoState.fromList(it)
}
}
158.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
FindResultState.fromList(it)
TabContentState.fromList(it)
}
}
159.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CustomSelectionAction.fromList(it)
FindResultState.fromList(it)
}
}
160.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CustomSelectionAction.fromList(it)
}
}
161.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
WebExtensionData.fromList(it)
}
}
162.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
GeckoSuggestion.fromList(it)
}
}
163.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TabContent.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
@@ -1164,106 +1257,118 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
stream.write(135)
writeValue(stream, value.raw)
}
is TranslationOptions -> {
is GeckoSuggestionType -> {
stream.write(136)
writeValue(stream, value.toList())
writeValue(stream, value.raw)
}
is ReaderState -> {
is TranslationOptions -> {
stream.write(137)
writeValue(stream, value.toList())
}
is LastMediaAccessState -> {
is ReaderState -> {
stream.write(138)
writeValue(stream, value.toList())
}
is HistoryMetadataKey -> {
is LastMediaAccessState -> {
stream.write(139)
writeValue(stream, value.toList())
}
is PackageCategoryValue -> {
is HistoryMetadataKey -> {
stream.write(140)
writeValue(stream, value.toList())
}
is ExternalPackage -> {
is PackageCategoryValue -> {
stream.write(141)
writeValue(stream, value.toList())
}
is LoadUrlFlagsValue -> {
is ExternalPackage -> {
stream.write(142)
writeValue(stream, value.toList())
}
is SourceValue -> {
is LoadUrlFlagsValue -> {
stream.write(143)
writeValue(stream, value.toList())
}
is TabState -> {
is SourceValue -> {
stream.write(144)
writeValue(stream, value.toList())
}
is RecoverableTab -> {
is TabState -> {
stream.write(145)
writeValue(stream, value.toList())
}
is RecoverableBrowserState -> {
is RecoverableTab -> {
stream.write(146)
writeValue(stream, value.toList())
}
is IconRequest -> {
is RecoverableBrowserState -> {
stream.write(147)
writeValue(stream, value.toList())
}
is ResourceSize -> {
is IconRequest -> {
stream.write(148)
writeValue(stream, value.toList())
}
is Resource -> {
is ResourceSize -> {
stream.write(149)
writeValue(stream, value.toList())
}
is IconResult -> {
is Resource -> {
stream.write(150)
writeValue(stream, value.toList())
}
is CookiePartitionKey -> {
is IconResult -> {
stream.write(151)
writeValue(stream, value.toList())
}
is Cookie -> {
is CookiePartitionKey -> {
stream.write(152)
writeValue(stream, value.toList())
}
is HistoryItem -> {
is Cookie -> {
stream.write(153)
writeValue(stream, value.toList())
}
is HistoryState -> {
is HistoryItem -> {
stream.write(154)
writeValue(stream, value.toList())
}
is ReaderableState -> {
is HistoryState -> {
stream.write(155)
writeValue(stream, value.toList())
}
is SecurityInfoState -> {
is ReaderableState -> {
stream.write(156)
writeValue(stream, value.toList())
}
is TabContentState -> {
is SecurityInfoState -> {
stream.write(157)
writeValue(stream, value.toList())
}
is FindResultState -> {
is TabContentState -> {
stream.write(158)
writeValue(stream, value.toList())
}
is CustomSelectionAction -> {
is FindResultState -> {
stream.write(159)
writeValue(stream, value.toList())
}
is WebExtensionData -> {
is CustomSelectionAction -> {
stream.write(160)
writeValue(stream, value.toList())
}
is WebExtensionData -> {
stream.write(161)
writeValue(stream, value.toList())
}
is GeckoSuggestion -> {
stream.write(162)
writeValue(stream, value.toList())
}
is TabContent -> {
stream.write(163)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
@@ -2553,12 +2658,12 @@ class ReaderViewController(private val binaryMessenger: BinaryMessenger, private
GeckoPigeonCodec()
}
}
fun appearanceButtonVisibility(visibleArg: Boolean, callback: (Result<Unit>) -> Unit)
fun appearanceButtonVisibility(timestampArg: Long, visibleArg: Boolean, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(visibleArg)) {
channel.send(listOf(timestampArg, visibleArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2743,3 +2848,90 @@ class GeckoAddonEvents(private val binaryMessenger: BinaryMessenger, private val
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoSuggestionApi {
fun onInputChanged(text: String, providers: List<GeckoSuggestionType>)
companion object {
/** The codec used by GeckoSuggestionApi. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
/** Sets up an instance of `GeckoSuggestionApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoSuggestionApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.onInputChanged$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val textArg = args[0] as String
val providersArg = args[1] as List<GeckoSuggestionType>
val wrapped: List<Any?> = try {
api.onInputChanged(textArg, providersArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class GeckoSuggestionEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by GeckoSuggestionEvents. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
}
fun onSuggestionResult(timestampArg: Long, suggestionTypeArg: GeckoSuggestionType, suggestionsArg: List<GeckoSuggestion>, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, suggestionTypeArg, suggestionsArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(createConnectionError(channelName)))
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class GeckoTabContentEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by GeckoTabContentEvents. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
}
fun onContentUpdate(timestampArg: Long, contentArg: TabContent, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, contentArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(createConnectionError(channelName)))
}
}
}
}
@@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package eu.lensai.flutter_mozilla_components.pip
import android.app.Activity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.session.PictureInPictureFeature
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
class PictureInPictureIntegration(
private val store: BrowserStore,
activity: Activity,
private val customTabId: String?,
private val whiteList: List<String> = listOf("youtube.com/tv"),
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
private val pictureFeature = PictureInPictureFeature(store, activity)
private var whiteListed = false
override fun start() {
scope = store.flowScoped { flow ->
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabId) }
.distinctUntilChangedBy { it.content.url }
.collect { whiteListed = isWhitelisted(it.content.url) }
}
}
override fun stop() {
scope?.cancel()
}
fun onHomePressed() = if (whiteListed) {
pictureFeature.enterPipModeCompat()
} else {
pictureFeature.onHomePressed()
}
private fun isWhitelisted(url: String): Boolean {
val exists = whiteList.firstOrNull { url.contains(it) }
return exists != null
}
}
@@ -48,6 +48,7 @@
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:supportsPictureInPicture="true"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
@@ -0,0 +1,25 @@
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Compiled output
build/
# Optional npm cache directory
.npm/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
{
"name": "readability",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@mozilla/readability": "^0.5.0",
"remove-markdown": "^0.5.5",
"turndown": "^7.2.0"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"babel-loader": "^9.2.1",
"clean-webpack-plugin": "^4.0.0",
"terser-webpack-plugin": "^5.3.10",
"webpack": "^5.96.1",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4"
}
}
@@ -0,0 +1,35 @@
import { Readability } from '@mozilla/readability';
import TurndownService from 'turndown';
const removeMarkdown = require('remove-markdown');
const turndownService = new TurndownService();
function parseReaderable(document, options) {
const clonedDoc = document.cloneNode(true);
const fullMarkdown = turndownService.turndown(clonedDoc.body.innerHTML);
let response = {
fullContentMarkdown: fullMarkdown,
fullContentPlain: removeMarkdown(fullMarkdown),
}
const reader = new Readability(clonedDoc, options);
const article = reader.parse();
if(article != null) {
const extractedMarkdown = turndownService.turndown(article.content);
response = {
...response,
extractedContentMarkdown: extractedMarkdown,
extractedContentPlain: removeMarkdown(extractedMarkdown)
};
}
return response;
}
window.parseReaderable = parseReaderable;
export { parseReaderable };
@@ -0,0 +1,49 @@
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'readability.min.js',
library: 'ReadabilityBundle',
libraryTarget: 'umd',
globalObject: 'this'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false
})
],
usedExports: true,
sideEffects: true
},
plugins: [
new CleanWebpackPlugin(),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
generateStatsFile: true,
statsOptions: { source: false }
})
]
};
@@ -11,17 +11,22 @@ export 'src/domain/services/gecko_icon.dart';
export 'src/domain/services/gecko_readerable.dart';
export 'src/domain/services/gecko_selection_action.dart';
export 'src/domain/services/gecko_session.dart';
export 'src/domain/services/gecko_suggestions.dart';
export 'src/domain/services/gecko_tab.dart';
export 'src/domain/services/gecko_tab_content.dart';
export 'src/geckoview_widget.dart';
export 'src/pigeons/gecko.g.dart'
show
CookieSameSiteStatus,
GeckoSuggestion,
GeckoSuggestionType,
HistoryMetadataKey,
IconSource,
IconType,
Resource,
ResourceSize,
SecurityInfoState,
TabContent,
TabContentState,
WebExtensionActionType,
WebExtensionData;
@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/src/extensions/subject.dart';
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
import 'package:rxdart/rxdart.dart';
@@ -8,7 +9,6 @@ class GeckoReaderableService extends ReaderViewController {
final ReaderViewEvents _events;
final _appearanceVisibility = BehaviorSubject<bool>();
final _readerVisibility = BehaviorSubject<bool>();
Stream<bool> get appearanceVisibility => _appearanceVisibility.stream;
@@ -21,8 +21,8 @@ class GeckoReaderableService extends ReaderViewController {
}
@override
void appearanceButtonVisibility(bool visible) {
_appearanceVisibility.add(visible);
void appearanceButtonVisibility(int timestamp, bool visible) {
_appearanceVisibility.addWhenMoreRecent(timestamp, null, visible);
}
GeckoReaderableService.setUp({
@@ -43,6 +43,5 @@ class GeckoReaderableService extends ReaderViewController {
void dispose() {
unawaited(_appearanceVisibility.close());
unawaited(_readerVisibility.close());
}
}
@@ -0,0 +1,56 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/src/extensions/subject.dart';
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
import 'package:rxdart/rxdart.dart';
final _apiInstance = GeckoSuggestionApi();
class GeckoSuggestionsService extends GeckoSuggestionEvents {
final GeckoSuggestionApi _api;
final _suggestionsSubject = BehaviorSubject<List<GeckoSuggestion>>();
Stream<List<GeckoSuggestion>> get suggestionsStream =>
_suggestionsSubject.stream;
GeckoSuggestionsService.setUp({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
GeckoSuggestionApi? api,
}) : _api = api ?? _apiInstance {
GeckoSuggestionEvents.setUp(
this,
binaryMessenger: binaryMessenger,
messageChannelSuffix: messageChannelSuffix,
);
}
Future<void> onInputChanged(
String text, {
List<GeckoSuggestionType> providers = const [
GeckoSuggestionType.session,
GeckoSuggestionType.clipboard,
GeckoSuggestionType.history,
],
}) {
return _api.onInputChanged(text, providers);
}
@override
void onSuggestionResult(
int timestamp,
GeckoSuggestionType suggestionType,
List<GeckoSuggestion> suggestions,
) {
_suggestionsSubject.addWhenMoreRecent(
timestamp,
suggestionType,
suggestions,
);
}
void dispose() {
unawaited(_suggestionsSubject.close());
}
}
@@ -0,0 +1,32 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/src/extensions/subject.dart';
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
import 'package:rxdart/rxdart.dart';
class GeckoTabContentService extends GeckoTabContentEvents {
final _contentSubject = BehaviorSubject<TabContent>();
Stream<TabContent> get tabContentStream => _contentSubject.stream;
GeckoTabContentService.setUp({
BinaryMessenger? binaryMessenger,
String messageChannelSuffix = '',
}) {
GeckoTabContentEvents.setUp(
this,
binaryMessenger: binaryMessenger,
messageChannelSuffix: messageChannelSuffix,
);
}
@override
void onContentUpdate(int timestamp, TabContent content) {
_contentSubject.addWhenMoreRecent(timestamp, content.tabId, content);
}
void dispose() {
unawaited(_contentSubject.close());
}
}
@@ -88,6 +88,12 @@ enum WebExtensionActionType {
page,
}
enum GeckoSuggestionType {
session,
clipboard,
history,
}
/// Translation options that map to the Gecko Translations Options.
///
/// @property downloadModel If the necessary models should be downloaded on request. If false, then
@@ -1073,6 +1079,98 @@ class WebExtensionData {
}
}
class GeckoSuggestion {
GeckoSuggestion({
required this.id,
required this.type,
required this.score,
this.title,
this.description,
this.editSuggestion,
this.icon,
});
String id;
GeckoSuggestionType type;
int score;
String? title;
String? description;
String? editSuggestion;
Uint8List? icon;
Object encode() {
return <Object?>[
id,
type,
score,
title,
description,
editSuggestion,
icon,
];
}
static GeckoSuggestion decode(Object result) {
result as List<Object?>;
return GeckoSuggestion(
id: result[0]! as String,
type: result[1]! as GeckoSuggestionType,
score: result[2]! as int,
title: result[3] as String?,
description: result[4] as String?,
editSuggestion: result[5] as String?,
icon: result[6] as Uint8List?,
);
}
}
class TabContent {
TabContent({
required this.tabId,
this.fullContentMarkdown,
this.fullContentPlain,
this.extractedContentMarkdown,
this.extractedContentPlain,
});
String tabId;
String? fullContentMarkdown;
String? fullContentPlain;
String? extractedContentMarkdown;
String? extractedContentPlain;
Object encode() {
return <Object?>[
tabId,
fullContentMarkdown,
fullContentPlain,
extractedContentMarkdown,
extractedContentPlain,
];
}
static TabContent decode(Object result) {
result as List<Object?>;
return TabContent(
tabId: result[0]! as String,
fullContentMarkdown: result[1] as String?,
fullContentPlain: result[2] as String?,
extractedContentMarkdown: result[3] as String?,
extractedContentPlain: result[4] as String?,
);
}
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@@ -1102,81 +1200,90 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is WebExtensionActionType) {
buffer.putUint8(135);
writeValue(buffer, value.index);
} else if (value is TranslationOptions) {
} else if (value is GeckoSuggestionType) {
buffer.putUint8(136);
writeValue(buffer, value.encode());
} else if (value is ReaderState) {
writeValue(buffer, value.index);
} else if (value is TranslationOptions) {
buffer.putUint8(137);
writeValue(buffer, value.encode());
} else if (value is LastMediaAccessState) {
} else if (value is ReaderState) {
buffer.putUint8(138);
writeValue(buffer, value.encode());
} else if (value is HistoryMetadataKey) {
} else if (value is LastMediaAccessState) {
buffer.putUint8(139);
writeValue(buffer, value.encode());
} else if (value is PackageCategoryValue) {
} else if (value is HistoryMetadataKey) {
buffer.putUint8(140);
writeValue(buffer, value.encode());
} else if (value is ExternalPackage) {
} else if (value is PackageCategoryValue) {
buffer.putUint8(141);
writeValue(buffer, value.encode());
} else if (value is LoadUrlFlagsValue) {
} else if (value is ExternalPackage) {
buffer.putUint8(142);
writeValue(buffer, value.encode());
} else if (value is SourceValue) {
} else if (value is LoadUrlFlagsValue) {
buffer.putUint8(143);
writeValue(buffer, value.encode());
} else if (value is TabState) {
} else if (value is SourceValue) {
buffer.putUint8(144);
writeValue(buffer, value.encode());
} else if (value is RecoverableTab) {
} else if (value is TabState) {
buffer.putUint8(145);
writeValue(buffer, value.encode());
} else if (value is RecoverableBrowserState) {
} else if (value is RecoverableTab) {
buffer.putUint8(146);
writeValue(buffer, value.encode());
} else if (value is IconRequest) {
} else if (value is RecoverableBrowserState) {
buffer.putUint8(147);
writeValue(buffer, value.encode());
} else if (value is ResourceSize) {
} else if (value is IconRequest) {
buffer.putUint8(148);
writeValue(buffer, value.encode());
} else if (value is Resource) {
} else if (value is ResourceSize) {
buffer.putUint8(149);
writeValue(buffer, value.encode());
} else if (value is IconResult) {
} else if (value is Resource) {
buffer.putUint8(150);
writeValue(buffer, value.encode());
} else if (value is CookiePartitionKey) {
} else if (value is IconResult) {
buffer.putUint8(151);
writeValue(buffer, value.encode());
} else if (value is Cookie) {
} else if (value is CookiePartitionKey) {
buffer.putUint8(152);
writeValue(buffer, value.encode());
} else if (value is HistoryItem) {
} else if (value is Cookie) {
buffer.putUint8(153);
writeValue(buffer, value.encode());
} else if (value is HistoryState) {
} else if (value is HistoryItem) {
buffer.putUint8(154);
writeValue(buffer, value.encode());
} else if (value is ReaderableState) {
} else if (value is HistoryState) {
buffer.putUint8(155);
writeValue(buffer, value.encode());
} else if (value is SecurityInfoState) {
} else if (value is ReaderableState) {
buffer.putUint8(156);
writeValue(buffer, value.encode());
} else if (value is TabContentState) {
} else if (value is SecurityInfoState) {
buffer.putUint8(157);
writeValue(buffer, value.encode());
} else if (value is FindResultState) {
} else if (value is TabContentState) {
buffer.putUint8(158);
writeValue(buffer, value.encode());
} else if (value is CustomSelectionAction) {
} else if (value is FindResultState) {
buffer.putUint8(159);
writeValue(buffer, value.encode());
} else if (value is WebExtensionData) {
} else if (value is CustomSelectionAction) {
buffer.putUint8(160);
writeValue(buffer, value.encode());
} else if (value is WebExtensionData) {
buffer.putUint8(161);
writeValue(buffer, value.encode());
} else if (value is GeckoSuggestion) {
buffer.putUint8(162);
writeValue(buffer, value.encode());
} else if (value is TabContent) {
buffer.putUint8(163);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@@ -1207,55 +1314,62 @@ class _PigeonCodec extends StandardMessageCodec {
final int? value = readValue(buffer) as int?;
return value == null ? null : WebExtensionActionType.values[value];
case 136:
return TranslationOptions.decode(readValue(buffer)!);
final int? value = readValue(buffer) as int?;
return value == null ? null : GeckoSuggestionType.values[value];
case 137:
return ReaderState.decode(readValue(buffer)!);
return TranslationOptions.decode(readValue(buffer)!);
case 138:
return LastMediaAccessState.decode(readValue(buffer)!);
return ReaderState.decode(readValue(buffer)!);
case 139:
return HistoryMetadataKey.decode(readValue(buffer)!);
return LastMediaAccessState.decode(readValue(buffer)!);
case 140:
return PackageCategoryValue.decode(readValue(buffer)!);
return HistoryMetadataKey.decode(readValue(buffer)!);
case 141:
return ExternalPackage.decode(readValue(buffer)!);
return PackageCategoryValue.decode(readValue(buffer)!);
case 142:
return LoadUrlFlagsValue.decode(readValue(buffer)!);
return ExternalPackage.decode(readValue(buffer)!);
case 143:
return SourceValue.decode(readValue(buffer)!);
return LoadUrlFlagsValue.decode(readValue(buffer)!);
case 144:
return TabState.decode(readValue(buffer)!);
return SourceValue.decode(readValue(buffer)!);
case 145:
return RecoverableTab.decode(readValue(buffer)!);
return TabState.decode(readValue(buffer)!);
case 146:
return RecoverableBrowserState.decode(readValue(buffer)!);
return RecoverableTab.decode(readValue(buffer)!);
case 147:
return IconRequest.decode(readValue(buffer)!);
return RecoverableBrowserState.decode(readValue(buffer)!);
case 148:
return ResourceSize.decode(readValue(buffer)!);
return IconRequest.decode(readValue(buffer)!);
case 149:
return Resource.decode(readValue(buffer)!);
return ResourceSize.decode(readValue(buffer)!);
case 150:
return IconResult.decode(readValue(buffer)!);
return Resource.decode(readValue(buffer)!);
case 151:
return CookiePartitionKey.decode(readValue(buffer)!);
return IconResult.decode(readValue(buffer)!);
case 152:
return Cookie.decode(readValue(buffer)!);
return CookiePartitionKey.decode(readValue(buffer)!);
case 153:
return HistoryItem.decode(readValue(buffer)!);
return Cookie.decode(readValue(buffer)!);
case 154:
return HistoryState.decode(readValue(buffer)!);
return HistoryItem.decode(readValue(buffer)!);
case 155:
return ReaderableState.decode(readValue(buffer)!);
return HistoryState.decode(readValue(buffer)!);
case 156:
return SecurityInfoState.decode(readValue(buffer)!);
return ReaderableState.decode(readValue(buffer)!);
case 157:
return TabContentState.decode(readValue(buffer)!);
return SecurityInfoState.decode(readValue(buffer)!);
case 158:
return FindResultState.decode(readValue(buffer)!);
return TabContentState.decode(readValue(buffer)!);
case 159:
return CustomSelectionAction.decode(readValue(buffer)!);
return FindResultState.decode(readValue(buffer)!);
case 160:
return CustomSelectionAction.decode(readValue(buffer)!);
case 161:
return WebExtensionData.decode(readValue(buffer)!);
case 162:
return GeckoSuggestion.decode(readValue(buffer)!);
case 163:
return TabContent.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
@@ -2812,7 +2926,7 @@ class ReaderViewEvents {
abstract class ReaderViewController {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
void appearanceButtonVisibility(bool visible);
void appearanceButtonVisibility(int timestamp, bool visible);
static void setUp(ReaderViewController? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
@@ -2827,11 +2941,14 @@ abstract class ReaderViewController {
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility was null.');
final List<Object?> args = (message as List<Object?>?)!;
final bool? arg_visible = (args[0] as bool?);
final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility was null, expected non-null int.');
final bool? arg_visible = (args[1] as bool?);
assert(arg_visible != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility was null, expected non-null bool.');
try {
api.appearanceButtonVisibility(arg_visible!);
api.appearanceButtonVisibility(arg_timestamp!, arg_visible!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
@@ -3088,3 +3205,118 @@ abstract class GeckoAddonEvents {
}
}
}
class GeckoSuggestionApi {
/// Constructor for [GeckoSuggestionApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
GeckoSuggestionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<void> onInputChanged(String text, List<GeckoSuggestionType> providers) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.onInputChanged$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(<Object?>[text, providers]) as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
}
abstract class GeckoSuggestionEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
void onSuggestionResult(int timestamp, GeckoSuggestionType suggestionType, List<GeckoSuggestion> suggestions);
static void setUp(GeckoSuggestionEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult was null, expected non-null int.');
final GeckoSuggestionType? arg_suggestionType = (args[1] as GeckoSuggestionType?);
assert(arg_suggestionType != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult was null, expected non-null GeckoSuggestionType.');
final List<GeckoSuggestion>? arg_suggestions = (args[2] as List<Object?>?)?.cast<GeckoSuggestion>();
assert(arg_suggestions != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult was null, expected non-null List<GeckoSuggestion>.');
try {
api.onSuggestionResult(arg_timestamp!, arg_suggestionType!, arg_suggestions!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
abstract class GeckoTabContentEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
void onContentUpdate(int timestamp, TabContent content);
static void setUp(GeckoTabContentEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate was null, expected non-null int.');
final TabContent? arg_content = (args[1] as TabContent?);
assert(arg_content != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate was null, expected non-null TabContent.');
try {
api.onContentUpdate(arg_timestamp!, arg_content!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
}
}
@@ -499,6 +499,44 @@ class WebExtensionData {
);
}
enum GeckoSuggestionType { session, clipboard, history }
class GeckoSuggestion {
final String id;
final GeckoSuggestionType type;
final int score;
final String? title;
final String? description;
final String? editSuggestion;
final Uint8List? icon;
GeckoSuggestion(
this.id,
this.type,
this.score,
this.title,
this.description,
this.editSuggestion,
this.icon,
);
}
class TabContent {
final String tabId;
final String? fullContentMarkdown;
final String? fullContentPlain;
final String? extractedContentMarkdown;
final String? extractedContentPlain;
TabContent(
this.tabId,
this.fullContentMarkdown,
this.fullContentPlain,
this.extractedContentMarkdown,
this.extractedContentPlain,
);
}
// /// Represents all the different supported types of data that can be found from long clicking
// /// an element.
// sealed class HitResult {
@@ -843,7 +881,7 @@ abstract class ReaderViewEvents {
@FlutterApi()
abstract class ReaderViewController {
void appearanceButtonVisibility(bool visible);
void appearanceButtonVisibility(int timestamp, bool visible);
}
@HostApi()
@@ -885,3 +923,25 @@ abstract class GeckoAddonEvents {
Uint8List icon,
);
}
@HostApi()
abstract class GeckoSuggestionApi {
void onInputChanged(String text, List<GeckoSuggestionType> providers);
}
@FlutterApi()
abstract class GeckoSuggestionEvents {
void onSuggestionResult(
int timestamp,
GeckoSuggestionType suggestionType,
List<GeckoSuggestion> suggestions,
);
}
@FlutterApi()
abstract class GeckoTabContentEvents {
void onContentUpdate(
int timestamp,
TabContent content,
);
}