From f84f447545b0c1aed566eb692f4f9224ddcb1e67 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 8 Sep 2024 17:14:05 -0400 Subject: [PATCH 01/12] feat: added autocomplete options to url input --- .../custom_autocomplete_options.dart | 71 +++++++++++++++++ lib/screens/login_screen.dart | 76 ++++++++++++++++--- 2 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 lib/components/custom_autocomplete_options.dart diff --git a/lib/components/custom_autocomplete_options.dart b/lib/components/custom_autocomplete_options.dart new file mode 100644 index 0000000..7cb1785 --- /dev/null +++ b/lib/components/custom_autocomplete_options.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class CustomAutocompleteOptions extends StatelessWidget { + const CustomAutocompleteOptions({ + super.key, + required this.displayStringForOption, + required this.onSelected, + required this.openDirection, + required this.options, + required this.maxOptionsHeight, + required this.maxOptionsWidth, + }); + + final AutocompleteOptionToString displayStringForOption; + + final AutocompleteOnSelected onSelected; + final OptionsViewOpenDirection openDirection; + + final Iterable options; + final double maxOptionsHeight; + final double maxOptionsWidth; + + @override + Widget build(BuildContext context) { + final AlignmentDirectional optionsAlignment = switch (openDirection) { + OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart, + OptionsViewOpenDirection.down => AlignmentDirectional.topStart, + }; + return Align( + alignment: optionsAlignment, + child: Material( + elevation: 4.0, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: maxOptionsHeight, + maxWidth: maxOptionsWidth, + ), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (BuildContext context, int index) { + final T option = options.elementAt(index); + return InkWell( + onTap: () { + onSelected(option); + }, + child: Builder(builder: (BuildContext context) { + final bool highlight = + AutocompleteHighlightedOption.of(context) == index; + if (highlight) { + SchedulerBinding.instance.addPostFrameCallback( + (Duration timeStamp) { + Scrollable.ensureVisible(context, alignment: 0.5); + }, debugLabel: 'AutocompleteOptions.ensureVisible'); + } + return Container( + color: highlight ? Theme.of(context).focusColor : null, + padding: const EdgeInsets.all(16.0), + child: Text(displayStringForOption(option)), + ); + }), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index d1ad132..91df54b 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -1,11 +1,13 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:jellyflix/components/custom_autocomplete_options.dart'; import 'package:jellyflix/models/screen_paths.dart'; import 'package:jellyflix/models/user.dart'; import 'package:jellyflix/providers/auth_provider.dart'; @@ -56,15 +58,7 @@ class LoginScreen extends HookConsumerWidget { const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), - child: TextField( - controller: serverAddress, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: - AppLocalizations.of(context)!.serverAddress, - hintText: 'http://', - ), - ), + child: UrlFieldInput(serverAddress: serverAddress), ), Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), @@ -266,3 +260,67 @@ class LoginScreen extends HookConsumerWidget { .trim(); } } + +class UrlFieldInput extends ConsumerWidget { + const UrlFieldInput({super.key, required this.serverAddress}); + + final TextEditingController serverAddress; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final savedAddress = ref.watch(allProfilesProvider); + + // needed to get width of the url text filed + // so we can assign that to the autocomplete width + final urlTextFieldKey = GlobalKey(); + + return RawAutocomplete( + focusNode: FocusNode(), + textEditingController: serverAddress, + optionsBuilder: (TextEditingValue textEditingValue) { + final result = savedAddress.valueOrNull + ?.where((element) => element.serverAdress! + .toLowerCase() + .contains(textEditingValue.text)) + .map((e) => e.serverAdress!); + return result == null || result.isEmpty + ? ['http://', 'https://'] + : result; + }, + onSelected: (option) => serverAddress.text = option, + optionsViewOpenDirection: OptionsViewOpenDirection.down, + fieldViewBuilder: (context, controller, focusNode, _) { + return TextField( + key: urlTextFieldKey, + focusNode: focusNode, + controller: controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: AppLocalizations.of(context)!.serverAddress, + hintText: 'http://', + ), + ); + }, + optionsViewBuilder: ( + BuildContext context, + void Function(String) onSelected, + Iterable options, + ) { + RenderBox? renderBox; + if (urlTextFieldKey.currentContext?.findRenderObject() != null) { + renderBox = + urlTextFieldKey.currentContext!.findRenderObject() as RenderBox; + } + + return CustomAutocompleteOptions( + displayStringForOption: RawAutocomplete.defaultStringForOption, + onSelected: onSelected, + options: options, + openDirection: OptionsViewOpenDirection.down, + maxOptionsHeight: 100, + maxOptionsWidth: renderBox?.size.width ?? 300, + ); + }, + ); + } +} From f6f325d0ce7e4901b2e1795dcf6dc7576cdb0fcb Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 8 Sep 2024 17:27:02 -0400 Subject: [PATCH 02/12] fix analyzer warnings --- lib/screens/login_screen.dart | 1 - lib/screens/player_screen.dart | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 91df54b..a6cf9a1 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -1,7 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index a3f3ab1..1abf002 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -140,6 +140,7 @@ class _PlayerSreenState extends ConsumerState { ); }); + if (!context.mounted) return; player.stream.error.listen((error) { showDialog( context: context, From ec452b0091d8284e67c674029d8cb98fa200fc0c Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 8 Sep 2024 21:18:06 -0400 Subject: [PATCH 03/12] added lowercase for autofill --- lib/screens/login_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index a6cf9a1..1498637 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -280,10 +280,11 @@ class UrlFieldInput extends ConsumerWidget { final result = savedAddress.valueOrNull ?.where((element) => element.serverAdress! .toLowerCase() - .contains(textEditingValue.text)) + .contains(textEditingValue.text.toLowerCase())) .map((e) => e.serverAdress!); return result == null || result.isEmpty ? ['http://', 'https://'] + .where((e) => e.contains(textEditingValue.text.toLowerCase())) : result; }, onSelected: (option) => serverAddress.text = option, From 993e3185653db4ed2aa3d0697847e0e6c099ea97 Mon Sep 17 00:00:00 2001 From: RA341 Date: Thu, 12 Sep 2024 11:45:22 -0400 Subject: [PATCH 04/12] moved url field to file --- .../custom_autocomplete_options.dart | 1 + lib/components/url_autocomplete_field.dart | 71 +++++++++++++++++++ lib/screens/login_screen.dart | 66 +---------------- 3 files changed, 73 insertions(+), 65 deletions(-) create mode 100644 lib/components/url_autocomplete_field.dart diff --git a/lib/components/custom_autocomplete_options.dart b/lib/components/custom_autocomplete_options.dart index 7cb1785..87c5032 100644 --- a/lib/components/custom_autocomplete_options.dart +++ b/lib/components/custom_autocomplete_options.dart @@ -27,6 +27,7 @@ class CustomAutocompleteOptions extends StatelessWidget { OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart, OptionsViewOpenDirection.down => AlignmentDirectional.topStart, }; + // taken from flutter\lib\src\material\autocomplete.dart and slightly modified return Align( alignment: optionsAlignment, child: Material( diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart new file mode 100644 index 0000000..e6e2c47 --- /dev/null +++ b/lib/components/url_autocomplete_field.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:jellyflix/components/custom_autocomplete_options.dart'; +import 'package:jellyflix/providers/auth_provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class UrlFieldInput extends ConsumerWidget { + const UrlFieldInput({super.key, required this.serverAddress}); + + final TextEditingController serverAddress; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final savedAddress = ref.watch(allProfilesProvider); + + // needed to get width of the url text filed + // so we can assign that to the autocomplete width + final urlTextFieldKey = GlobalKey(); + + return RawAutocomplete( + focusNode: FocusNode(), + textEditingController: serverAddress, + optionsBuilder: (TextEditingValue textEditingValue) { + final result = savedAddress.valueOrNull + ?.where((element) => element.serverAdress! + .toLowerCase() + .contains(textEditingValue.text.toLowerCase())) + .map((e) => e.serverAdress!); + return result == null || result.isEmpty + ? ['http://', 'https://'] + .where((e) => e.contains(textEditingValue.text.toLowerCase())) + : result; + }, + onSelected: (option) => serverAddress.text = option, + optionsViewOpenDirection: OptionsViewOpenDirection.down, + fieldViewBuilder: (context, controller, focusNode, _) { + return TextField( + key: urlTextFieldKey, + focusNode: focusNode, + controller: controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: AppLocalizations.of(context)!.serverAddress, + hintText: 'http://', + ), + ); + }, + optionsViewBuilder: ( + BuildContext context, + void Function(String) onSelected, + Iterable options, + ) { + RenderBox? renderBox; + if (urlTextFieldKey.currentContext?.findRenderObject() != null) { + renderBox = + urlTextFieldKey.currentContext!.findRenderObject() as RenderBox; + } + + return CustomAutocompleteOptions( + displayStringForOption: RawAutocomplete.defaultStringForOption, + onSelected: onSelected, + options: options, + openDirection: OptionsViewOpenDirection.down, + maxOptionsHeight: 100, + maxOptionsWidth: renderBox?.size.width ?? 300, + ); + }, + ); + } +} diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 1498637..753561c 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -6,7 +6,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:jellyflix/components/custom_autocomplete_options.dart'; +import 'package:jellyflix/components/url_autocomplete_field.dart'; import 'package:jellyflix/models/screen_paths.dart'; import 'package:jellyflix/models/user.dart'; import 'package:jellyflix/providers/auth_provider.dart'; @@ -260,67 +260,3 @@ class LoginScreen extends HookConsumerWidget { } } -class UrlFieldInput extends ConsumerWidget { - const UrlFieldInput({super.key, required this.serverAddress}); - - final TextEditingController serverAddress; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final savedAddress = ref.watch(allProfilesProvider); - - // needed to get width of the url text filed - // so we can assign that to the autocomplete width - final urlTextFieldKey = GlobalKey(); - - return RawAutocomplete( - focusNode: FocusNode(), - textEditingController: serverAddress, - optionsBuilder: (TextEditingValue textEditingValue) { - final result = savedAddress.valueOrNull - ?.where((element) => element.serverAdress! - .toLowerCase() - .contains(textEditingValue.text.toLowerCase())) - .map((e) => e.serverAdress!); - return result == null || result.isEmpty - ? ['http://', 'https://'] - .where((e) => e.contains(textEditingValue.text.toLowerCase())) - : result; - }, - onSelected: (option) => serverAddress.text = option, - optionsViewOpenDirection: OptionsViewOpenDirection.down, - fieldViewBuilder: (context, controller, focusNode, _) { - return TextField( - key: urlTextFieldKey, - focusNode: focusNode, - controller: controller, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: AppLocalizations.of(context)!.serverAddress, - hintText: 'http://', - ), - ); - }, - optionsViewBuilder: ( - BuildContext context, - void Function(String) onSelected, - Iterable options, - ) { - RenderBox? renderBox; - if (urlTextFieldKey.currentContext?.findRenderObject() != null) { - renderBox = - urlTextFieldKey.currentContext!.findRenderObject() as RenderBox; - } - - return CustomAutocompleteOptions( - displayStringForOption: RawAutocomplete.defaultStringForOption, - onSelected: onSelected, - options: options, - openDirection: OptionsViewOpenDirection.down, - maxOptionsHeight: 100, - maxOptionsWidth: renderBox?.size.width ?? 300, - ); - }, - ); - } -} From 3d1906d5dedfa9de7f9846bfb35e2fe213a566b7 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sat, 21 Sep 2024 23:58:54 -0400 Subject: [PATCH 05/12] added keyboard shortcuts for autocomplete options --- .../custom_autocomplete_options.dart | 11 +++- lib/components/url_autocomplete_field.dart | 63 +++++++++++++++++-- lib/providers/url_autocomplete_provider.dart | 23 +++++++ 3 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 lib/providers/url_autocomplete_provider.dart diff --git a/lib/components/custom_autocomplete_options.dart b/lib/components/custom_autocomplete_options.dart index 87c5032..45ba154 100644 --- a/lib/components/custom_autocomplete_options.dart +++ b/lib/components/custom_autocomplete_options.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:jellyflix/providers/url_autocomplete_provider.dart'; -class CustomAutocompleteOptions extends StatelessWidget { +class CustomAutocompleteOptions extends ConsumerWidget { const CustomAutocompleteOptions({ super.key, required this.displayStringForOption, @@ -22,7 +24,7 @@ class CustomAutocompleteOptions extends StatelessWidget { final double maxOptionsWidth; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final AlignmentDirectional optionsAlignment = switch (openDirection) { OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart, OptionsViewOpenDirection.down => AlignmentDirectional.topStart, @@ -51,6 +53,11 @@ class CustomAutocompleteOptions extends StatelessWidget { final bool highlight = AutocompleteHighlightedOption.of(context) == index; if (highlight) { + // wrapped in a future to update the options after building + Future( + () => ref.read(selectedOptionProvider.notifier).state = + index, + ); SchedulerBinding.instance.addPostFrameCallback( (Duration timeStamp) { Scrollable.ensureVisible(context, alignment: 0.5); diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart index e6e2c47..5ebca43 100644 --- a/lib/components/url_autocomplete_field.dart +++ b/lib/components/url_autocomplete_field.dart @@ -2,8 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:jellyflix/components/custom_autocomplete_options.dart'; +import 'package:jellyflix/models/user.dart'; import 'package:jellyflix/providers/auth_provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:jellyflix/providers/url_autocomplete_provider.dart'; + +// Courtesy of sevenrats for the shortcuts class UrlFieldInput extends ConsumerWidget { const UrlFieldInput({super.key, required this.serverAddress}); @@ -12,14 +16,26 @@ class UrlFieldInput extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final savedAddress = ref.watch(allProfilesProvider); - - // needed to get width of the url text filed + final savedAddress = ref.read(allProfilesProvider); + // Needed to get width of the URL text field // so we can assign that to the autocomplete width final urlTextFieldKey = GlobalKey(); + // Create a custom FocusNode to listen for key events + final focusNode = FocusNode(); + + // Attach the key listener for Tab key press + focusNode.onKeyEvent = (FocusNode node, KeyEvent event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.enter) { + _selectAutocompleteOption(focusNode, serverAddress, savedAddress, ref); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }; + return RawAutocomplete( - focusNode: FocusNode(), + focusNode: focusNode, textEditingController: serverAddress, optionsBuilder: (TextEditingValue textEditingValue) { final result = savedAddress.valueOrNull @@ -27,12 +43,15 @@ class UrlFieldInput extends ConsumerWidget { .toLowerCase() .contains(textEditingValue.text.toLowerCase())) .map((e) => e.serverAdress!); - return result == null || result.isEmpty + final options = result == null || result.isEmpty ? ['http://', 'https://'] .where((e) => e.contains(textEditingValue.text.toLowerCase())) : result; + ref.read(optionsListProvider.notifier).overwriteList(options); + ref.invalidate(selectedOptionProvider); + return options; }, - onSelected: (option) => serverAddress.text = option, + // onSelected: (option) => serverAddress.text = option, optionsViewOpenDirection: OptionsViewOpenDirection.down, fieldViewBuilder: (context, controller, focusNode, _) { return TextField( @@ -68,4 +87,36 @@ class UrlFieldInput extends ConsumerWidget { }, ); } + + void _selectAutocompleteOption( + FocusNode focusNode, + TextEditingController controller, + AsyncValue> savedAddress, + WidgetRef ref, + ) { + final selectedInd = ref.read(selectedOptionProvider); + // there is a potential race condition that happens occasionally essentially + // sometimes the optionsListProvider remains empty even after options are built + // I don't understand why it happens, cannot recreate it and is pretty much random, + // so its wrapped in a try catch + try { + final currentOptions = ref.read(optionsListProvider); + final user = savedAddress.valueOrNull?.firstWhere( + (element) => + element.serverAdress!.startsWith(currentOptions[selectedInd]), + ); + + if (user != null) { + controller.text = user.serverAdress!; + // Move the cursor to the end of the text + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), + ); + } + } catch (e) { + print('Option list accessed a element out of bounds'); + print(e); + } + } } + diff --git a/lib/providers/url_autocomplete_provider.dart b/lib/providers/url_autocomplete_provider.dart new file mode 100644 index 0000000..5ca505f --- /dev/null +++ b/lib/providers/url_autocomplete_provider.dart @@ -0,0 +1,23 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +// helpful providers to run autocompleting urls + +final optionsListProvider = + NotifierProvider.autoDispose>(() { + return OptionsListNotifier(); +}); + +class OptionsListNotifier extends AutoDisposeNotifier> { + @override + List build() { + return []; + } + + void overwriteList(Iterable element) { + state = [...element].toList(); + } +} + +final selectedOptionProvider = StateProvider((ref) { + return 0; +}); From 0447e084bf4685013bb518c4b2deaeceeb22f163 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 22 Sep 2024 00:12:58 -0400 Subject: [PATCH 06/12] added back onselected callback --- lib/components/url_autocomplete_field.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart index 5ebca43..06f4aa2 100644 --- a/lib/components/url_autocomplete_field.dart +++ b/lib/components/url_autocomplete_field.dart @@ -17,6 +17,7 @@ class UrlFieldInput extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final savedAddress = ref.read(allProfilesProvider); + // Needed to get width of the URL text field // so we can assign that to the autocomplete width final urlTextFieldKey = GlobalKey(); @@ -43,15 +44,18 @@ class UrlFieldInput extends ConsumerWidget { .toLowerCase() .contains(textEditingValue.text.toLowerCase())) .map((e) => e.serverAdress!); + final options = result == null || result.isEmpty ? ['http://', 'https://'] .where((e) => e.contains(textEditingValue.text.toLowerCase())) : result; + ref.read(optionsListProvider.notifier).overwriteList(options); ref.invalidate(selectedOptionProvider); + return options; }, - // onSelected: (option) => serverAddress.text = option, + onSelected: (option) => serverAddress.text = option, optionsViewOpenDirection: OptionsViewOpenDirection.down, fieldViewBuilder: (context, controller, focusNode, _) { return TextField( From 6dbe50f27893964df853f93d56c3db1712569950 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 22 Sep 2024 00:22:35 -0400 Subject: [PATCH 07/12] added hint text to options --- .../custom_autocomplete_options.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/components/custom_autocomplete_options.dart b/lib/components/custom_autocomplete_options.dart index 45ba154..8288256 100644 --- a/lib/components/custom_autocomplete_options.dart +++ b/lib/components/custom_autocomplete_options.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -66,7 +68,21 @@ class CustomAutocompleteOptions extends ConsumerWidget { return Container( color: highlight ? Theme.of(context).focusColor : null, padding: const EdgeInsets.all(16.0), - child: Text(displayStringForOption(option)), + child: highlight + ? Platform.isAndroid || + Platform + .isIOS // do not show shortcut hint on mobile platforms + ? Text(displayStringForOption(option)) + : Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text(displayStringForOption(option)), + const SizedBox(width: 20), + const Text('Enter to fill') + ], + ) + : Text(displayStringForOption(option)), ); }), ); From 4cb5222cef60d592b9101f5d98f02b5625a0fcd9 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 22 Sep 2024 16:15:49 -0400 Subject: [PATCH 08/12] refactored and fixed the autocomplete logic --- lib/components/url_autocomplete_field.dart | 34 ++++++++++++---------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart index 06f4aa2..3ba7ffa 100644 --- a/lib/components/url_autocomplete_field.dart +++ b/lib/components/url_autocomplete_field.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -6,6 +7,7 @@ import 'package:jellyflix/models/user.dart'; import 'package:jellyflix/providers/auth_provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:jellyflix/providers/url_autocomplete_provider.dart'; +import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart'; // Courtesy of sevenrats for the shortcuts @@ -85,7 +87,7 @@ class UrlFieldInput extends ConsumerWidget { onSelected: onSelected, options: options, openDirection: OptionsViewOpenDirection.down, - maxOptionsHeight: 100, + maxOptionsHeight: 150, maxOptionsWidth: renderBox?.size.width ?? 300, ); }, @@ -103,24 +105,24 @@ class UrlFieldInput extends ConsumerWidget { // sometimes the optionsListProvider remains empty even after options are built // I don't understand why it happens, cannot recreate it and is pretty much random, // so its wrapped in a try catch + final currentOptions = ref.read(optionsListProvider); try { - final currentOptions = ref.read(optionsListProvider); - final user = savedAddress.valueOrNull?.firstWhere( - (element) => - element.serverAdress!.startsWith(currentOptions[selectedInd]), - ); + final option = currentOptions[selectedInd]; - if (user != null) { - controller.text = user.serverAdress!; - // Move the cursor to the end of the text - controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length), - ); - } + controller.text = option; + // Move the cursor to the end of the text + controller.selection = TextSelection.fromPosition( + TextPosition(offset: controller.text.length), + ); } catch (e) { - print('Option list accessed a element out of bounds'); - print(e); + if (kDebugMode) { + print('Option list accessed a element out of bounds'); + print('The following are the available options'); + print(currentOptions); + print('Index that was accessed'); + print(selectedInd); + print(e); + } } } } - From b61f2131313501ad074e5334e39afb0cd0574e2f Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 22 Sep 2024 16:33:10 -0400 Subject: [PATCH 09/12] removed duplicates from options --- lib/components/url_autocomplete_field.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart index 3ba7ffa..72dd0be 100644 --- a/lib/components/url_autocomplete_field.dart +++ b/lib/components/url_autocomplete_field.dart @@ -7,7 +7,6 @@ import 'package:jellyflix/models/user.dart'; import 'package:jellyflix/providers/auth_provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:jellyflix/providers/url_autocomplete_provider.dart'; -import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart'; // Courtesy of sevenrats for the shortcuts @@ -45,7 +44,7 @@ class UrlFieldInput extends ConsumerWidget { ?.where((element) => element.serverAdress! .toLowerCase() .contains(textEditingValue.text.toLowerCase())) - .map((e) => e.serverAdress!); + .map((e) => e.serverAdress!).toSet(); // remove duplicates final options = result == null || result.isEmpty ? ['http://', 'https://'] From ac00dcfcd53428ccc6571f7f6ac931868631a7ac Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 22 Sep 2024 17:05:21 -0400 Subject: [PATCH 10/12] switched to ref.watch to update state thanks sevenrats --- lib/components/url_autocomplete_field.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart index 72dd0be..555bfdc 100644 --- a/lib/components/url_autocomplete_field.dart +++ b/lib/components/url_autocomplete_field.dart @@ -51,7 +51,7 @@ class UrlFieldInput extends ConsumerWidget { .where((e) => e.contains(textEditingValue.text.toLowerCase())) : result; - ref.read(optionsListProvider.notifier).overwriteList(options); + ref.watch(optionsListProvider.notifier).overwriteList(options); ref.invalidate(selectedOptionProvider); return options; From 3210994eed6585aa7d4d1dc3afb235e51c021ff6 Mon Sep 17 00:00:00 2001 From: RA341 Date: Sun, 22 Sep 2024 17:12:45 -0400 Subject: [PATCH 11/12] hide autocomplete options if value exactly matches --- lib/components/url_autocomplete_field.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart index 555bfdc..4b416f2 100644 --- a/lib/components/url_autocomplete_field.dart +++ b/lib/components/url_autocomplete_field.dart @@ -41,14 +41,20 @@ class UrlFieldInput extends ConsumerWidget { textEditingController: serverAddress, optionsBuilder: (TextEditingValue textEditingValue) { final result = savedAddress.valueOrNull - ?.where((element) => element.serverAdress! - .toLowerCase() - .contains(textEditingValue.text.toLowerCase())) - .map((e) => e.serverAdress!).toSet(); // remove duplicates + ?.where( + (element) => + element.serverAdress! + .toLowerCase() + .contains(textEditingValue.text.toLowerCase()) && + element.serverAdress! != textEditingValue.text.toLowerCase(), + ) + .map((e) => e.serverAdress!) + .toSet(); // remove duplicates final options = result == null || result.isEmpty - ? ['http://', 'https://'] - .where((e) => e.contains(textEditingValue.text.toLowerCase())) + ? ['http://', 'https://'].where((e) => + e.contains(textEditingValue.text.toLowerCase()) && + e != textEditingValue.text.toLowerCase()) : result; ref.watch(optionsListProvider.notifier).overwriteList(options); From 95d553042e3df0a6c73927290f2952080a789c69 Mon Sep 17 00:00:00 2001 From: RA341 Date: Mon, 23 Sep 2024 14:07:37 -0400 Subject: [PATCH 12/12] refactor and simplified logic --- .../custom_autocomplete_options.dart | 27 ++++++------ lib/components/url_autocomplete_field.dart | 41 ++----------------- lib/providers/url_autocomplete_provider.dart | 22 +--------- 3 files changed, 18 insertions(+), 72 deletions(-) diff --git a/lib/components/custom_autocomplete_options.dart b/lib/components/custom_autocomplete_options.dart index 8288256..fd3bf3b 100644 --- a/lib/components/custom_autocomplete_options.dart +++ b/lib/components/custom_autocomplete_options.dart @@ -58,30 +58,27 @@ class CustomAutocompleteOptions extends ConsumerWidget { // wrapped in a future to update the options after building Future( () => ref.read(selectedOptionProvider.notifier).state = - index, + option as String, ); SchedulerBinding.instance.addPostFrameCallback( (Duration timeStamp) { Scrollable.ensureVisible(context, alignment: 0.5); }, debugLabel: 'AutocompleteOptions.ensureVisible'); } + return Container( color: highlight ? Theme.of(context).focusColor : null, padding: const EdgeInsets.all(16.0), - child: highlight - ? Platform.isAndroid || - Platform - .isIOS // do not show shortcut hint on mobile platforms - ? Text(displayStringForOption(option)) - : Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text(displayStringForOption(option)), - const SizedBox(width: 20), - const Text('Enter to fill') - ], - ) + // do not show shortcut hint on mobile platforms + child: highlight && !(Platform.isAndroid || Platform.isIOS) + ? Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(displayStringForOption(option)), + const SizedBox(width: 20), + const Text('Enter to fill') + ], + ) : Text(displayStringForOption(option)), ); }), diff --git a/lib/components/url_autocomplete_field.dart b/lib/components/url_autocomplete_field.dart index 4b416f2..08cdab0 100644 --- a/lib/components/url_autocomplete_field.dart +++ b/lib/components/url_autocomplete_field.dart @@ -1,9 +1,7 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:jellyflix/components/custom_autocomplete_options.dart'; -import 'package:jellyflix/models/user.dart'; import 'package:jellyflix/providers/auth_provider.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:jellyflix/providers/url_autocomplete_provider.dart'; @@ -30,7 +28,7 @@ class UrlFieldInput extends ConsumerWidget { focusNode.onKeyEvent = (FocusNode node, KeyEvent event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) { - _selectAutocompleteOption(focusNode, serverAddress, savedAddress, ref); + serverAddress.text = ref.read(selectedOptionProvider); return KeyEventResult.handled; } return KeyEventResult.ignored; @@ -44,8 +42,8 @@ class UrlFieldInput extends ConsumerWidget { ?.where( (element) => element.serverAdress! - .toLowerCase() .contains(textEditingValue.text.toLowerCase()) && + // ensure that the option is not already filled element.serverAdress! != textEditingValue.text.toLowerCase(), ) .map((e) => e.serverAdress!) @@ -54,10 +52,11 @@ class UrlFieldInput extends ConsumerWidget { final options = result == null || result.isEmpty ? ['http://', 'https://'].where((e) => e.contains(textEditingValue.text.toLowerCase()) && + // ensure that the option is not already filled e != textEditingValue.text.toLowerCase()) : result; - ref.watch(optionsListProvider.notifier).overwriteList(options); + // clear options on change ref.invalidate(selectedOptionProvider); return options; @@ -98,36 +97,4 @@ class UrlFieldInput extends ConsumerWidget { }, ); } - - void _selectAutocompleteOption( - FocusNode focusNode, - TextEditingController controller, - AsyncValue> savedAddress, - WidgetRef ref, - ) { - final selectedInd = ref.read(selectedOptionProvider); - // there is a potential race condition that happens occasionally essentially - // sometimes the optionsListProvider remains empty even after options are built - // I don't understand why it happens, cannot recreate it and is pretty much random, - // so its wrapped in a try catch - final currentOptions = ref.read(optionsListProvider); - try { - final option = currentOptions[selectedInd]; - - controller.text = option; - // Move the cursor to the end of the text - controller.selection = TextSelection.fromPosition( - TextPosition(offset: controller.text.length), - ); - } catch (e) { - if (kDebugMode) { - print('Option list accessed a element out of bounds'); - print('The following are the available options'); - print(currentOptions); - print('Index that was accessed'); - print(selectedInd); - print(e); - } - } - } } diff --git a/lib/providers/url_autocomplete_provider.dart b/lib/providers/url_autocomplete_provider.dart index 5ca505f..1447cb6 100644 --- a/lib/providers/url_autocomplete_provider.dart +++ b/lib/providers/url_autocomplete_provider.dart @@ -1,23 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -// helpful providers to run autocompleting urls - -final optionsListProvider = - NotifierProvider.autoDispose>(() { - return OptionsListNotifier(); -}); - -class OptionsListNotifier extends AutoDisposeNotifier> { - @override - List build() { - return []; - } - - void overwriteList(Iterable element) { - state = [...element].toList(); - } -} - -final selectedOptionProvider = StateProvider((ref) { - return 0; +final selectedOptionProvider = StateProvider((ref) { + return ''; });