diff --git a/app/lib/core/routing/routes.browser.dart b/app/lib/core/routing/routes.browser.dart index 04a2da4b..a19f3f06 100644 --- a/app/lib/core/routing/routes.browser.dart +++ b/app/lib/core/routing/routes.browser.dart @@ -211,7 +211,7 @@ class OpenSharedContentRoute extends GoRouteData with $OpenSharedContentRoute { @override Page buildPage(BuildContext context, GoRouterState state) { - return DialogPage( + return BottomSheetPage( builder: (_) => OpenSharedContent( sharedUrl: Uri.tryParse(sharedUrl) ?? Uri.parse('about:blank'), ), 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 index ae0a3ea4..015389d1 100644 --- 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 @@ -17,16 +17,18 @@ * 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'; +import 'package:drift/drift.dart' hide Column; 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:flutter_mozilla_components/flutter_mozilla_components.dart' + show GeckoBrowserService; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/core/design/app_colors.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/presentation/icons/weblibre_icons.dart'; import 'package:weblibre/utils/form_validators.dart'; class OpenSharedContent extends HookConsumerWidget { @@ -38,6 +40,7 @@ class OpenSharedContent extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final formKey = useMemoized(() => GlobalKey()); final textController = useTextEditingController(text: sharedUrl.toString()); + final appColors = AppColors.of(context); final selectedContainer = useState(null); @@ -59,16 +62,32 @@ class OpenSharedContent extends HookConsumerWidget { } } - return Form( - key: formKey, - child: SimpleDialog( - title: const Text('Open URL'), - children: [ - Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 16.0), - child: SizedBox( - width: double.maxFinite, - child: ContainerChips( + Future openCustomTab(bool isPrivate) async { + if (formKey.currentState?.validate() == true) { + await GeckoBrowserService().openInCustomTab( + url: Uri.parse(textController.text), + private: isPrivate, + contextId: selectedContainer.value?.id, + ); + + if (context.mounted) { + context.pop(true); + } + } + } + + return SafeArea( + child: Form( + key: formKey, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Open link', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + ContainerChips( displayMenu: false, selectedContainer: selectedContainer.value, onSelected: (container) { @@ -78,36 +97,75 @@ class OpenSharedContent extends HookConsumerWidget { selectedContainer.value = null; }, ), - ), + TextFormField( + controller: textController, + keyboardType: TextInputType.url, + minLines: 1, + maxLines: 10, + validator: (value) { + return validateUrl(value, eagerParsing: false); + }, + ), + const SizedBox(height: 8), + _OpenActionTile( + title: 'Open in new tab', + subtitle: 'Add to your browser tabs', + icon: MdiIcons.tab, + privateColor: appColors.privateTabPurple, + onTap: () => openTab(false), + onPrivateTap: () => openTab(true), + ), + _OpenActionTile( + title: 'Open in custom tab', + subtitle: 'Open in a separate window', + icon: MdiIcons.applicationOutline, + privateColor: appColors.privateTabPurple, + onTap: () => openCustomTab(false), + onPrivateTap: () => openCustomTab(true), + ), + ], ), - Padding( - padding: const EdgeInsetsDirectional.symmetric(horizontal: 16.0), - child: TextFormField( - controller: textController, - keyboardType: TextInputType.url, - minLines: 1, - maxLines: 10, - validator: (value) { - return validateUrl(value, eagerParsing: false); - }, - ), - ), - ListTile( - title: const Text('Open Regular Tab'), - leading: const Icon(MdiIcons.tab), - onTap: () async { - await openTab(false); - }, - ), - ListTile( - title: const Text('Open Private Tab'), - leading: const Icon(WebLibreIcons.privateTab), - onTap: () async { - await openTab(true); - }, - ), - ], + ), ), ); } } + +class _OpenActionTile extends StatelessWidget { + final String title; + final String? subtitle; + final IconData icon; + final Color privateColor; + final VoidCallback onTap; + final VoidCallback onPrivateTap; + + const _OpenActionTile({ + required this.title, + this.subtitle, + required this.icon, + required this.privateColor, + required this.onTap, + required this.onPrivateTap, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + subtitle: subtitle != null ? Text(subtitle!) : null, + leading: Icon(icon), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 24, child: VerticalDivider(width: 16)), + IconButton( + icon: Icon(MdiIcons.dominoMask, color: privateColor, size: 24), + tooltip: 'Private', + onPressed: onPrivateTap, + ), + ], + ), + onTap: onTap, + ); + } +} diff --git a/app/lib/features/user/domain/services/user_backup.g.dart b/app/lib/features/user/domain/services/user_backup.g.dart index 384392a6..eb582181 100644 --- a/app/lib/features/user/domain/services/user_backup.g.dart +++ b/app/lib/features/user/domain/services/user_backup.g.dart @@ -41,7 +41,7 @@ final class UserBackupServiceProvider } } -String _$userBackupServiceHash() => r'a0bcf458e6b0a982f1fc90fce99e60cc3418f4d9'; +String _$userBackupServiceHash() => r'fb0b29075da3b32135d6309ef1bb7a7f119a3e25'; abstract class _$UserBackupService extends $Notifier { void build(); diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt index 36d33c87..99eeb75b 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt @@ -14,6 +14,7 @@ import eu.weblibre.flutter_mozilla_components.BrowserFragment import eu.weblibre.flutter_mozilla_components.GeckoViewFactory import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.ProfileContext +import eu.weblibre.flutter_mozilla_components.activities.ExternalAppBrowserActivity import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity import eu.weblibre.flutter_mozilla_components.feature.DefaultSelectionActionDelegate import eu.weblibre.flutter_mozilla_components.pigeons.AddonCollection @@ -58,7 +59,11 @@ import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewEvents import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import mozilla.components.browser.state.action.CustomTabListAction import mozilla.components.browser.state.action.SystemAction +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.createCustomTab import mozilla.components.feature.addons.logger import mozilla.components.support.base.ext.getStacktraceAsString import mozilla.components.support.base.log.Log @@ -386,4 +391,32 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { } } } + + override fun openInCustomTab(url: String, `private`: Boolean, contextId: String?) { + val currentActivity = requireNotNull(activity) { "Activity not attached" } + + val customTabConfig = CustomTabConfig() + + val tab = createCustomTab( + url = url, + contextId = contextId, + config = customTabConfig, + source = SessionState.Source.Internal.CustomTab, + private = `private`, + ) + + components.core.store.dispatch( + CustomTabListAction.AddCustomTabAction(tab) + ) + + components.useCases.sessionUseCases.loadUrl(url, tab.id) + + val intent = ExternalAppBrowserActivity.createIntent( + context = currentActivity, + customTabSessionId = tab.id, + ) + currentActivity.startActivity(intent) + + logger.debug("$TAG: Opened custom tab ${tab.id} for $url (private=$`private`)") + } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 922251c5..b11effdb 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -4394,6 +4394,7 @@ interface GeckoBrowserApi { fun initialize(profileFolder: String, logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection?, fxaServerOverride: String?, syncTokenServerOverride: String?) fun showNativeFragment(): Boolean fun onTrimMemory(level: Long) + fun openInCustomTab(url: String, private: Boolean, contextId: String?) companion object { /** The codec used by GeckoBrowserApi. */ @@ -4475,6 +4476,26 @@ interface GeckoBrowserApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.openInCustomTab$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val urlArg = args[0] as String + val privateArg = args[1] as Boolean + val contextIdArg = args[2] as String? + val wrapped: List = try { + api.openInCustomTab(urlArg, privateArg, contextIdArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart index 1d946682..11daa32a 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_browser.dart @@ -42,4 +42,16 @@ class GeckoBrowserService { Future onTrimMemory(int level) { return _api.onTrimMemory(level); } + + Future openInCustomTab({ + required Uri url, + required bool private, + String? contextId, + }) { + return _api.openInCustomTab( + url: url.toString(), + private: private, + contextId: contextId, + ); + } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 7fb9bdf8..c64cca53 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -5160,6 +5160,28 @@ class GeckoBrowserApi { return; } } + + Future openInCustomTab({required String url, required bool private, required String? contextId, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.openInCustomTab$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, private, contextId]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + 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; + } + } } class GeckoSyncApi { diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 25fe310a..883f2bee 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1003,6 +1003,11 @@ abstract class GeckoBrowserApi { ); bool showNativeFragment(); void onTrimMemory(int level); + void openInCustomTab({ + required String url, + required bool private, + required String? contextId, + }); } enum SyncEngineValue { history, bookmarks, tabs }