clipboard url handling

This commit is contained in:
Fabian Freund
2025-05-29 14:21:35 +02:00
parent 05d938309c
commit 6985e5f2d5
6 changed files with 113 additions and 35 deletions
@@ -216,12 +216,20 @@ class _BrowserViewState extends ConsumerState<BrowserView>
if (_suggestionCountTime != null && if (_suggestionCountTime != null &&
DateTime.now().difference(_suggestionCountTime!) > DateTime.now().difference(_suggestionCountTime!) >
widget.suggestionTimeout) { widget.suggestionTimeout) {
showSuggestNewTabMessage( //Don't do anything if a child route is active
context, if (GoRouterState.of(context).topRoute?.name == BrowserRoute.name) {
onAdd: () async { unawaited(
await const SearchRoute(tabType: TabType.regular).push(context); showSuggestNewTabMessage(
}, context,
); onAdd: (searchText) async {
await SearchRoute(
tabType: TabType.regular,
searchText: searchText ?? SearchRoute.emptySearchText,
).push(context);
},
),
);
}
} }
_suggestionCountTime = null; _suggestionCountTime = null;
@@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart'; import 'package:nullability/nullability.dart';
import 'package:weblibre/features/geckoview/features/search/domain/providers/engine_suggestions.dart'; import 'package:weblibre/features/geckoview/features/search/domain/providers/engine_suggestions.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/clipboard_fill.dart';
import 'package:weblibre/presentation/hooks/listenable_callback.dart'; import 'package:weblibre/presentation/hooks/listenable_callback.dart';
import 'package:weblibre/presentation/widgets/auto_suggest_text_field.dart'; import 'package:weblibre/presentation/widgets/auto_suggest_text_field.dart';
import 'package:weblibre/utils/form_validators.dart'; import 'package:weblibre/utils/form_validators.dart';
@@ -51,26 +52,32 @@ class EditUrlDialog extends HookConsumerWidget {
return Form( return Form(
key: formKey, key: formKey,
child: AutoSuggestTextField( child: Column(
controller: addressTextController, mainAxisSize: MainAxisSize.min,
focusNode: addressTextFocusNode, children: [
suggestion: suggestion.value, AutoSuggestTextField(
// enableIMEPersonalizedLearning: !incognitoEnabled, controller: addressTextController,
maxLines: null, focusNode: addressTextFocusNode,
keyboardType: TextInputType.url, suggestion: suggestion.value,
decoration: const InputDecoration(hintText: 'Enter URL'), // enableIMEPersonalizedLearning: !incognitoEnabled,
onTap: () { maxLines: null,
if (!addressTextFocusNode.hasFocus) { keyboardType: TextInputType.url,
// Select all text when the field is tapped decoration: const InputDecoration(hintText: 'Enter URL'),
addressTextController.selection = TextSelection( onTap: () {
baseOffset: 0, if (!addressTextFocusNode.hasFocus) {
extentOffset: addressTextController.text.length, // Select all text when the field is tapped
); addressTextController.selection = TextSelection(
} baseOffset: 0,
}, extentOffset: addressTextController.text.length,
validator: (value) { );
return validateUrl(value, requireAuthority: false); }
}, },
validator: (value) {
return validateUrl(value, requireAuthority: false);
},
),
ClipboardFillLink(controller: addressTextController),
],
), ),
); );
}, },
@@ -10,6 +10,7 @@ import 'package:weblibre/features/bangs/domain/providers/search.dart';
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.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.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/clipboard_fill.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_field.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_field.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/feed_search.dart';
import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart'; import 'package:weblibre/features/geckoview/features/search/presentation/widgets/search_modules/history_suggestions.dart';
@@ -164,6 +165,9 @@ class SearchScreen extends HookConsumerWidget {
), ),
), ),
), ),
SliverToBoxAdapter(
child: ClipboardFillLink(controller: searchTextController),
),
const SliverToBoxAdapter(child: Divider()), const SliverToBoxAdapter(child: Divider()),
SearchTermSuggestions( SearchTermSuggestions(
searchTextController: searchTextController, searchTextController: searchTextController,
@@ -0,0 +1,30 @@
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:weblibre/presentation/hooks/cached_future.dart';
import 'package:weblibre/utils/clipboard.dart';
class ClipboardFillLink extends HookWidget {
final TextEditingController controller;
const ClipboardFillLink({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final clipboardUrl = useCachedFuture(() => tryGetUriFromClipboard());
final currentText = useValueListenable(controller);
return Visibility(
visible:
clipboardUrl.hasData &&
clipboardUrl.data.toString() != currentText.text,
child: ListTile(
leading: const Icon(MdiIcons.linkPlus),
title: const Text('Fill link from clipboard'),
onTap: () {
controller.text = clipboardUrl.data.toString();
},
),
);
}
}
+7
View File
@@ -0,0 +1,7 @@
import 'package:flutter/services.dart';
import 'package:weblibre/utils/uri_parser.dart';
Future<Uri?> tryGetUriFromClipboard({bool eagerParsing = true}) async {
final data = await Clipboard.getData('text/plain');
return tryParseUrl(data?.text, eagerParsing: eagerParsing);
}
+31 -9
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nullability/nullability.dart'; import 'package:nullability/nullability.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:weblibre/utils/clipboard.dart';
void showErrorMessage(BuildContext context, String message) { void showErrorMessage(BuildContext context, String message) {
final snackBar = SnackBar( final snackBar = SnackBar(
@@ -52,18 +53,39 @@ void showTabOpenedMessage(
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
} }
void showSuggestNewTabMessage( Future<void> showSuggestNewTabMessage(
BuildContext context, { BuildContext context, {
required void Function() onAdd, required void Function(String? searchText) onAdd,
Duration duration = const Duration(seconds: 2), Duration duration = const Duration(seconds: 2),
}) { }) async {
final snackBar = SnackBar( final clipboardUrl = await tryGetUriFromClipboard();
content: const Text('Want to open a new tab?'),
action: SnackBarAction(label: 'New Tab', onPressed: onAdd),
duration: duration,
);
ScaffoldMessenger.of(context).showSnackBar(snackBar); final snackBar =
(clipboardUrl != null)
? SnackBar(
content: const Text('Want to open link from clipboard?'),
action: SnackBarAction(
label: 'Open',
onPressed: () {
onAdd(clipboardUrl.toString());
},
),
duration: duration,
)
: SnackBar(
content: const Text('Want to open a new tab?'),
action: SnackBarAction(
label: 'New Tab',
onPressed: () {
onAdd(null);
},
),
duration: duration,
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
} }
void showTabSwitchMessage( void showTabSwitchMessage(