From e8879c1d44097a342a50cd1d26fff8c5dbe31ffe Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 29 Aug 2025 13:37:35 +0200 Subject: [PATCH 01/14] prepare new release --- CHANGELOG.md | 7 +++++++ app/pubspec.yaml | 2 +- metadata/en-US/changelogs/26.txt | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 metadata/en-US/changelogs/26.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 75053552..c190e0e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.9.22 +### GeckoView 142.0 + +* Added setting to use third party CA certificates +* Added setting to control tab bar swipe behavior +* Downgraded AGP for F-Droid compatibility + ## 0.9.21 ### GeckoView 142.0 diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 15f79ec0..88c163d3 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,7 @@ name: weblibre description: "The Privacy-Focused & AI-Powered Research Browser" publish_to: 'none' resolution: workspace -version: 0.9.22-alpha-2+25 +version: 0.9.22+26 environment: sdk: '>=3.8.0 <4.0.0' diff --git a/metadata/en-US/changelogs/26.txt b/metadata/en-US/changelogs/26.txt new file mode 100644 index 00000000..fcca9c44 --- /dev/null +++ b/metadata/en-US/changelogs/26.txt @@ -0,0 +1,5 @@ +GeckoView 142.0 + +* Added setting to use third party CA certificates +* Added setting to control tab bar swipe behavior +* Downgraded AGP for F-Droid compatibility \ No newline at end of file From 58e2fe671c62f76c8ad864c8965f0eec21a3b844 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 10 Sep 2025 08:35:47 +0200 Subject: [PATCH 02/14] imporved uri aprsing for localhost and ports --- app/lib/utils/uri_parser.dart | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/lib/utils/uri_parser.dart b/app/lib/utils/uri_parser.dart index fda07830..78de5944 100644 --- a/app/lib/utils/uri_parser.dart +++ b/app/lib/utils/uri_parser.dart @@ -28,10 +28,21 @@ Uri? tryParseUrl(String? input, {bool eagerParsing = false}) { if (uri != null) { if (uri.authority.isEmpty && eagerParsing) { if (uri.pathSegments.isNotEmpty) { - if (_domainRegex.hasMatch(uri.pathSegments.first)) { - uri = Uri.tryParse('https://$input'); - } else if (InternetAddress.tryParse(uri.pathSegments.first) != null) { - uri = Uri.tryParse('https://$input'); + int? port; + var firstSegment = uri.pathSegments.first; + + //When there is no scheme while aprsing, the port becomse the first segment because : is treated as delimeter + if (int.tryParse(firstSegment) case final int segmentPort) { + port = segmentPort; + firstSegment = uri.scheme; + } + + if (_domainRegex.hasMatch(firstSegment)) { + uri = Uri.tryParse('https://$input')?.replace(port: port); + } else if (firstSegment == 'localhost') { + uri = Uri.tryParse('https://$input')?.replace(port: port); + } else if (InternetAddress.tryParse(firstSegment) != null) { + uri = Uri.tryParse('https://$input')?.replace(port: port); } } } From 97b38e75a576bd797101d54a251cee7442212260 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 10 Sep 2025 08:36:11 +0200 Subject: [PATCH 03/14] disable autocorrect for address by default --- app/lib/presentation/widgets/auto_suggest_text_field.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/lib/presentation/widgets/auto_suggest_text_field.dart b/app/lib/presentation/widgets/auto_suggest_text_field.dart index 9736f29c..d26ff106 100644 --- a/app/lib/presentation/widgets/auto_suggest_text_field.dart +++ b/app/lib/presentation/widgets/auto_suggest_text_field.dart @@ -49,6 +49,7 @@ class AutoSuggestTextField extends HookWidget { final bool? enableIMEPersonalizedLearning; final TapRegionCallback? onTapOutside; final VoidCallback? onTap; + final bool autocorrect; const AutoSuggestTextField({ super.key, @@ -77,6 +78,7 @@ class AutoSuggestTextField extends HookWidget { this.enableIMEPersonalizedLearning = true, this.onTapOutside, this.onTap, + this.autocorrect = false, }); bool _suggestionHasMatch() => @@ -216,6 +218,7 @@ class AutoSuggestTextField extends HookWidget { onChanged: onChanged, onEditingComplete: onEditingComplete, validator: validator, + autocorrect: autocorrect, onFieldSubmitted: onSubmitted.mapNotNull( (onSubmitted) => (value) { if (_suggestionHasMatch()) { From 82f687b5b1fee86f58cd4e5d59aebf2385305ad2 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 10 Sep 2025 08:45:52 +0200 Subject: [PATCH 04/14] try to address context issues after inactivity --- .../presentation/widgets/sheets/view_tab.dart | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart index 66cb3348..c8f25f29 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart @@ -86,6 +86,8 @@ class ViewTabSheetWidget extends HookConsumerWidget { final scrolledTo = useRef(0.0); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!context.mounted) return; + final header = headerKey.currentContext?.findRenderObject(); final text = textFieldKey.currentContext?.findRenderObject(); @@ -96,14 +98,16 @@ class ViewTabSheetWidget extends HookConsumerWidget { final relative = totalHeight / MediaQuery.of(context).size.height; - if (draggableScrollableController.size < relative && - relative > scrolledTo.value) { - await draggableScrollableController.animateTo( - relative, - duration: const Duration(milliseconds: 150), - curve: Curves.easeInOut, - ); - scrolledTo.value = relative; + if (relative >= 0 && relative <= 1) { + if (draggableScrollableController.size < relative && + relative > scrolledTo.value) { + await draggableScrollableController.animateTo( + relative, + duration: const Duration(milliseconds: 150), + curve: Curves.easeInOut, + ); + scrolledTo.value = relative; + } } } } @@ -113,22 +117,28 @@ class ViewTabSheetWidget extends HookConsumerWidget { }); final bottomInsets = useRef(0.0); - useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - final diff = - ((MediaQuery.of(context).viewInsets.bottom / 2) / - MediaQuery.of(context).size.height) - - bottomInsets.value; + useEffect( + () { + WidgetsBinding.instance.addPostFrameCallback((_) { + final diff = + ((MediaQuery.of(context).viewInsets.bottom / 2) / + MediaQuery.of(context).size.height) - + bottomInsets.value; - draggableScrollableController.jumpTo( - draggableScrollableController.size + diff, - ); + draggableScrollableController.jumpTo( + draggableScrollableController.size + diff, + ); - bottomInsets.value += diff; - }); + bottomInsets.value += diff; + }); - return null; - }, [MediaQuery.of(context).viewInsets.bottom]); + return null; + }, + [ + MediaQuery.of(context).viewInsets.bottom, + MediaQuery.of(context).size.height, + ], + ); return NestedScrollView( physics: const NeverScrollableScrollPhysics(), From 4f37be28e69c8e6b5dc87ccfc8810808dd035d29 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 10 Sep 2025 10:22:34 +0200 Subject: [PATCH 05/14] clear snackbars before showing new ones --- app/lib/utils/ui_helper.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/lib/utils/ui_helper.dart b/app/lib/utils/ui_helper.dart index b6c1be88..a85780d8 100644 --- a/app/lib/utils/ui_helper.dart +++ b/app/lib/utils/ui_helper.dart @@ -116,6 +116,8 @@ void showTabSwitchMessage( String? tabName, void Function()? onSwitch, }) { + ScaffoldMessenger.of(context).clearSnackBars(); + final message = switch (tabName.whenNotEmpty) { String() => "New tab '$tabName' opened", null => 'New tab opened', @@ -155,6 +157,8 @@ void showTabUndoClose( int count = 1, Duration duration = const Duration(seconds: 2), }) { + ScaffoldMessenger.of(context).clearSnackBars(); + final snackBar = SnackBar( content: (count > 1) ? Text('$count Tabs closed') From 6622dc5b27575f6143a582537bbad95472705421 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 10 Sep 2025 14:47:30 +0200 Subject: [PATCH 06/14] make eager parsing required --- .../features/web_feed/presentation/add_feed_dialog.dart | 6 +++++- .../features/web_feed/presentation/screens/feed_edit.dart | 8 +++++++- app/lib/utils/form_validators.dart | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/lib/features/web_feed/presentation/add_feed_dialog.dart b/app/lib/features/web_feed/presentation/add_feed_dialog.dart index fc608593..a3fd6c17 100644 --- a/app/lib/features/web_feed/presentation/add_feed_dialog.dart +++ b/app/lib/features/web_feed/presentation/add_feed_dialog.dart @@ -53,7 +53,11 @@ class AddFeedDialog extends HookConsumerWidget { controller: textController, keyboardType: TextInputType.url, validator: (value) { - return validateUrl(value, onlyHttpProtocol: true); + return validateUrl( + value, + onlyHttpProtocol: true, + eagerParsing: false, + ); }, ), ), diff --git a/app/lib/features/web_feed/presentation/screens/feed_edit.dart b/app/lib/features/web_feed/presentation/screens/feed_edit.dart index d4e3bee1..9fd4836a 100644 --- a/app/lib/features/web_feed/presentation/screens/feed_edit.dart +++ b/app/lib/features/web_feed/presentation/screens/feed_edit.dart @@ -233,6 +233,7 @@ class _FeedEditContent extends HookConsumerWidget { value, onlyHttpProtocol: true, required: false, + eagerParsing: false, ); }, ), @@ -249,6 +250,7 @@ class _FeedEditContent extends HookConsumerWidget { value, onlyHttpProtocol: true, required: false, + eagerParsing: false, ); }, ), @@ -269,7 +271,11 @@ class _FeedEditContent extends HookConsumerWidget { controller: urlTextController, autovalidateMode: AutovalidateMode.onUserInteraction, validator: (value) { - return validateUrl(value, onlyHttpProtocol: true); + return validateUrl( + value, + onlyHttpProtocol: true, + eagerParsing: false, + ); }, ), ], diff --git a/app/lib/utils/form_validators.dart b/app/lib/utils/form_validators.dart index 1fe2fa5f..250a8546 100644 --- a/app/lib/utils/form_validators.dart +++ b/app/lib/utils/form_validators.dart @@ -22,8 +22,8 @@ import 'package:weblibre/utils/uri_parser.dart' as uri_parser; String? validateUrl( String? value, { + required bool eagerParsing, bool requireAuthority = true, - bool eagerParsing = true, bool onlyHttpProtocol = false, bool required = true, }) { From 6798b200342bba1fa977669c9edd214db27a17af Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 07:16:08 +0200 Subject: [PATCH 07/14] add fallback parameter --- .../android/src/main/assets/extensions/ml_engine/api/ml.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js index 647ea7fe..6836947b 100644 --- a/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js +++ b/packages/flutter_mozilla_components/android/src/main/assets/extensions/ml_engine/api/ml.js @@ -16,6 +16,7 @@ const SMART_TAB_GROUPING_CONFIG = { taskName: ML_TASK_FEATURE_EXTRACTION, featureId: "smart-tab-embedding", backend: "onnx", + fallbackBackend: "onnx", }, topicGeneration: { dtype: "q8", @@ -23,6 +24,7 @@ const SMART_TAB_GROUPING_CONFIG = { taskName: ML_TASK_TEXT2TEXT, featureId: "smart-tab-topic", backend: "onnx", + fallbackBackend: "onnx", }, // dataConfig: { // titleKey: "label", From 7ab57a14b0e49330497a75407697e04035c7c60e Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 07:16:52 +0200 Subject: [PATCH 08/14] fix file upload --- .../BaseBrowserFragment.kt | 41 +++++++++++++++++++ .../BrowserFragment.kt | 7 ++++ .../EngineProvider.kt | 2 +- .../components/Core.kt | 7 +++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt index 51b2067f..a41ed4dd 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt @@ -16,6 +16,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.CallSuper @@ -34,6 +35,7 @@ import mozilla.components.feature.downloads.temporary.ShareResourceFeature import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature import mozilla.components.feature.privatemode.feature.SecureWindowFeature import mozilla.components.feature.prompts.PromptFeature +import mozilla.components.feature.prompts.file.AndroidPhotoPicker import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.PictureInPictureFeature import mozilla.components.feature.session.SessionFeature @@ -43,6 +45,7 @@ import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction import mozilla.components.feature.webauthn.WebAuthnFeature import mozilla.components.support.base.feature.ActivityResultHandler +import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.log.logger.Logger @@ -74,6 +77,20 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit private var pictureInPictureFeature: PictureInPictureFeature? = null + // Registers a photo picker activity launcher in single-select mode. + private val singleMediaPicker = + AndroidPhotoPicker.singleMediaPicker( + { this }, + { promptFeature.get() }, + ) + + // Registers a photo picker activity launcher in multi-select mode. + private val multipleMediaPicker = + AndroidPhotoPicker.multipleMediaPicker( + { this }, + { promptFeature.get() }, + ) + private val sessionId: String? get() = arguments?.getString(SESSION_ID_KEY) @@ -224,6 +241,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit onNeedToRequestPermissions = { permissions -> requestPromptsPermissionsLauncher.launch(permissions) }, + androidPhotoPicker = AndroidPhotoPicker( + requireContext(), + singleMediaPicker, + multipleMediaPicker, + ), ), owner = this, view = view, @@ -392,6 +414,21 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit } } + @Suppress("OVERRIDE_DEPRECATION") + final override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ) { + val feature: PermissionsFeature? = when (requestCode) { + REQUEST_CODE_DOWNLOAD_PERMISSIONS -> downloadsFeature.get() + REQUEST_CODE_PROMPT_PERMISSIONS -> promptFeature.get() + REQUEST_CODE_APP_PERMISSIONS -> sitePermissionsFeature.get() + else -> null + } + feature?.onPermissionsResult(permissions, grantResults) + } + @CallSuper override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean { return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) } @@ -400,6 +437,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit companion object { private const val SESSION_ID_KEY = "session_id" + private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 + private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2 + private const val REQUEST_CODE_APP_PERMISSIONS = 3 + @JvmStatic protected fun Bundle.putSessionId(sessionId: String?) { putString(SESSION_ID_KEY, sessionId) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt index d06ed558..bb7a425a 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BrowserFragment.kt @@ -10,6 +10,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View +import androidx.annotation.CallSuper import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActivity import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature @@ -39,6 +40,12 @@ class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler { } } + @Deprecated("Deprecated in Java") + @CallSuper + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, data, resultCode) + } + @Suppress("LongMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt index dd84436b..4de96e51 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/EngineProvider.kt @@ -70,7 +70,7 @@ object EngineProvider { builder.aboutConfigEnabled(true) builder.extensionsProcessEnabled(true) builder.extensionsWebAPIEnabled(true) - builder.debugLogging(components.logLevel == Log.Priority.DEBUG) + //builder.debugLogging(components.logLevel == Log.Priority.DEBUG) builder.consoleOutput(components.logLevel == Log.Priority.DEBUG) builder.contentBlocking(contentBlocking.build()) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt index ab5dd674..28ed7112 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/Core.kt @@ -46,8 +46,11 @@ import mozilla.components.feature.addons.update.DefaultAddonUpdater import mozilla.components.feature.customtabs.store.CustomTabsServiceStore import mozilla.components.feature.downloads.DownloadMiddleware import mozilla.components.feature.media.MediaSessionFeature +import mozilla.components.feature.media.middleware.LastMediaAccessMiddleware import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware +import mozilla.components.feature.prompts.PromptMiddleware import mozilla.components.feature.prompts.file.FileUploadsDirCleaner +import mozilla.components.feature.prompts.file.FileUploadsDirCleanerMiddleware import mozilla.components.feature.readerview.ReaderViewMiddleware import mozilla.components.feature.session.HistoryDelegate import mozilla.components.feature.session.middleware.LastAccessMiddleware @@ -166,9 +169,11 @@ class Core(private val context: Context, ReaderViewMiddleware(), UndoMiddleware(), LastAccessMiddleware(), -// PromptMiddleware(), SessionPrioritizationMiddleware(), RecordingDevicesMiddleware(context, components.notificationsDelegate), + PromptMiddleware(), + FileUploadsDirCleanerMiddleware(fileUploadsDirCleaner), + LastMediaAccessMiddleware(), ) + EngineMiddleware.create( engine, // We are disabling automatic suspending of engine sessions under memory pressure. From 77cda075cade19fca6e9a6e01a978c511de5e135 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 10:15:13 +0200 Subject: [PATCH 09/14] upgrade packages and regenerate --- .../features/tabs/data/database/database.g.dart | 12 ++++++------ app/pubspec.yaml | 13 +++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart index cca3404e..c8284aa8 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart @@ -1256,28 +1256,28 @@ abstract class _$TabDatabase extends GeneratedDatabase { ); } - Selectable previousTabByTimestamp({required String tabId}) { + Selectable previousTabByTimestamp({required String tabId}) { return customSelect( 'WITH ranked_tabs AS (SELECT id, timestamp, LAG(id)OVER (ORDER BY timestamp RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_tab_id FROM tab) SELECT prev_tab_id FROM ranked_tabs WHERE id = ?1', variables: [Variable(tabId)], readsFrom: {tab}, - ).map((QueryRow row) => row.read('prev_tab_id')); + ).map((QueryRow row) => row.readNullable('prev_tab_id')); } - Selectable previousTabByOrderKey({required String tabId}) { + Selectable previousTabByOrderKey({required String tabId}) { return customSelect( 'WITH ranked_tabs AS (SELECT id, order_key, LAG(id)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS prev_tab_id FROM tab) SELECT prev_tab_id FROM ranked_tabs WHERE id = ?1', variables: [Variable(tabId)], readsFrom: {tab}, - ).map((QueryRow row) => row.read('prev_tab_id')); + ).map((QueryRow row) => row.readNullable('prev_tab_id')); } - Selectable nextTabByOrderKey({required String tabId}) { + Selectable nextTabByOrderKey({required String tabId}) { return customSelect( 'WITH ranked_tabs AS (SELECT id, order_key, LEAD(id)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_tab_id FROM tab) SELECT next_tab_id FROM ranked_tabs WHERE id = ?1', variables: [Variable(tabId)], readsFrom: {tab}, - ).map((QueryRow row) => row.read('next_tab_id')); + ).map((QueryRow row) => row.readNullable('next_tab_id')); } @override diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 88c163d3..3fb12651 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -28,9 +28,9 @@ dependencies: flutter_reorderable_grid_view: ^5.5.1 flutter_secure_storage: ^10.0.0-beta.4 flutter_slidable: ^4.0.1 - flutter_svg: ^2.2.0 - go_router: ^16.2.0 - google_fonts: ^6.3.0 + flutter_svg: ^2.2.1 + go_router: ^16.2.1 + google_fonts: ^6.3.1 graphview: ^1.2.0 home_widget: ^0.8.0 hooks_riverpod: ^2.6.1 @@ -62,17 +62,14 @@ dependencies: skeletonizer: ^2.1.0+1 sliver_tools: ^0.2.12 smooth_page_indicator: ^1.2.1 - socks5_proxy: - git: - url: https://github.com/sylvieon/socks_dart.git - ref: 04550bc08f42cc1a35f1e870f651b93a4f9aceab + socks5_proxy: ^2.1.1 speech_to_text_google_dialog: git: url: https://github.com/FaFre/speech_to_text_google_dialog.git sqlite3: ^2.9.0 sqlite3_flutter_libs: ^0.5.39 synchronized: ^3.4.0 - text_scroll: ^0.2.0 + text_scroll: ^0.2.1 timeago: ^3.7.1 tor: git: From 0b343835107069d1b8a870cd8c09981bd38fddf4 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 10:16:34 +0200 Subject: [PATCH 10/14] setting to show link open prompt --- app/lib/core/routing/routes.browser.dart | 19 ++++ app/lib/core/routing/routes.dart | 1 + app/lib/core/routing/routes.g.dart | 42 +++++++- .../geckoview/domain/repositories/tab.dart | 68 ++++--------- .../geckoview/domain/repositories/tab.g.dart | 2 +- .../dialogs/open_shared_content.dart | 99 +++++++++++++++++++ .../browser_modules/bottom_app_bar.dart | 3 + .../search/presentation/screens/search.dart | 9 +- .../onboarding/presentation/onboarding.dart | 2 + .../screens/general_settings.dart | 26 +++-- .../user/data/models/general_settings.dart | 12 ++- .../user/data/models/general_settings.g.dart | 34 ++++--- .../domain/repositories/general_settings.dart | 2 +- .../repositories/general_settings.g.dart | 2 +- .../presentation/screens/feed_article.dart | 2 + app/lib/presentation/main_app.dart | 75 ++++++++++++++ 16 files changed, 312 insertions(+), 86 deletions(-) create mode 100644 app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart diff --git a/app/lib/core/routing/routes.browser.dart b/app/lib/core/routing/routes.browser.dart index 6a14cb27..c104b48c 100644 --- a/app/lib/core/routing/routes.browser.dart +++ b/app/lib/core/routing/routes.browser.dart @@ -54,6 +54,10 @@ part of 'routes.dart'; name: 'TabTreeRoute', path: 'tab_tree/:rootTabId', ), + TypedGoRoute( + name: 'OpenSharedContentRoute', + path: 'open_content', + ), ], ) class BrowserRoute extends GoRouteData with _$BrowserRoute { @@ -75,9 +79,12 @@ class SearchRoute extends GoRouteData with _$SearchRoute { //This should be nullable but isnt allowed by go_router final String searchText; + final bool $extra; + const SearchRoute({ required this.tabType, this.searchText = SearchRoute.emptySearchText, + this.$extra = false, }); @override @@ -87,6 +94,7 @@ class SearchRoute extends GoRouteData with _$SearchRoute { initialSearchText: (searchText.isEmpty || searchText == emptySearchText) ? null : searchText, + launchedFromIntent: $extra, ); } } @@ -159,3 +167,14 @@ class TabTreeRoute extends GoRouteData with _$TabTreeRoute { return DialogPage(builder: (_) => TabTreeDialog(rootTabId)); } } + +class OpenSharedContentRoute extends GoRouteData with _$OpenSharedContentRoute { + final Uri $extra; + + const OpenSharedContentRoute(this.$extra); + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return DialogPage(builder: (_) => OpenSharedContent(sharedUrl: $extra)); + } +} diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index b652c384..2214ace5 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -26,6 +26,7 @@ import 'package:weblibre/features/about/presentation/screens/about.dart'; import 'package:weblibre/features/bangs/presentation/screens/categories.dart'; import 'package:weblibre/features/bangs/presentation/screens/list.dart'; import 'package:weblibre/features/bangs/presentation/screens/search.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/tab_tree.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/screens/browser.dart'; import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart'; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index d845a288..48760524 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -333,6 +333,12 @@ RouteBase get $browserRoute => GoRouteData.$route( factory: _$TabTreeRoute._fromState, ), + GoRouteData.$route( + path: 'open_content', + name: 'OpenSharedContentRoute', + + factory: _$OpenSharedContentRoute._fromState, + ), ], ); @@ -361,6 +367,7 @@ mixin _$SearchRoute on GoRouteData { tabType: _$TabTypeEnumMap._$fromName(state.pathParameters['tabType']!)!, searchText: state.pathParameters['searchText'] ?? SearchRoute.emptySearchText, + $extra: state.extra as bool, ); SearchRoute get _self => this as SearchRoute; @@ -371,17 +378,19 @@ mixin _$SearchRoute on GoRouteData { ); @override - void go(BuildContext context) => context.go(location); + void go(BuildContext context) => context.go(location, extra: _self.$extra); @override - Future push(BuildContext context) => context.push(location); + Future push(BuildContext context) => + context.push(location, extra: _self.$extra); @override void pushReplacement(BuildContext context) => - context.pushReplacement(location); + context.pushReplacement(location, extra: _self.$extra); @override - void replace(BuildContext context) => context.replace(location); + void replace(BuildContext context) => + context.replace(location, extra: _self.$extra); } const _$TabTypeEnumMap = { @@ -552,6 +561,31 @@ mixin _$TabTreeRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin _$OpenSharedContentRoute on GoRouteData { + static OpenSharedContentRoute _fromState(GoRouterState state) => + OpenSharedContentRoute(state.extra as Uri); + + OpenSharedContentRoute get _self => this as OpenSharedContentRoute; + + @override + String get location => GoRouteData.$location('/open_content'); + + @override + void go(BuildContext context) => context.go(location, extra: _self.$extra); + + @override + Future push(BuildContext context) => + context.push(location, extra: _self.$extra); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location, extra: _self.$extra); + + @override + void replace(BuildContext context) => + context.replace(location, extra: _self.$extra); +} + extension on Map { T? _$fromName(String? value) => entries.where((element) => element.value == value).firstOrNull?.key; diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index 2e87bbaf..a527073f 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -25,21 +25,16 @@ 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/core/routing/routes.dart'; -import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart'; import 'package:weblibre/features/geckoview/domain/providers.dart'; import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart'; import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; -import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; -import 'package:weblibre/features/geckoview/features/browser/domain/providers/intent.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; -import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart'; -import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/debouncer.dart'; part 'tab.g.dart'; @@ -68,12 +63,16 @@ class TabRepository extends _$TabRepository { required bool private, HistoryMetadataKey? historyMetadata, Map? additionalHeaders, + Value? container, + bool launchedFromIntent = false, }) async { - final selectedContainer = await ref - .read(selectedContainerProvider.notifier) - .fetchData(); + final assingedContainer = + container ?? + Value( + await ref.read(selectedContainerProvider.notifier).fetchData(), + ); - return ref + final newTabId = await ref .read(tabDatabaseProvider) .tabDao .upsertContainerTabTransactional( @@ -84,7 +83,7 @@ class TabRepository extends _$TabRepository { startLoading: startLoading, parentId: parentId, flags: flags, - contextId: selectedContainer?.metadata.contextualIdentity, + contextId: assingedContainer.value?.metadata.contextualIdentity, source: source, private: private, historyMetadata: historyMetadata, @@ -92,8 +91,14 @@ class TabRepository extends _$TabRepository { ); }, parentId: Value(parentId), - containerId: Value(selectedContainer?.id), + containerId: Value(assingedContainer.value?.id), ); + + if (launchedFromIntent) { + _tabFromIntent.add(newTabId); + } + + return newTabId; } Future duplicateTab({ @@ -371,45 +376,6 @@ class TabRepository extends _$TabRepository { }, ); - ref.listen( - fireImmediately: true, - engineBoundIntentStreamProvider, - (previous, next) { - next.whenData((value) async { - final isPrivate = - ref - .read(generalSettingsWithDefaultsProvider) - .defaultIntentTabType == - TabType.private; - - switch (value) { - case SharedUrl(): - _tabFromIntent.add( - await addTab(url: value.url, private: isPrivate), - ); - case SharedText(): - final defaultSearchBang = - ref.read(selectedBangDataProvider()) ?? - await ref.read(defaultSearchBangDataProvider.future); - - _tabFromIntent.add( - await addTab( - url: defaultSearchBang?.getTemplateUrl(value.text), - private: isPrivate, - ), - ); - } - }); - }, - onError: (error, stackTrace) { - logger.e( - 'Error listening to engineBoundIntentStreamProvider', - error: error, - stackTrace: stackTrace, - ); - }, - ); - ref.onDispose(() async { tabStateDebouncer.dispose(); await tabAddedSub.cancel(); diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index 12642bf0..889ccfa8 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabRepositoryHash() => r'4f562c1456eadcda7986ea2048667dd083f37618'; +String _$tabRepositoryHash() => r'45af26417881bca38973cf702073b1c7857f1cfa'; /// See also [TabRepository]. @ProviderFor(TabRepository) diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart new file mode 100644 index 00000000..ce817a6d --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/open_shared_content.dart @@ -0,0 +1,99 @@ +import 'package:drift/drift.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart'; +import 'package:weblibre/utils/form_validators.dart'; + +class OpenSharedContent extends HookConsumerWidget { + final Uri sharedUrl; + + const OpenSharedContent({required this.sharedUrl}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + final textController = useTextEditingController(text: sharedUrl.toString()); + + final selectedContainer = useState(null); + + Future openTab(bool isPrivate) async { + if (formKey.currentState?.validate() == true) { + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: Uri.parse(textController.text), + private: isPrivate, + container: Value(selectedContainer.value), + launchedFromIntent: true, + ); + } + } + + return Form( + key: formKey, + child: SimpleDialog( + title: const Text('Open URL'), + children: [ + Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 16.0), + child: SizedBox( + height: 48, + width: double.maxFinite, + child: ContainerChips( + displayMenu: false, + selectedContainer: selectedContainer.value, + onSelected: (container) { + selectedContainer.value = container; + }, + onDeleted: (container) { + selectedContainer.value = null; + }, + ), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 16.0), + child: TextFormField( + controller: textController, + keyboardType: TextInputType.url, + minLines: 1, + maxLines: 10, + validator: (value) { + return validateUrl( + value, + onlyHttpProtocol: true, + eagerParsing: false, + ); + }, + ), + ), + ListTile( + title: const Text('Open Regular Tab'), + leading: const Icon(MdiIcons.tab), + onTap: () async { + await openTab(false); + if (context.mounted) { + context.pop(); + } + }, + ), + ListTile( + title: const Text('Open Private Tab'), + leading: const Icon(MdiIcons.tabUnselected), + onTap: () async { + await openTab(true); + if (context.mounted) { + context.pop(); + } + }, + ), + ], + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart index f3edaac3..88dee46c 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; @@ -242,6 +243,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { .addTab( url: ref.read(docsUriProvider), private: isPrivate, + container: const Value(null), ); }, leadingIcon: const Icon(Icons.help), @@ -301,6 +303,7 @@ class BrowserBottomAppBar extends HookConsumerWidget { .addTab( url: Uri.parse('https://addons.mozilla.org'), private: isPrivate, + container: const Value(null), ); }, leadingIcon: const Icon(MdiIcons.puzzlePlus), diff --git a/app/lib/features/geckoview/features/search/presentation/screens/search.dart b/app/lib/features/geckoview/features/search/presentation/screens/search.dart index 1605b863..0029a228 100644 --- a/app/lib/features/geckoview/features/search/presentation/screens/search.dart +++ b/app/lib/features/geckoview/features/search/presentation/screens/search.dart @@ -44,8 +44,13 @@ import 'package:weblibre/utils/uri_parser.dart' as uri_parser; class SearchScreen extends HookConsumerWidget { final String? initialSearchText; final TabType tabType; + final bool launchedFromIntent; - const SearchScreen({required this.initialSearchText, required this.tabType}); + const SearchScreen({ + required this.initialSearchText, + required this.tabType, + this.launchedFromIntent = false, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -109,6 +114,7 @@ class SearchScreen extends HookConsumerWidget { parentId: (selectedTabType.value == TabType.child) ? ref.read(selectedTabProvider) : null, + launchedFromIntent: launchedFromIntent, ); if (context.mounted) { @@ -212,6 +218,7 @@ class SearchScreen extends HookConsumerWidget { (selectedTabType.value == TabType.child) ? ref.read(selectedTabProvider) : null, + launchedFromIntent: launchedFromIntent, ); if (context.mounted) { diff --git a/app/lib/features/onboarding/presentation/onboarding.dart b/app/lib/features/onboarding/presentation/onboarding.dart index ac32dd5a..0023e181 100644 --- a/app/lib/features/onboarding/presentation/onboarding.dart +++ b/app/lib/features/onboarding/presentation/onboarding.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -147,6 +148,7 @@ class OnboardingScreen extends HookConsumerWidget { .addTab( url: ref.read(docsUriProvider), private: false, + container: const Value(null), ); ref.invalidate(routerProvider); diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index c9aac65a..831248d0 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -175,17 +175,22 @@ class GeneralSettingsScreen extends HookConsumerWidget { showSelectedIcon: false, segments: const [ ButtonSegment( - value: TabType.regular, + value: TabIntentOpenSetting.ask, + label: Text('Prompt'), + icon: Icon(MdiIcons.messageQuestion), + ), + ButtonSegment( + value: TabIntentOpenSetting.regular, label: Text('Regular'), icon: Icon(MdiIcons.tab), ), ButtonSegment( - value: TabType.private, + value: TabIntentOpenSetting.private, label: Text('Private'), icon: Icon(MdiIcons.tabUnselected), ), ], - selected: {generalSettings.defaultIntentTabType}, + selected: {generalSettings.tabIntentOpenSetting}, onSelectionChanged: (value) async { await ref .read( @@ -193,15 +198,16 @@ class GeneralSettingsScreen extends HookConsumerWidget { ) .save( (currentSettings) => currentSettings.copyWith - .defaultIntentTabType(value.first), + .tabIntentOpenSetting(value.first), ); }, - style: switch (generalSettings.defaultIntentTabType) { - TabType.regular => null, - TabType.private => SegmentedButton.styleFrom( - selectedBackgroundColor: const Color(0x648000D7), - ), - TabType.child => null, + style: switch (generalSettings.tabIntentOpenSetting) { + TabIntentOpenSetting.regular => null, + TabIntentOpenSetting.private => + SegmentedButton.styleFrom( + selectedBackgroundColor: const Color(0x648000D7), + ), + TabIntentOpenSetting.ask => null, }, ), ), diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index ac2a2018..cdf3d0cd 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -31,6 +31,8 @@ const _fallbackAutocompleteProvider = SearchSuggestionProviders.none; enum TabBarSwipeAction { switchLastOpened, navigateOrderedTabs } +enum TabIntentOpenSetting { regular, private, ask } + enum DeleteBrowsingDataType { tabs('Open tabs'), history('Browsing history'), @@ -58,7 +60,7 @@ class GeneralSettings with FastEquatable { final bool showExtensionShortcut; final bool enableLocalAiFeatures; final TabType defaultCreateTabType; - final TabType defaultIntentTabType; + final TabIntentOpenSetting tabIntentOpenSetting; final bool proxyPrivateTabsTor; final bool autoHideTabBar; final TabBarSwipeAction tabBarSwipeAction; @@ -75,7 +77,7 @@ class GeneralSettings with FastEquatable { required this.showExtensionShortcut, required this.enableLocalAiFeatures, required this.defaultCreateTabType, - required this.defaultIntentTabType, + required this.tabIntentOpenSetting, required this.autoHideTabBar, required this.tabBarSwipeAction, }); @@ -92,7 +94,7 @@ class GeneralSettings with FastEquatable { bool? showExtensionShortcut, bool? enableLocalAiFeatures, TabType? defaultCreateTabType, - TabType? defaultIntentTabType, + TabIntentOpenSetting? tabIntentOpenSetting, bool? autoHideTabBar, TabBarSwipeAction? tabBarSwipeAction, }) : themeMode = themeMode ?? ThemeMode.dark, @@ -106,7 +108,7 @@ class GeneralSettings with FastEquatable { showExtensionShortcut = showExtensionShortcut ?? false, enableLocalAiFeatures = enableLocalAiFeatures ?? true, defaultCreateTabType = defaultCreateTabType ?? TabType.regular, - defaultIntentTabType = defaultIntentTabType ?? TabType.regular, + tabIntentOpenSetting = tabIntentOpenSetting ?? TabIntentOpenSetting.ask, autoHideTabBar = autoHideTabBar ?? true, tabBarSwipeAction = tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened; @@ -128,7 +130,7 @@ class GeneralSettings with FastEquatable { showExtensionShortcut, enableLocalAiFeatures, defaultCreateTabType, - defaultIntentTabType, + tabIntentOpenSetting, proxyPrivateTabsTor, autoHideTabBar, tabBarSwipeAction, diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index ddb78cbe..b52bfc7f 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -33,7 +33,9 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings defaultCreateTabType(TabType defaultCreateTabType); - GeneralSettings defaultIntentTabType(TabType defaultIntentTabType); + GeneralSettings tabIntentOpenSetting( + TabIntentOpenSetting tabIntentOpenSetting, + ); GeneralSettings autoHideTabBar(bool autoHideTabBar); @@ -57,7 +59,7 @@ abstract class _$GeneralSettingsCWProxy { bool showExtensionShortcut, bool enableLocalAiFeatures, TabType defaultCreateTabType, - TabType defaultIntentTabType, + TabIntentOpenSetting tabIntentOpenSetting, bool autoHideTabBar, TabBarSwipeAction tabBarSwipeAction, }); @@ -115,8 +117,9 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { this(defaultCreateTabType: defaultCreateTabType); @override - GeneralSettings defaultIntentTabType(TabType defaultIntentTabType) => - this(defaultIntentTabType: defaultIntentTabType); + GeneralSettings tabIntentOpenSetting( + TabIntentOpenSetting tabIntentOpenSetting, + ) => this(tabIntentOpenSetting: tabIntentOpenSetting); @override GeneralSettings autoHideTabBar(bool autoHideTabBar) => @@ -145,7 +148,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? showExtensionShortcut = const $CopyWithPlaceholder(), Object? enableLocalAiFeatures = const $CopyWithPlaceholder(), Object? defaultCreateTabType = const $CopyWithPlaceholder(), - Object? defaultIntentTabType = const $CopyWithPlaceholder(), + Object? tabIntentOpenSetting = const $CopyWithPlaceholder(), Object? autoHideTabBar = const $CopyWithPlaceholder(), Object? tabBarSwipeAction = const $CopyWithPlaceholder(), }) { @@ -200,10 +203,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.defaultCreateTabType // ignore: cast_nullable_to_non_nullable : defaultCreateTabType as TabType, - defaultIntentTabType: defaultIntentTabType == const $CopyWithPlaceholder() - ? _value.defaultIntentTabType + tabIntentOpenSetting: tabIntentOpenSetting == const $CopyWithPlaceholder() + ? _value.tabIntentOpenSetting // ignore: cast_nullable_to_non_nullable - : defaultIntentTabType as TabType, + : tabIntentOpenSetting as TabIntentOpenSetting, autoHideTabBar: autoHideTabBar == const $CopyWithPlaceholder() ? _value.autoHideTabBar // ignore: cast_nullable_to_non_nullable @@ -248,9 +251,9 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => _$TabTypeEnumMap, json['defaultCreateTabType'], ), - defaultIntentTabType: $enumDecodeNullable( - _$TabTypeEnumMap, - json['defaultIntentTabType'], + tabIntentOpenSetting: $enumDecodeNullable( + _$TabIntentOpenSettingEnumMap, + json['tabIntentOpenSetting'], ), autoHideTabBar: json['autoHideTabBar'] as bool?, tabBarSwipeAction: $enumDecodeNullable( @@ -276,7 +279,8 @@ Map _$GeneralSettingsToJson( 'showExtensionShortcut': instance.showExtensionShortcut, 'enableLocalAiFeatures': instance.enableLocalAiFeatures, 'defaultCreateTabType': _$TabTypeEnumMap[instance.defaultCreateTabType]!, - 'defaultIntentTabType': _$TabTypeEnumMap[instance.defaultIntentTabType]!, + 'tabIntentOpenSetting': + _$TabIntentOpenSettingEnumMap[instance.tabIntentOpenSetting]!, 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, 'autoHideTabBar': instance.autoHideTabBar, 'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!, @@ -311,6 +315,12 @@ const _$TabTypeEnumMap = { TabType.child: 'child', }; +const _$TabIntentOpenSettingEnumMap = { + TabIntentOpenSetting.regular: 'regular', + TabIntentOpenSetting.private: 'private', + TabIntentOpenSetting.ask: 'ask', +}; + const _$TabBarSwipeActionEnumMap = { TabBarSwipeAction.switchLastOpened: 'switchLastOpened', TabBarSwipeAction.navigateOrderedTabs: 'navigateOrderedTabs', diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index c01eb5e7..fae8f966 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -88,7 +88,7 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.string, db.typeMapping, ), - 'defaultIntentTabType': settings['defaultIntentTabType']?.readAs( + 'tabIntentOpenSetting': settings['tabIntentOpenSetting']?.readAs( DriftSqlType.string, db.typeMapping, ), diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index 5a1c8da2..ae0c446f 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -27,7 +27,7 @@ final generalSettingsWithDefaultsProvider = typedef GeneralSettingsWithDefaultsRef = AutoDisposeProviderRef; String _$generalSettingsRepositoryHash() => - r'e311760a7773109a90c28275262883fcca348bbb'; + r'7dd57d726d4bf86e04cb33da9b7a1399d49f7915'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository) diff --git a/app/lib/features/web_feed/presentation/screens/feed_article.dart b/app/lib/features/web_feed/presentation/screens/feed_article.dart index 58293812..7b24715e 100644 --- a/app/lib/features/web_feed/presentation/screens/feed_article.dart +++ b/app/lib/features/web_feed/presentation/screens/feed_article.dart @@ -18,6 +18,7 @@ * along with this program. If not, see . */ import 'package:collection/collection.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -205,6 +206,7 @@ class FeedArticleScreen extends HookConsumerWidget { .addTab( url: articleLink.uri, private: isPrivate, + container: const Value(null), ); if (context.mounted) { diff --git a/app/lib/presentation/main_app.dart b/app/lib/presentation/main_app.dart index 5737e6ee..70312aad 100644 --- a/app/lib/presentation/main_app.dart +++ b/app/lib/presentation/main_app.dart @@ -19,8 +19,18 @@ */ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/providers/router.dart'; +import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/domain/services/app_initialization.dart'; +import 'package:weblibre/features/bangs/domain/providers/bangs.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; +import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/providers/intent.dart'; +import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; class MainApp extends HookConsumerWidget { @@ -40,6 +50,71 @@ class MainApp extends HookConsumerWidget { final initializationResult = ref.watch(appInitializationServiceProvider); final router = ref.watch(routerProvider); + ref.listen( + engineBoundIntentStreamProvider, + (previous, next) { + next.whenData((sharedContent) async { + final router = await ref.read(routerProvider.future); + final settings = ref.read(generalSettingsWithDefaultsProvider); + + switch (settings.tabIntentOpenSetting) { + case TabIntentOpenSetting.regular: + case TabIntentOpenSetting.private: + switch (sharedContent) { + case SharedUrl(): + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: sharedContent.url, + private: + settings.tabIntentOpenSetting == + TabIntentOpenSetting.private, + launchedFromIntent: true, + ); + case SharedText(): + final defaultSearchBang = + ref.read(selectedBangDataProvider()) ?? + await ref.read(defaultSearchBangDataProvider.future); + + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: defaultSearchBang?.getTemplateUrl( + sharedContent.text, + ), + private: + settings.tabIntentOpenSetting == + TabIntentOpenSetting.private, + launchedFromIntent: true, + ); + } + case TabIntentOpenSetting.ask: + switch (sharedContent) { + case SharedUrl(): + final route = OpenSharedContentRoute(sharedContent.url); + await router.push(route.location, extra: route.$extra); + case SharedText(): + final route = SearchRoute( + tabType: + ref.read(selectedTabTypeProvider) ?? + settings.defaultCreateTabType, + searchText: sharedContent.text, + $extra: true, //launched from intent + ); + await router.push(route.location); + } + } + }); + }, + onError: (error, stackTrace) { + logger.e( + 'Error listening to engineBoundIntentStreamProvider', + error: error, + stackTrace: stackTrace, + ); + }, + ); + return initializationResult.fold( (initializationState) { if (!initializationState.initialized) { From 88e450cee1076b45c0973581dda07f38c686a93e Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 10:17:34 +0200 Subject: [PATCH 11/14] prepare new alpha --- app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 3fb12651..17c5bd24 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,7 @@ name: weblibre description: "The Privacy-Focused & AI-Powered Research Browser" publish_to: 'none' resolution: workspace -version: 0.9.22+26 +version: 0.9.23-alpha-1+26 environment: sdk: '>=3.8.0 <4.0.0' From 7ca4f6a9c086c8b6a414e4fccef3d67f330c113e Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 14:47:34 +0200 Subject: [PATCH 12/14] update flutter version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0af03745..2a463f25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -77,7 +77,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.35.1 + flutter-version: 3.35.3 cache: true - name: Setup Flutter dependencies From 20cea6658eb49b960d37c1524eaa2462841d0278 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 14:48:55 +0200 Subject: [PATCH 13/14] upgrade mozilla components --- packages/flutter_mozilla_components/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle index 85623994..0dfda0bd 100644 --- a/packages/flutter_mozilla_components/android/build.gradle +++ b/packages/flutter_mozilla_components/android/build.gradle @@ -3,7 +3,7 @@ version = "1.0-SNAPSHOT" buildscript { ext.kotlin_version = "2.1.20" - ext.mozillaComponentsVersion = '142.0' + ext.mozillaComponentsVersion = '142.0.1' repositories { google() From 90118278f17a942b1a8ca05fc0c751afbaf425b8 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 11 Sep 2025 14:49:21 +0200 Subject: [PATCH 14/14] dont show master switch if there are no required options --- .../data/models/preference_setting.dart | 4 ++ .../screens/web_engine_hardening_group.dart | 65 ++++++++++--------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart index 722f5550..6b2ba5a8 100644 --- a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart +++ b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart @@ -33,6 +33,10 @@ class PreferenceSettingGroup with FastEquatable { (setting) => setting.requireUserOptIn || setting.isActive, ); + bool get showMasterSwitch => + settings.values.length > + settings.values.where((setting) => setting.requireUserOptIn).length; + bool get isPartlyActive => settings.values.any((setting) => setting.isActive); bool get hasInactiveOptional => settings.values.any( diff --git a/app/lib/features/settings/presentation/screens/web_engine_hardening_group.dart b/app/lib/features/settings/presentation/screens/web_engine_hardening_group.dart index afcf916d..2439abe7 100644 --- a/app/lib/features/settings/presentation/screens/web_engine_hardening_group.dart +++ b/app/lib/features/settings/presentation/screens/web_engine_hardening_group.dart @@ -47,46 +47,47 @@ class WebEngineHardeningGroupScreen extends HookConsumerWidget { data: (group) { return Column( children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Card( - color: theme.colorScheme.primaryContainer, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SwitchListTile( - value: group.isActiveOrOptional, - title: Text( - groupName, - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - ), - ), - subtitle: group.description.mapNotNull( - (description) => Text( - description, + if (group.showMasterSwitch) + Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + color: theme.colorScheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SwitchListTile( + value: group.isActiveOrOptional, + title: Text( + groupName, style: TextStyle( color: theme.colorScheme.onPrimaryContainer, ), ), - ), - onChanged: (value) async { - final notifier = ref.read( - preferenceSettingsGroupRepositoryProvider( - PreferencePartition.user, - groupName, - ).notifier, - ); + subtitle: group.description.mapNotNull( + (description) => Text( + description, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ), + onChanged: (value) async { + final notifier = ref.read( + preferenceSettingsGroupRepositoryProvider( + PreferencePartition.user, + groupName, + ).notifier, + ); - if (value) { - await notifier.apply(); - } else { - await notifier.reset(); - } - }, + if (value) { + await notifier.apply(); + } else { + await notifier.reset(); + } + }, + ), ), ), ), - ), Expanded( child: ListView( children: group.settings.entries.map((setting) {