add multi_trigger_autocomplete_plus

This commit is contained in:
DenserMeerkat
2025-02-15 20:54:01 +05:30
parent 21c65615e9
commit 2eedd1b41a
35 changed files with 3106 additions and 69 deletions

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart'; import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
import 'package:extended_text_field/extended_text_field.dart'; import 'package:extended_text_field/extended_text_field.dart';
import 'env_regexp_span_builder.dart'; import 'env_regexp_span_builder.dart';
import 'env_trigger_options.dart'; import 'env_trigger_options.dart';

View File

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

View File

@ -0,0 +1,24 @@
## 1.0.2
- Added `AutocompleteNoTrigger` to display autocomplete suggestions without requiring any trigger character or string.
## 1.0.1
- Made `triggerEnd` customizable instead of using a hardcoded space (`' '`).
- Enhanced the handling of triggers that share a common prefix, such as `{` and `{{`, ensuring that the correct trigger is identified and processed.
## 1.0.0
- Fixed `FieldView` focus getting lost on clicking the `OptionsView` on non-mobile platforms.
- Fixed `RangeError` when `textEditingValue.selection.isInvalid`.
[#11](https://github.com/xsahil03x/multi_trigger_autocomplete/issues/11)
- Fixed `AutocompleteTrigger` not getting triggered when the text is a multi-line
string. [#12](https://github.com/xsahil03x/multi_trigger_autocomplete/issues/12)
## 0.1.1
- Fixed Readme.
## 0.1.0
- Initial release.

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Sahil Kumar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,281 @@
# Multi Trigger Autocomplete Plus
[![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=102)](https://opensource.org/licenses/MIT) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/xsahil03x/multi_trigger_autocomplete/blob/master/LICENSE) [![Dart CI](https://github.com/xsahil03x/multi_trigger_autocomplete/workflows/multi_trigger_autocomplete/badge.svg)](https://github.com/xsahil03x/multi_trigger_autocomplete/actions) [![CodeCov](https://codecov.io/gh/xsahil03x/multi_trigger_autocomplete/branch/master/graph/badge.svg)](https://codecov.io/gh/xsahil03x/multi_trigger_autocomplete) [![Version](https://img.shields.io/pub/v/multi_trigger_autocomplete.svg)](https://pub.dartlang.org/packages/multi_trigger_autocomplete)
> This is a fork from [multi_trigger_autocomplete](https://pub.dev/packages/multi_trigger_autocomplete)
A flutter widget to add trigger based autocomplete functionality to your app.
**Show some ❤️ and star the repo to support the project**
<p>
<img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/package_demo.gif?raw=true" alt="An animated image of the MultiTriggerAutocomplete" height="400"/>
</p>
## Improvements
This fork includes the following improvements over the original package:
- **AutocompleteNoTrigger**: A Special trigger which allows allows autcomplete suggestions without a trigger character/string.
- **Enhanced Customization**: Allows customization of `triggerEnd` instead of using a hardcoded space (`' '`).
- **Prefix Triggers Handling**: Correctly identifies and handles triggers that share a common prefix, such as `{` and `{{`.
## Installation
Add the following to your `pubspec.yaml` and replace `[version]` with the latest version:
```yaml
dependencies:
multi_trigger_autocomplete_plus: ^[version]
```
## Usage
To use this package you must first wrap your top most widget
with [Portal](https://pub.dev/documentation/flutter_portal/latest/flutter_portal/Portal-class.html) as this package
uses [flutter_portal](https://pub.dev/packages/flutter_portal)
to show the options view.
(Credits to: [Remi Rousselet](https://github.com/rrousselGit))
> `Portal`, is the equivalent of [Overlay].
>
> This widget will need to be inserted above the widget that needs to render
> _under_ your overlays.
>
> If you want to display your overlays on the top of _everything_, a good place
> to insert that `Portal` is above `MaterialApp`:
>
> ```dart
> Portal(
> child: MaterialApp(
> ...
> )
> );
> ```
>
> (works for `CupertinoApp` too)
>
> This way `Portal` will render above everything. But you could place it
> somewhere else to change the clip behavior.
Import the package:
```dart
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
```
Use the widget:
```dart
MultiTriggerAutocomplete(
optionsAlignment: OptionsAlignment.topStart,
autocompleteTriggers: [
// Add the triggers you want to use for autocomplete
AutocompleteTrigger(
trigger: '@',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return MentionAutocompleteOptions(
query: autocompleteQuery.query,
onMentionUserTap: (user) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(user.id);
},
);
},
),
AutocompleteTrigger(
trigger: '#',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return HashtagAutocompleteOptions(
query: autocompleteQuery.query,
onHashtagTap: (hashtag) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete
.acceptAutocompleteOption(hashtag.name);
},
);
},
),
AutocompleteTrigger(
trigger: ':',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return EmojiAutocompleteOptions(
query: autocompleteQuery.query,
onEmojiTap: (emoji) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(
emoji.char,
// Passing false as we don't want the trigger [:] to
// get prefixed to the option in case of emoji.
keepTrigger: false,
);
},
);
},
),
],
// Add the text field widget you want to use for autocomplete
fieldViewBuilder: (context, controller, focusNode) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ChatMessageTextField(
focusNode: focusNode,
controller: controller,
),
);
},
),
```
## Demo
| Mention Autocomplete | Hashtag Autocomplete | Emoji Autocomplete |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/mention_demo.gif?raw=true" height="400" alt="Mention Autocomplete"/> | <img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/hashtag_demo.gif?raw=true" height="400" alt="Hashtag Autocomplete"/> | <img src="https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/blob/master/asset/emoji_demo.gif?raw=true" height="400" alt="Emoji Autocomplete"/> |
## Customization
### MultiTriggerAutocomplete
```dart
MultiTriggerAutocomplete(
// Defines the autocomplete trigger that will be used to match the
// text.
autocompleteTriggers: autocompleteTriggers,
// Defines the alignment of the options view relative to the
// fieldView.
//
// By default, the options view is aligned to the bottom of the
// fieldView.
optionsAlignment: OptionsAlignment.topStart,
// Defines the width to make the options as a multiple of the width
// of the fieldView.
//
// Setting this to 1 makes the options view width matches the width
// of the fieldView.
//
// Use null to remove this constraint.
optionsWidthFactor: 1.0,
// Defines the duration of the debounce period for the
// [TextEditingController].
//
// This is the time between the last character typed and the matching
// is performed.
debounceDuration: const Duration(milliseconds: 350),
// Defines the initial value to set in the internal
// [TextEditingController].
//
// This value will be ignored if [TextEditingController] is provided.
initialValue: const TextEditingValue(text: 'Hello'),
// Defines the [TextEditingController] that will be used for the
// fieldView.
//
// If this parameter is provided, then [focusNode] must also be
// provided.
textEditingController: TextEditingController(text: 'Hello'),
// Defines the [FocusNode] that will be used for the fieldView.
//
// If this parameter is provided, then [textEditingController] must
// also be provided.
focusNode: FocusNode(),
// Defines the fieldView that will be used to input the text.
//
// By default, a [TextFormField] is used.
fieldViewBuilder: (context, controller, focusNode) {
return TextField(
controller: controller,
focusNode: focusNode,
);
},
),
```
### AutocompleteTrigger
```dart
AutocompleteTrigger(
// The trigger string/character that will be used to trigger the
// autocomplete.
trigger: '@',
// The trigger end string/character that will be used to determine
// the end of the trigger.
// By default, it's a space.
triggerEnd: ' ',
// If true, the [trigger] should only be recognised at
// the start of the input text.
//
// valid example: "@luke hello"
// invalid example: "Hello @luke"
triggerOnlyAtStart: false,
// If true, the [trigger] should only be recognised after
// a space.
//
// valid example: "@luke", "Hello @luke"
// invalid example: "Hello@luke"
triggerOnlyAfterSpace: true,
// A minimum number of characters can be provided to only show
// suggestions after the user has input enough characters.
//
// example:
// "Hello @l" -> Shows zero suggestions.
// "Hello @lu" -> Shows suggestions for @lu.
minimumRequiredCharacters: 2,
// The options view builder is used to build the options view
// that will be shown when the [trigger] is detected.
optionsViewBuilder: (context, autocompleteQuery, controller) {
return MentionAutocompleteOptions(
query: autocompleteQuery.query,
onMentionUserTap: (user) {
// Accept the autocomplete option.
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(user.id);
},
);
},
)
```
### AutocompleteNoTrigger
Can be used to display autocomplete suggestions without requiring a trigger string or character.
```dart
AutocompleteNoTrigger(
// A minimum number of characters can be provided to only show
// suggestions after the user has input enough characters.
minimumRequiredCharacters: 2,
// The options view builder is used to build the options view
// that will be shown when the [trigger] is detected.
optionsViewBuilder: (context, autocompleteQuery, controller) {
return MentionAutocompleteOptions(
query: autocompleteQuery.query,
onMentionUserTap: (user) {
// Accept the autocomplete option.
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(user.id);
// Handle field unfocusing manually using a FocusNode
focusNode.unfocus();
},
);
},
)
```
## License
[MIT License](LICENSE)

View File

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

View File

@ -0,0 +1,47 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@ -0,0 +1,16 @@
# example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,133 @@
import 'package:example/src/chat_message_list.dart';
import 'package:example/src/chat_message_text_field.dart';
import 'package:example/src/data.dart';
import 'package:example/src/options/options.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return Portal(
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
platform: TargetPlatform.iOS,
textTheme: GoogleFonts.robotoMonoTextTheme(
Theme.of(context).textTheme,
),
),
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final messages = [...sampleGroupConversation];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
toolbarHeight: 60,
backgroundColor: const Color(0xFF5B61B9),
title: const Text(
'Multi Trigger Autocomplete',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
centerTitle: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
),
),
body: Column(
children: [
Expanded(child: ChatMessageList(messages: messages)),
MultiTriggerAutocomplete(
optionsAlignment: OptionsAlignment.topStart,
autocompleteTriggers: [
AutocompleteTrigger(
trigger: '@',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return MentionAutocompleteOptions(
query: autocompleteQuery.query,
onMentionUserTap: (user) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(user.id);
},
);
},
),
AutocompleteTrigger(
trigger: '#',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return HashtagAutocompleteOptions(
query: autocompleteQuery.query,
onHashtagTap: (hashtag) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete
.acceptAutocompleteOption(hashtag.name);
},
);
},
),
AutocompleteTrigger(
trigger: ':',
optionsViewBuilder: (context, autocompleteQuery, controller) {
return EmojiAutocompleteOptions(
query: autocompleteQuery.query,
onEmojiTap: (emoji) {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(
emoji.char,
// Passing false as we don't want the trigger [:] to
// get prefixed to the option in case of emoji.
keepTrigger: false,
);
},
);
},
),
],
fieldViewBuilder: (context, controller, focusNode) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: ChatMessageTextField(
focusNode: focusNode,
controller: controller,
onSend: (message) {
controller.clear();
setState(() {
messages.add(message);
});
},
),
);
},
),
],
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:example/src/models.dart';
import 'package:flutter/material.dart';
import 'package:flutter_parsed_text/flutter_parsed_text.dart';
import 'package:google_fonts/google_fonts.dart';
class ChatMessageList extends StatelessWidget {
const ChatMessageList({
Key? key,
required this.messages,
}) : super(key: key);
final List<ChatMessage> messages;
@override
Widget build(BuildContext context) {
final messages = this.messages.reversed.toList();
return ListView.separated(
reverse: true,
itemCount: messages.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (context, index) => const SizedBox(height: 8),
itemBuilder: (BuildContext context, int index) {
final message = messages[index];
return Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Color(0xFFE9EAF4),
borderRadius: BorderRadius.only(
topRight: Radius.circular(32.0),
topLeft: Radius.circular(32.0),
bottomLeft: Radius.circular(32.0),
),
),
child: ParsedText(
text: message.text,
style: GoogleFonts.robotoMono(
fontWeight: FontWeight.w500,
color: const Color(0xFF79708F),
),
parse: <MatchText>[
MatchText(
pattern: r"@[A-Za-z0-9_.-]*",
style: const TextStyle(color: Colors.green),
),
MatchText(
pattern: r"\B#+([\w]+)\b",
style: const TextStyle(color: Colors.blue),
),
],
),
),
),
const SizedBox(width: 8),
CircleAvatar(
backgroundImage: NetworkImage(message.sender.avatar),
),
],
);
},
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:example/src/models.dart';
import 'package:flutter/material.dart';
import 'dart:math' show Random;
import 'package:example/src/data.dart';
class ChatMessageTextField extends StatelessWidget {
const ChatMessageTextField({
Key? key,
required this.focusNode,
required this.controller,
required this.onSend,
}) : super(key: key);
final FocusNode focusNode;
final TextEditingController controller;
final ValueSetter<ChatMessage> onSend;
final _sender = const [sahil, avni, gaurav];
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 4, 4, 4),
decoration: const BoxDecoration(
color: Color(0xFFF7F7F8),
borderRadius: BorderRadius.all(Radius.circular(40.0)),
),
child: Row(
children: [
Expanded(
child: TextField(
focusNode: focusNode,
controller: controller,
decoration: const InputDecoration.collapsed(
hintText: "Type your message...",
),
),
),
IconButton(
onPressed: () {
final message = ChatMessage(
text: controller.text,
createdAt: DateTime.now(),
sender: _sender[Random().nextInt(_sender.length)],
);
onSend(message);
},
padding: const EdgeInsets.all(4),
icon: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Color(0xFF5B61B9),
),
alignment: Alignment.center,
child: const Icon(
Icons.send_rounded,
color: Colors.white,
size: 24,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,243 @@
import 'package:example/src/models.dart';
const sahil = User(
id: 'xsahil03x',
name: 'Sahil Kumar',
avatar: 'https://bit.ly/3yEVRrD',
);
const avni = User(
id: 'avu.saxena',
name: 'Avni Saxena',
avatar: 'https://bit.ly/3PbPBii',
);
const trapti = User(
id: 'trapti2711',
name: 'Trapti Gupta',
avatar: 'https://bit.ly/3aDHtba',
);
const gaurav = User(
id: 'itsmegb98',
name: 'Gaurav Bhadouriya',
avatar: 'https://bit.ly/3PmNdES',
);
const amit = User(
id: 'amitk_15',
name: 'Amit Kumar',
avatar: 'https://bit.ly/3P9GPB8',
);
const ayush = User(
id: 'ayushpgupta',
name: 'Ayush Gupta',
avatar: 'https://bit.ly/3Rw61Dv',
);
const shubham = User(
id: 'someshubham',
name: 'Shubham Jain',
avatar: 'https://bit.ly/3Rs3uud',
);
const kUsers = <User>[
sahil,
avni,
gaurav,
trapti,
amit,
ayush,
shubham,
];
const kHashtags = <Hashtag>[
Hashtag(
name: 'dart',
weight: 1,
description:
'Dart is a language for building fast, scalable and maintainable applications.',
image: 'https://dwglogo.com/wp-content/uploads/2018/03/Dart_logo.png',
),
Hashtag(
name: 'flutter',
weight: 2,
description:
'Flutter is a framework for building native Android and iOS applications for Google\'s mobile platforms.',
image:
'https://storage.googleapis.com/cms-storage-bucket/0dbfcc7a59cd1cf16282.png',
),
Hashtag(
name: 'firebase',
weight: 3,
description:
'Firebase is a cloud platform for building mobile and web apps.',
image:
'https://firebase.google.com/static/downloads/brand-guidelines/PNG/logo-logomark.png',
),
Hashtag(
name: 'google',
weight: 4,
description:
'Google is a company that builds products and services for the world\'s users.',
image: 'https://dwglogo.com/wp-content/uploads/2016/06/G-icon-1068x735.png',
),
Hashtag(
name: 'apple',
weight: 5,
description:
'Apple is a company that builds products and services for the world\'s users.',
image:
'https://dwglogo.com/wp-content/uploads/2016/02/Apple_logo-1068x601.png',
),
Hashtag(
name: 'microsoft',
weight: 6,
description:
'Microsoft is a company that builds products and services for the world\'s users.',
image:
'https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Microsoft_logo.svg/2048px-Microsoft_logo.svg.png',
),
Hashtag(
name: 'facebook',
weight: 7,
description:
'Facebook is a company that builds products and services for the world\'s users.',
image: 'https://www.facebook.com/images/fb_icon_325x325.png',
),
Hashtag(
name: 'twitter',
weight: 8,
description:
'Twitter is a company that builds products and services for the world\'s users.',
image:
'https://dwglogo.com/wp-content/uploads/2019/02/Twitter_logo-1024x705.png',
),
Hashtag(
name: 'instagram',
weight: 9,
description:
'Instagram is a company that builds products and services for the world\'s users.',
image:
'https://w7.pngwing.com/pngs/648/943/png-transparent-instagram-logo-logo-instagram-computer-icons-camera-instagram-logo-text-trademark-magenta.png',
),
Hashtag(
name: 'snapchat',
weight: 10,
description:
'Snapchat is a company that builds products and services for the world\'s users.',
image:
'https://dwglogo.com/wp-content/uploads/2016/06/dotted_logo_of_snapchat-1068x601.png',
),
Hashtag(
name: 'youtube',
weight: 11,
description:
'YouTube is a company that builds products and services for the world\'s users.',
image:
'https://dwglogo.com/wp-content/uploads/2020/05/1200px-YouTube_logo-1024x729.png',
),
];
const kEmojis = <Emoji>[
Emoji(
char: '😀',
shortName: ':grinning:',
unicode: '1f600',
),
Emoji(
char: '😂',
shortName: ':joy:',
unicode: '1f602',
),
Emoji(
char: '😃',
shortName: ':smiley:',
unicode: '1f603',
),
Emoji(
char: '😄',
shortName: ':smile:',
unicode: '1f604',
),
Emoji(
char: '😅',
shortName: ':sweat_smile:',
unicode: '1f605',
),
Emoji(
char: '😆',
shortName: ':laughing:',
unicode: '1f606',
),
Emoji(
char: '😇',
shortName: ':wink:',
unicode: '1f609',
),
Emoji(
char: '😈',
shortName: ':smirk:',
unicode: '1f60f',
),
Emoji(
char: '😉',
shortName: ':wink2:',
unicode: '1f609',
),
Emoji(
char: '😊',
shortName: ':blush:',
unicode: '1f60a',
),
Emoji(
char: '😋',
shortName: ':yum:',
unicode: '1f60b',
),
Emoji(
char: '😌',
shortName: ':relieved:',
unicode: '1f60c',
),
Emoji(
char: '😍',
shortName: ':heart_eyes:',
unicode: '1f60d',
),
];
final sampleGroupConversation = [
ChatMessage(
text: 'Hey there! What\'s up?',
createdAt: DateTime.now().subtract(const Duration(seconds: 1)),
sender: sahil,
),
ChatMessage(
text: 'Nothing. Just chilling and watching YouTube. What about you?',
createdAt: DateTime.now().subtract(const Duration(seconds: 2)),
sender: avni,
),
ChatMessage(
text: 'Yeah I know. I\'m in the same position 😂',
createdAt: DateTime.now().subtract(const Duration(seconds: 3)),
sender: sahil,
),
ChatMessage(
text: 'I\'m just trying to get some sleep',
createdAt: DateTime.now().subtract(const Duration(seconds: 4)),
sender: gaurav,
),
ChatMessage(
text:
'Same here! Been watching YouTube for the past 5 hours despite of having so much to do! 😅',
createdAt: DateTime.now().subtract(const Duration(seconds: 5)),
sender: trapti,
),
ChatMessage(
text: 'It\'s hard to be productive',
createdAt: DateTime.now().subtract(const Duration(seconds: 6)),
sender: avni,
),
];

View File

@ -0,0 +1,49 @@
class Emoji {
const Emoji({
required this.char,
required this.shortName,
required this.unicode,
});
final String char;
final String shortName;
final String unicode;
}
class Hashtag {
const Hashtag({
required this.name,
required this.weight,
required this.description,
required this.image,
});
final String name;
final int weight;
final String description;
final String image;
}
class User {
const User({
required this.id,
required this.name,
required this.avatar,
});
final String id;
final String name;
final String avatar;
}
class ChatMessage {
const ChatMessage({
required this.text,
required this.createdAt,
required this.sender,
});
final String text;
final DateTime createdAt;
final User sender;
}

View File

@ -0,0 +1,71 @@
import 'package:example/src/data.dart';
import 'package:flutter/material.dart';
import 'package:example/src/models.dart';
class EmojiAutocompleteOptions extends StatelessWidget {
const EmojiAutocompleteOptions({
Key? key,
required this.query,
required this.onEmojiTap,
}) : super(key: key);
final String query;
final ValueSetter<Emoji> onEmojiTap;
@override
Widget build(BuildContext context) {
final emojis = kEmojis.where((it) {
final normalizedOption = it.shortName.toLowerCase();
final normalizedQuery = query.toLowerCase();
return normalizedOption.contains(normalizedQuery);
});
if (emojis.isEmpty) return const SizedBox.shrink();
return Card(
margin: const EdgeInsets.all(8),
elevation: 2,
// color: _streamChatTheme.colorTheme.barsBg,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.hardEdge,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: const Color(0xFFF7F7F8),
child: ListTile(
dense: true,
horizontalTitleGap: 0,
title: Text("Emoji's matching '$query'"),
),
),
const Divider(height: 0),
LimitedBox(
maxHeight: MediaQuery.of(context).size.height * 0.3,
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: emojis.length,
separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (context, i) {
final emoji = emojis.elementAt(i);
return ListTile(
dense: true,
leading: Text(
emoji.char,
style: const TextStyle(fontSize: 24),
),
title: Text(emoji.shortName),
onTap: () => onEmojiTap(emoji),
);
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:example/src/data.dart';
import 'package:flutter/material.dart';
import 'package:example/src/models.dart';
class HashtagAutocompleteOptions extends StatelessWidget {
const HashtagAutocompleteOptions({
Key? key,
required this.query,
required this.onHashtagTap,
}) : super(key: key);
final String query;
final ValueSetter<Hashtag> onHashtagTap;
@override
Widget build(BuildContext context) {
final hashtags = kHashtags.where((it) {
final normalizedOption = it.name.toLowerCase();
final normalizedQuery = query.toLowerCase();
return normalizedOption.contains(normalizedQuery);
});
if (hashtags.isEmpty) return const SizedBox.shrink();
return Card(
margin: const EdgeInsets.all(8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.hardEdge,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: const Color(0xFFE9EAF4),
child: ListTile(
dense: true,
horizontalTitleGap: 0,
title: Text("Hashtags matching '$query'"),
),
),
const Divider(height: 0),
LimitedBox(
maxHeight: MediaQuery.of(context).size.height * 0.3,
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: hashtags.length,
separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (context, i) {
final hashtag = hashtags.elementAt(i);
return ListTile(
dense: true,
leading: CircleAvatar(
backgroundColor: const Color(0xFFF7F7F8),
backgroundImage: NetworkImage(
hashtag.image,
scale: 0.5,
),
),
title: Text('#${hashtag.name}'),
subtitle: Text(
hashtag.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => onHashtagTap(hashtag),
);
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:example/src/data.dart';
import 'package:flutter/material.dart';
import 'package:example/src/models.dart';
class MentionAutocompleteOptions extends StatelessWidget {
const MentionAutocompleteOptions({
Key? key,
required this.query,
required this.onMentionUserTap,
}) : super(key: key);
final String query;
final ValueSetter<User> onMentionUserTap;
@override
Widget build(BuildContext context) {
final users = kUsers.where((it) {
final normalizedId = it.id.toLowerCase();
final normalizedName = it.name.toLowerCase();
final normalizedQuery = query.toLowerCase();
return normalizedId.contains(normalizedQuery) ||
normalizedName.contains(normalizedQuery);
});
if (users.isEmpty) return const SizedBox.shrink();
return Card(
margin: const EdgeInsets.all(8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.hardEdge,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: const Color(0xFFF7F7F8),
child: ListTile(
dense: true,
horizontalTitleGap: 0,
title: Text("Users matching '$query'"),
),
),
LimitedBox(
maxHeight: MediaQuery.of(context).size.height * 0.3,
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: users.length,
separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (context, i) {
final user = users.elementAt(i);
return ListTile(
dense: true,
leading: CircleAvatar(
backgroundImage: NetworkImage(user.avatar),
),
title: Text(user.name),
subtitle: Text('@${user.id}'),
onTap: () => onMentionUserTap(user),
);
},
),
),
],
),
);
}
}

View File

@ -0,0 +1,3 @@
export 'emoji_autocomplete_options.dart';
export 'hashtag_autocomplete_options.dart';
export 'mention_autocomplete_options.dart';

View File

@ -0,0 +1,364 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev"
source: hosted
version: "2.12.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev"
source: hosted
version: "2.0.3"
flutter_parsed_text:
dependency: "direct main"
description:
name: flutter_parsed_text
sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
flutter_portal:
dependency: transitive
description:
name: flutter_portal
sha256: "4601b3dc24f385b3761721bd852a3f6c09cddd4e943dd184ed58ee1f43006257"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: e20ff62b158b96f392bfc8afe29dee1503c94fbea2cbe8186fd59b756b8ae982
url: "https://pub.dev"
source: hosted
version: "5.1.0"
http:
dependency: transitive
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev"
source: hosted
version: "10.0.8"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev"
source: hosted
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
multi_trigger_autocomplete_plus:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "1.0.2"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
url: "https://pub.dev"
source: hosted
version: "2.2.15"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev"
source: hosted
version: "14.3.1"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.7.0-0 <4.0.0"
flutter: ">=3.24.0"

View File

@ -0,0 +1,91 @@
name: example
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.18.0 <4.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.5
flutter_parsed_text: ^2.2.1
google_fonts: ^5.0.0
multi_trigger_autocomplete_plus:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View File

@ -0,0 +1,4 @@
# melos_managed_dependency_overrides: multi_trigger_autocomplete_plus
dependency_overrides:
multi_trigger_autocomplete_plus:
path: ..

View File

@ -0,0 +1,8 @@
library multi_trigger_autocomplete_plus;
export 'package:flutter_portal/flutter_portal.dart' show Portal;
export 'src/multi_trigger_autocomplete.dart';
export 'src/autocomplete_trigger.dart';
export 'src/autocomplete_no_trigger.dart';
export 'src/autocomplete_query.dart';

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
class AutocompleteNoTrigger extends AutocompleteTrigger {
/// Creates a [AutocompleteNoTrigger] which can be used to trigger
/// suggestions without a trigger.
const AutocompleteNoTrigger({
required super.optionsViewBuilder,
super.minimumRequiredCharacters = 0,
}) : super(
trigger: '',
triggerEnd: '',
triggerOnlyAtStart: false,
triggerOnlyAfterSpace: false,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AutocompleteNoTrigger &&
runtimeType == other.runtimeType &&
optionsViewBuilder == other.optionsViewBuilder &&
minimumRequiredCharacters == other.minimumRequiredCharacters;
@override
int get hashCode =>
optionsViewBuilder.hashCode ^ minimumRequiredCharacters.hashCode;
/// Returns the [AutocompleteQuery] for the current text field value.
@override
AutocompleteQuery? invokingTrigger(TextEditingValue textEditingValue) {
final text = textEditingValue.text;
final selection = textEditingValue.selection;
if (text.length < minimumRequiredCharacters) return null;
return AutocompleteQuery(
query: text,
selection: selection,
);
}
}

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class AutocompleteQuery {
/// Creates a [AutocompleteQuery] with the specified [query] and
/// [selection].
const AutocompleteQuery({
required this.query,
required this.selection,
});
/// The query string.
final String query;
/// The selection in the text field.
final TextSelection selection;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AutocompleteQuery &&
runtimeType == other.runtimeType &&
query == other.query &&
selection == other.selection;
@override
int get hashCode => query.hashCode ^ selection.hashCode;
}

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:multi_trigger_autocomplete_plus/src/autocomplete_query.dart';
/// The type of the [AutocompleteTrigger] callback which returns a [Widget] that
/// displays the specified [options].
typedef AutocompleteTriggerOptionsViewBuilder = Widget Function(
BuildContext context,
AutocompleteQuery autocompleteQuery,
TextEditingController textEditingController,
);
class AutocompleteTrigger {
/// Creates a [AutocompleteTrigger] which can be used to trigger
/// autocomplete suggestions.
const AutocompleteTrigger({
required this.trigger,
this.triggerEnd = ' ',
required this.optionsViewBuilder,
this.triggerOnlyAtStart = false,
this.triggerOnlyAfterSpace = true,
this.minimumRequiredCharacters = 0,
});
/// The trigger character.
///
/// eg. '@', '#', ':'
final String trigger;
/// The trigger end character.
/// This is used to determine when the trigger ends.
/// By default, it's a space.
final String triggerEnd;
/// Whether the [trigger] should only be recognised at the start of the input.
final bool triggerOnlyAtStart;
/// Whether the [trigger] should only be recognised after a space.
final bool triggerOnlyAfterSpace;
/// The minimum required characters for the [trigger] to start recognising
/// a autocomplete options.
final int minimumRequiredCharacters;
/// Builds the selectable options widgets from a list of options objects.
///
/// The options are displayed floating above or below the field using a
/// [PortalTarget] inside of an [Portal].
final AutocompleteTriggerOptionsViewBuilder optionsViewBuilder;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AutocompleteTrigger &&
runtimeType == other.runtimeType &&
trigger == other.trigger &&
triggerEnd == other.triggerEnd &&
triggerOnlyAtStart == other.triggerOnlyAtStart &&
triggerOnlyAfterSpace == other.triggerOnlyAfterSpace &&
minimumRequiredCharacters == other.minimumRequiredCharacters;
@override
int get hashCode =>
trigger.hashCode ^
triggerOnlyAtStart.hashCode ^
triggerOnlyAfterSpace.hashCode ^
minimumRequiredCharacters.hashCode;
/// Checks if the user is invoking the recognising [trigger] and returns
/// the autocomplete query if so.
AutocompleteQuery? invokingTrigger(TextEditingValue textEditingValue) {
final text = textEditingValue.text;
final selection = textEditingValue.selection;
// If the selection is invalid, then it's not a trigger.
if (!selection.isValid) return null;
final cursorPosition = selection.baseOffset;
// Find the first [trigger] location before the input cursor.
final firstTriggerIndexBeforeCursor =
text.substring(0, cursorPosition).lastIndexOf(trigger);
// If the [trigger] is not found before the cursor, then it's not a trigger.
if (firstTriggerIndexBeforeCursor == -1) return null;
// If the [trigger] is found before the cursor, but the [trigger] is only
// recognised at the start of the input, then it's not a trigger.
if (triggerOnlyAtStart && firstTriggerIndexBeforeCursor != 0) {
return null;
}
// Only show typing suggestions after a space, new line or at the start of the input.
// valid examples: "@user", "Hello @user", "Hello\n@user"
// invalid examples: "Hello@user"
final textBeforeTrigger = text.substring(0, firstTriggerIndexBeforeCursor);
if (triggerOnlyAfterSpace &&
textBeforeTrigger.isNotEmpty &&
!(textBeforeTrigger.endsWith(triggerEnd) ||
textBeforeTrigger.endsWith('\n'))) {
return null;
}
// The suggestion range. Protect against invalid ranges.
final suggestionStart = firstTriggerIndexBeforeCursor + trigger.length;
final suggestionEnd = cursorPosition;
if (suggestionStart > suggestionEnd) return null;
// Fetch the suggestion text. The suggestions can't have spaces/triggerEnd.
// valid example: "@luke_skywa..."
// invalid example: "@luke skywa..."
final suggestionText = text.substring(suggestionStart, suggestionEnd);
if (suggestionText.contains(triggerEnd)) return null;
// A minimum number of characters can be provided to only show
// suggestions after the customer has input enough characters.
if (suggestionText.length < minimumRequiredCharacters) return null;
return AutocompleteQuery(
query: suggestionText,
selection: TextSelection(
baseOffset: suggestionStart,
extentOffset: suggestionEnd,
),
);
}
}

View File

@ -0,0 +1,483 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
/// The type of the Autocomplete callback which returns the widget that
/// contains the input [TextField] or [TextFormField].
///
/// See also:
///
/// * [RawAutocomplete.fieldViewBuilder], which is of this type.
typedef MultiTriggerAutocompleteFieldViewBuilder = Widget Function(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
);
/// Positions the [AutocompleteTrigger] options around the [TextField] or
/// [TextFormField] that triggered the autocomplete.
enum OptionsAlignment {
/// Positions the options to the top of the field.
top,
/// Positions the options to the bottom of the field.
bottom,
/// Positions the options to the top left of the field.
topStart,
/// Positions the options to the top right of the field.
topEnd,
/// Positions the options to the bottom left of the field.
bottomStart,
/// Positions the options to the bottom right of the field.
bottomEnd;
Anchor _toAnchor({double? widthFactor = 1.0}) {
switch (this) {
case OptionsAlignment.top:
return Aligned(
widthFactor: widthFactor,
follower: Alignment.bottomCenter,
target: Alignment.topCenter,
);
case OptionsAlignment.bottom:
return Aligned(
widthFactor: widthFactor,
follower: Alignment.topCenter,
target: Alignment.bottomCenter,
);
case OptionsAlignment.topStart:
return Aligned(
widthFactor: widthFactor,
follower: Alignment.bottomLeft,
target: Alignment.topLeft,
);
case OptionsAlignment.topEnd:
return Aligned(
widthFactor: widthFactor,
follower: Alignment.bottomRight,
target: Alignment.topRight,
);
case OptionsAlignment.bottomStart:
return Aligned(
widthFactor: widthFactor,
follower: Alignment.topLeft,
target: Alignment.bottomLeft,
);
case OptionsAlignment.bottomEnd:
return Aligned(
widthFactor: widthFactor,
follower: Alignment.topRight,
target: Alignment.bottomRight,
);
}
}
}
/// A widget that provides a text field with autocomplete functionality.
class MultiTriggerAutocomplete extends StatefulWidget {
/// Create an instance of StreamAutocomplete.
///
/// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
/// not be null.
const MultiTriggerAutocomplete({
super.key,
required this.autocompleteTriggers,
this.fieldViewBuilder = _defaultFieldViewBuilder,
this.focusNode,
this.textEditingController,
this.initialValue,
this.optionsAlignment = OptionsAlignment.bottom,
this.optionsWidthFactor = 1.0,
this.debounceDuration = const Duration(milliseconds: 300),
}) : assert((focusNode == null) == (textEditingController == null)),
assert(
!(textEditingController != null && initialValue != null),
'textEditingController and initialValue cannot be simultaneously defined.',
);
/// The triggers that trigger autocomplete.
final Iterable<AutocompleteTrigger> autocompleteTriggers;
/// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
/// Builds the field whose input is used to get the options.
///
/// Pass the provided [TextEditingController] to the field built here so that
/// RawAutocomplete can listen for changes.
/// {@endtemplate}
final MultiTriggerAutocompleteFieldViewBuilder fieldViewBuilder;
/// The [FocusNode] that is used for the text field.
///
/// {@template flutter.widgets.RawAutocomplete.split}
/// The main purpose of this parameter is to allow the use of a separate text
/// field located in another part of the widget tree instead of the text
/// field built by [fieldViewBuilder]. For example, it may be desirable to
/// place the text field in the AppBar and the options below in the main body.
///
/// When following this pattern, [fieldViewBuilder] can return
/// `SizedBox.shrink()` so that nothing is drawn where the text field would
/// normally be. A separate text field can be created elsewhere, and a
/// FocusNode and TextEditingController can be passed both to that text field
/// and to RawAutocomplete.
///
/// {@tool dartpad}
/// This examples shows how to create an autocomplete widget with the text
/// field in the AppBar and the results in the main body of the app.
///
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
/// {@end-tool}
/// {@endtemplate}
///
/// If this parameter is not null, then [textEditingController] must also be
/// not null.
final FocusNode? focusNode;
/// The [TextEditingController] that is used for the text field.
///
/// If this parameter is not null, then [focusNode] must also be not null.
final TextEditingController? textEditingController;
/// {@template flutter.widgets.RawAutocomplete.initialValue}
/// The initial value to use for the text field.
/// {@endtemplate}
///
/// Setting the initial value does not notify [textEditingController]'s
/// listeners, and thus will not cause the options UI to appear.
///
/// This parameter is ignored if [textEditingController] is defined.
final TextEditingValue? initialValue;
/// The alignment of the options.
///
/// The default value is [MultiTriggerAutocompleteAlignment.below].
final OptionsAlignment optionsAlignment;
/// The width to make the options as a multiple of the width of the
/// field.
///
/// The default value is 1.0, which makes the options the same width
/// as the field.
final double? optionsWidthFactor;
/// The duration of the debounce period for the [TextEditingController].
///
/// The default value is [300ms].
final Duration debounceDuration;
static Widget _defaultFieldViewBuilder(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
) {
return _MultiTriggerAutocompleteField(
focusNode: focusNode,
textEditingController: textEditingController,
);
}
/// Returns the nearest [StreamAutocomplete] ancestor of the given context.
static MultiTriggerAutocompleteState of(BuildContext context) {
final state =
context.findAncestorStateOfType<MultiTriggerAutocompleteState>();
assert(state != null, 'MultiTriggerAutocomplete not found');
return state!;
}
@override
MultiTriggerAutocompleteState createState() =>
MultiTriggerAutocompleteState();
}
class MultiTriggerAutocompleteState extends State<MultiTriggerAutocomplete> {
late TextEditingController _textEditingController;
late FocusNode _focusNode;
AutocompleteQuery? _currentQuery;
AutocompleteTrigger? _currentTrigger;
bool _hideOptions = false;
String _lastFieldText = '';
// True if the state indicates that the options should be visible.
bool get _shouldShowOptions {
return !_hideOptions &&
_focusNode.hasFocus &&
_currentQuery != null &&
_currentTrigger != null;
}
void acceptAutocompleteOption(
String option, {
bool keepTrigger = true,
}) {
if (option.isEmpty) return;
final query = _currentQuery;
final trigger = _currentTrigger;
if (query == null || trigger == null) return;
final querySelection = query.selection;
final text = _textEditingController.text;
var start = querySelection.baseOffset;
if (!keepTrigger) start -= 1;
final end = querySelection.extentOffset;
final alreadyContainsTriggerEnd =
text.substring(end).startsWith(trigger.triggerEnd);
// Having triggerEnd dismissing the auto-completion view.
if (!alreadyContainsTriggerEnd) option += trigger.triggerEnd;
var selectionOffset = start + option.length;
// In case the triggerEnd is already there, we need to move the cursor
// after the triggerEnd.
if (alreadyContainsTriggerEnd) selectionOffset += trigger.triggerEnd.length;
final newText = text.replaceRange(start, end, option);
final newSelection = TextSelection.collapsed(offset: selectionOffset);
_textEditingController.value = TextEditingValue(
text: newText,
selection: newSelection,
);
return closeOptions();
}
void closeOptions() {
final prevQuery = _currentQuery;
final prevTrigger = _currentTrigger;
if (prevQuery == null || prevTrigger == null) return;
_currentQuery = null;
_currentTrigger = null;
if (mounted) setState(() {});
}
void showOptions(
AutocompleteQuery query,
AutocompleteTrigger trigger,
) {
final prevQuery = _currentQuery;
final prevTrigger = _currentTrigger;
if (prevQuery == query && prevTrigger == trigger) return;
_currentQuery = query;
_currentTrigger = trigger;
if (mounted) setState(() {});
}
// Checks if there is any invoked autocomplete trigger and returns the
// one with has the longest trigger length along with the query that
// matches the current input.
_AutocompleteInvokedTriggerWithQuery? _getInvokedTriggerWithQuery(
TextEditingValue textEditingValue,
) {
final autocompleteTriggers = widget.autocompleteTriggers.toSet();
AutocompleteTrigger? finalTrigger;
AutocompleteQuery? finalQuery;
for (final autocompleteTrigger in autocompleteTriggers) {
final query = autocompleteTrigger.invokingTrigger(textEditingValue);
if (query != null &&
(finalTrigger == null ||
autocompleteTrigger.trigger.length >
finalTrigger.trigger.length)) {
finalTrigger = autocompleteTrigger;
finalQuery = query;
}
}
if (finalTrigger != null && finalQuery != null) {
return _AutocompleteInvokedTriggerWithQuery(finalTrigger, finalQuery);
}
return null;
}
Timer? _debounceTimer;
// Called when _textEditingController changes.
void _onChangedField() {
if (_debounceTimer?.isActive == true) _debounceTimer?.cancel();
_debounceTimer = Timer(widget.debounceDuration, () {
final textEditingValue = _textEditingController.value;
// If the content has not changed, then there is nothing to do.
if (textEditingValue.text == _lastFieldText) return;
// Make sure the options are no longer hidden if the content of the
// field changes.
_hideOptions = false;
_lastFieldText = textEditingValue.text;
// If the text field is empty, then there is no need to do anything.
if (textEditingValue.text.isEmpty) return closeOptions();
// If the text field is not empty, then we need to check if the
// text field contains a trigger.
final triggerWithQuery = _getInvokedTriggerWithQuery(textEditingValue);
// If the text field does not contain a trigger, then there is no need
// to do anything.
if (triggerWithQuery == null) return closeOptions();
// If the text field contains a trigger, then we need to open the
// portal.
final trigger = triggerWithQuery.trigger;
final query = triggerWithQuery.query;
return showOptions(query, trigger);
});
}
// Called when the field's FocusNode changes.
void _onChangedFocus() {
// Options should no longer be hidden when the field is re-focused.
_hideOptions = !_focusNode.hasFocus;
if (mounted) setState(() {});
}
// Handle a potential change in textEditingController by properly disposing of
// the old one and setting up the new one, if needed.
void _updateTextEditingController(
TextEditingController? old, TextEditingController? current) {
if ((old == null && current == null) || old == current) {
return;
}
if (old == null) {
_textEditingController.removeListener(_onChangedField);
_textEditingController.dispose();
_textEditingController = current!;
} else if (current == null) {
_textEditingController.removeListener(_onChangedField);
_textEditingController = TextEditingController();
} else {
_textEditingController.removeListener(_onChangedField);
_textEditingController = current;
}
_textEditingController.addListener(_onChangedField);
}
// Handle a potential change in focusNode by properly disposing of the old one
// and setting up the new one, if needed.
void _updateFocusNode(FocusNode? old, FocusNode? current) {
if ((old == null && current == null) || old == current) {
return;
}
if (old == null) {
_focusNode.removeListener(_onChangedFocus);
_focusNode.dispose();
_focusNode = current!;
} else if (current == null) {
_focusNode.removeListener(_onChangedFocus);
_focusNode = FocusNode();
} else {
_focusNode.removeListener(_onChangedFocus);
_focusNode = current;
}
_focusNode.addListener(_onChangedFocus);
}
@override
void initState() {
super.initState();
_textEditingController = widget.textEditingController ??
TextEditingController.fromValue(widget.initialValue);
_textEditingController.addListener(_onChangedField);
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_onChangedFocus);
}
@override
void didUpdateWidget(MultiTriggerAutocomplete oldWidget) {
super.didUpdateWidget(oldWidget);
_updateTextEditingController(
oldWidget.textEditingController,
widget.textEditingController,
);
_updateFocusNode(oldWidget.focusNode, widget.focusNode);
}
@override
void dispose() {
_textEditingController.removeListener(_onChangedField);
if (widget.textEditingController == null) {
_textEditingController.dispose();
}
_focusNode.removeListener(_onChangedFocus);
if (widget.focusNode == null) {
_focusNode.dispose();
}
_debounceTimer?.cancel();
_currentTrigger = null;
_currentQuery = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
// Adding additional builder so that [MultiTriggerAutocomplete.of] works.
return Builder(
builder: (context) {
final anchor = widget.optionsAlignment._toAnchor(
widthFactor: widget.optionsWidthFactor,
);
final shouldShowOptions = _shouldShowOptions;
final optionViewBuilder = shouldShowOptions
? TextFieldTapRegion(
child: _currentTrigger!.optionsViewBuilder(
context,
_currentQuery!,
_textEditingController,
),
)
: null;
return PortalTarget(
anchor: anchor,
visible: shouldShowOptions,
portalFollower: optionViewBuilder,
child: widget.fieldViewBuilder(
context,
_textEditingController,
_focusNode,
),
);
},
);
}
}
class _AutocompleteInvokedTriggerWithQuery {
const _AutocompleteInvokedTriggerWithQuery(this.trigger, this.query);
final AutocompleteTrigger trigger;
final AutocompleteQuery query;
}
// The default Material-style Autocomplete text field.
class _MultiTriggerAutocompleteField extends StatelessWidget {
const _MultiTriggerAutocompleteField({
Key? key,
required this.focusNode,
required this.textEditingController,
}) : super(key: key);
final FocusNode focusNode;
final TextEditingController textEditingController;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: textEditingController,
focusNode: focusNode,
);
}
}

View File

@ -0,0 +1,35 @@
name: multi_trigger_autocomplete_plus
homepage: https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus
description: A flutter widget to add trigger based autocomplete functionality to your app.
version: 1.0.2
repository: https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus
issue_tracker: https://github.com/DenserMeerkat/multi_trigger_autocomplete_plus/issues
environment:
sdk: ">=2.18.0 <4.0.0"
flutter: ">=3.3.0"
dependencies:
flutter:
sdk: flutter
flutter_portal: ^1.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
topics:
- mentions
- autocomplete
- autocomplete-search
- autocomplete-textfield
- autocomplete-suggestions
screenshots:
- description: "Emoji Autocomplete Demo"
path: asset/emoji_demo.gif
- description: "Hashtag Autocomplete Demo"
path: asset/hashtag_demo.gif
- description: "Mention Autocomplete Demo"
path: asset/mention_demo.gif

View File

@ -0,0 +1,300 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:multi_trigger_autocomplete_plus/src/autocomplete_trigger.dart';
void main() {
group('Autocomplete with trigger `@`', () {
final trigger = AutocompleteTrigger(
trigger: '@',
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
});
test('should return null if `@` is not found', () {
const text = 'Hello There';
const value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
final invoked = trigger.invokingTrigger(value);
expect(invoked, isNull);
});
test(
'should return null if `@` is found but the cursor is not at the triggered word',
() {
const text = 'Hello there @Sahil Kumar';
const value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
final invoked = trigger.invokingTrigger(value);
expect(invoked, isNull);
},
);
test(
'should return the autocomplete query if `@` is found and the cursor is at the triggered word',
() {
const text = 'Hello there @Sahil Kumar';
const value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: 18),
);
final invoked = trigger.invokingTrigger(value);
expect(invoked, isNotNull);
expect(invoked!.query, 'Sahil');
expect(
invoked.selection,
const TextSelection(baseOffset: 13, extentOffset: 18),
);
},
);
test(
'should return null if `@` is found but the cursor is not after a space',
() {
const text = 'Hello there@Sahil Kumar';
const value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: 16),
);
final invoked = trigger.invokingTrigger(value);
expect(invoked, isNull);
},
);
test(
'should return the autocomplete query if `@` is found and the cursor is at the triggered word containing `_` between them',
() {
const text = 'Hello there @Sahil_Kumar';
const value = TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
final invoked = trigger.invokingTrigger(value);
expect(invoked, isNotNull);
expect(invoked!.query, 'Sahil_Kumar');
expect(
invoked.selection,
const TextSelection(baseOffset: 13, extentOffset: 24),
);
},
);
});
group('Autocomplete trigger with `triggerOnlyAtStart` true', () {
final trigger = AutocompleteTrigger(
trigger: '@',
triggerOnlyAtStart: true,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
test(
'should return query if `@` is invoked at the start and cursor is after the word',
() {
final invoked = trigger.invokingTrigger(
const TextEditingValue(
text: '@Sahil hey',
selection: TextSelection.collapsed(offset: 6),
),
);
expect(invoked, isNotNull);
expect(invoked!.query, 'Sahil');
expect(
invoked.selection,
const TextSelection(baseOffset: 1, extentOffset: 6),
);
},
);
test(
"should return null if `@` is found but it's not invoked at the start",
() {
const text = 'Hey @Sahil';
final invoked = trigger.invokingTrigger(
const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
),
);
expect(invoked, isNull);
},
);
});
group('Autocomplete trigger with `minimumRequiredCharacters` 3', () {
final trigger = AutocompleteTrigger(
trigger: '@',
minimumRequiredCharacters: 3,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
test(
'should return query if `@` is invoked cursor is after the word which is at least 3 characters long',
() {
const text = 'Hey @Sahil';
final invoked = trigger.invokingTrigger(
const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
),
);
expect(invoked, isNotNull);
expect(invoked!.query, 'Sahil');
expect(
invoked.selection,
const TextSelection(baseOffset: 5, extentOffset: 10),
);
},
);
test(
"should return null if `@` is found but the word is less than 3 characters long",
() {
const text = 'Hey @Sahil';
final invoked = trigger.invokingTrigger(
const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: 6),
),
);
expect(invoked, isNull);
},
);
});
group('2 Autocomplete trigger cannot be considered equal if', () {
test('they have different `trigger`', () {
final trigger1 = AutocompleteTrigger(
trigger: '@',
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
final trigger2 = AutocompleteTrigger(
trigger: '#',
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
expect(trigger1, isNot(trigger2));
});
test('they have different `triggerOnlyAtStart`', () {
final trigger1 = AutocompleteTrigger(
trigger: '@',
triggerOnlyAtStart: true,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
final trigger2 = AutocompleteTrigger(
trigger: '@',
triggerOnlyAtStart: false,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
expect(trigger1, isNot(trigger2));
});
test('they have different `triggerOnlyAfterSpace`', () {
final trigger1 = AutocompleteTrigger(
trigger: '@',
triggerOnlyAfterSpace: true,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
final trigger2 = AutocompleteTrigger(
trigger: '@',
triggerOnlyAfterSpace: false,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
expect(trigger1, isNot(trigger2));
});
test('they have different `minimumRequiredCharacters`', () {
final trigger1 = AutocompleteTrigger(
trigger: '@',
minimumRequiredCharacters: 3,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
final trigger2 = AutocompleteTrigger(
trigger: '@',
minimumRequiredCharacters: 4,
optionsViewBuilder: (
context,
autocompleteQuery,
textEditingController,
) {
return const SizedBox.shrink();
},
);
expect(trigger1, isNot(trigger2));
});
});
}

View File

@ -0,0 +1,328 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:multi_trigger_autocomplete_plus/multi_trigger_autocomplete.dart';
void main() {
testWidgets('should render fine', (tester) async {
const multiTriggerAutocompleteKey = Key('multiTriggerAutocomplete');
const multiTriggerAutocomplete = Boilerplate(
child: MultiTriggerAutocomplete(
key: multiTriggerAutocompleteKey,
autocompleteTriggers: [],
),
);
await tester.pumpWidget(multiTriggerAutocomplete);
expect(find.byKey(multiTriggerAutocompleteKey), findsOneWidget);
});
testWidgets(
'should render fine if both `textEditingController` and `focusNode` is provided',
(tester) async {
const multiTriggerAutocompleteKey = Key('multiTriggerAutocomplete');
final multiTriggerAutocomplete = Boilerplate(
child: MultiTriggerAutocomplete(
key: multiTriggerAutocompleteKey,
autocompleteTriggers: const [],
textEditingController: TextEditingController(),
focusNode: FocusNode(),
),
);
await tester.pumpWidget(multiTriggerAutocomplete);
expect(find.byKey(multiTriggerAutocompleteKey), findsOneWidget);
},
);
testWidgets(
"should throw assertion if `textEditingController` is provided but `focusNode` isn't",
(tester) async {
expect(
() => Boilerplate(
child: MultiTriggerAutocomplete(
autocompleteTriggers: const [],
textEditingController: TextEditingController(),
),
),
throwsAssertionError,
);
},
);
testWidgets(
"should throw assertion if `focusNode` is provided but `textEditingController` isn't",
(tester) async {
expect(
() => Boilerplate(
child: MultiTriggerAutocomplete(
autocompleteTriggers: const [],
focusNode: FocusNode(),
),
),
throwsAssertionError,
);
},
);
testWidgets(
"should render fine if `initialValue` is defined without `textEditingController`",
(tester) async {
const multiTriggerAutocompleteKey = Key('multiTriggerAutocomplete');
const multiTriggerAutocomplete = Boilerplate(
child: MultiTriggerAutocomplete(
key: multiTriggerAutocompleteKey,
autocompleteTriggers: [],
initialValue: TextEditingValue(text: 'initialValue'),
),
);
await tester.pumpWidget(multiTriggerAutocomplete);
expect(find.byKey(multiTriggerAutocompleteKey), findsOneWidget);
},
);
testWidgets(
"should throw assertion if `initialValue` is defined along with `textEditingController`",
(tester) async {
expect(
() => Boilerplate(
child: MultiTriggerAutocomplete(
autocompleteTriggers: const [],
initialValue: const TextEditingValue(text: 'initialValue'),
textEditingController: TextEditingController(),
focusNode: FocusNode(),
),
),
throwsAssertionError,
);
},
);
group('MultiTriggerAutocomplete', () {
const kUsers = [
User(id: 'xsahil03x', name: 'Sahil Kumar'),
User(id: 'avu.saxena', name: 'Avni Saxena'),
User(id: 'trapti2711', name: 'Trapti Gupta'),
User(id: 'itsmegb98', name: 'Gaurav Bhadouriya'),
User(id: 'amitk_15', name: 'Amit Kumar'),
User(id: 'ayushpgupta', name: 'Ayush Gupta'),
User(id: 'someshubham', name: 'Shubham Jain'),
];
const kDebounceDuration = Duration(milliseconds: 300);
final mentionTrigger = AutocompleteTrigger(
trigger: '@',
optionsViewBuilder: (context, autocompleteQuery, controller) {
final query = autocompleteQuery.query;
final filteredUsers = kUsers.where((user) {
return user.name.toLowerCase().contains(query.toLowerCase());
}).toList();
return ListView.builder(
itemCount: filteredUsers.length,
itemBuilder: (context, index) {
final user = filteredUsers[index];
return ListTile(
title: Text(user.name),
onTap: () {
final autocomplete = MultiTriggerAutocomplete.of(context);
return autocomplete.acceptAutocompleteOption(user.name);
},
);
},
);
},
);
testWidgets(
'can filter and select a list of string options',
(tester) async {
await tester.pumpWidget(
Boilerplate(
child: MultiTriggerAutocomplete(
debounceDuration: kDebounceDuration,
autocompleteTriggers: [mentionTrigger],
),
),
);
// The field is always rendered, but the options are not unless needed.
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsNothing);
// Entering the trigger text. All the options are displayed.
await tester.enterText(find.byType(TextFormField), '@');
await tester.pumpAndSettle(kDebounceDuration);
expect(find.byType(ListView), findsOneWidget);
ListView list =
find.byType(ListView).evaluate().first.widget as ListView;
expect(list.semanticChildCount, kUsers.length);
// Enter query text. The options are filtered by the text.
await tester.enterText(find.byType(TextFormField), '@sa');
await tester.pumpAndSettle(kDebounceDuration);
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
list = find.byType(ListView).evaluate().first.widget as ListView;
// '@Sahil kumar' and '@Avni Saxena' are displayed.
expect(list.semanticChildCount, 2);
// Select a option. The options hide and the field updates to show the
// selection.
await tester.tap(find.byType(InkWell).first);
await tester.pump();
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsNothing);
final TextFormField field =
find.byType(TextFormField).evaluate().first.widget as TextFormField;
expect(field.controller!.text, '@Sahil Kumar ');
// Modify the field text. The options appear again and are filtered.
await tester.enterText(find.byType(TextFormField), '@av');
await tester.pumpAndSettle(kDebounceDuration);
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
list = find.byType(ListView).evaluate().first.widget as ListView;
// '@Avni Saxena' and '@Gaurav Bhadouriya' are displayed.
expect(list.semanticChildCount, 2);
},
);
testWidgets(
'options position changes when alignment changed',
(tester) async {
late StateSetter setState;
OptionsAlignment alignment = OptionsAlignment.bottom;
await tester.pumpWidget(
Boilerplate(
child: StatefulBuilder(
builder: (context, setter) {
setState = setter;
return MultiTriggerAutocomplete(
optionsAlignment: alignment,
debounceDuration: kDebounceDuration,
autocompleteTriggers: [mentionTrigger],
);
},
),
),
);
// Field is shown but not options.
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsNothing);
// Enter text to show the options.
await tester.enterText(find.byType(TextFormField), '@');
await tester.pumpAndSettle(kDebounceDuration);
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
// Options are just below the field.
final optionsOffset = tester.getTopLeft(find.byType(ListView));
Offset fieldOffset = tester.getTopLeft(find.byType(TextFormField));
final fieldSize = tester.getSize(find.byType(TextFormField));
expect(optionsOffset.dy, fieldOffset.dy + fieldSize.height);
// Changing the alignment should change the position of options.
setState(() => alignment = OptionsAlignment.top);
await tester.pump();
fieldOffset = tester.getBottomLeft(find.byType(TextFormField));
final optionsOffsetOpen = tester.getBottomLeft(find.byType(ListView));
expect(optionsOffsetOpen.dy, isNot(equals(optionsOffset.dy)));
expect(optionsOffsetOpen.dy, fieldOffset.dy - fieldSize.height);
},
);
testWidgets(
'initialValue sets initial text field value',
(WidgetTester tester) async {
await tester.pumpWidget(
Boilerplate(
child: MultiTriggerAutocomplete(
// Should initialize text field with '@sa'.
initialValue: const TextEditingValue(text: '@sa'),
debounceDuration: kDebounceDuration,
autocompleteTriggers: [mentionTrigger],
),
),
);
// The field is always rendered, but the options are not unless needed.
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsNothing);
// The text editing controller value starts off with initialized value.
final TextFormField field =
find.byType(TextFormField).evaluate().first.widget as TextFormField;
expect(field.controller!.text, '@sa');
// Focus the empty field. All the options are displayed.
await tester.tap(find.byType(TextFormField));
await tester.pumpAndSettle(kDebounceDuration);
expect(find.byType(ListView), findsOneWidget);
ListView list =
find.byType(ListView).evaluate().first.widget as ListView;
// '@Sahil kumar' and '@Avni Saxena' are displayed.
expect(list.semanticChildCount, 2);
// Select an option. The options hide and the field updates to show the
// selection.
await tester.tap(find.byType(InkWell).first);
await tester.pump();
expect(find.byType(TextFormField), findsOneWidget);
expect(find.byType(ListView), findsNothing);
expect(field.controller!.text, '@Sahil Kumar ');
},
);
testWidgets('can build a custom field', (WidgetTester tester) async {
const fieldKey = Key('fieldKey');
await tester.pumpWidget(
Boilerplate(
child: MultiTriggerAutocomplete(
autocompleteTriggers: [mentionTrigger],
fieldViewBuilder: (context, controller, focusNode) {
return Container(key: fieldKey);
},
),
),
);
// The custom field is rendered and not the default TextFormField.
expect(find.byKey(fieldKey), findsOneWidget);
expect(find.byType(TextFormField), findsNothing);
});
});
}
class User {
const User({
required this.id,
required this.name,
});
final String id;
final String name;
}
class Boilerplate extends StatelessWidget {
const Boilerplate({Key? key, this.child}) : super(key: key);
final Widget? child;
@override
Widget build(BuildContext context) {
return Portal(
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: child,
),
),
);
}
}

View File

@ -5,23 +5,23 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "72.0.0" version: "76.0.0"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
source: sdk source: sdk
version: "0.3.2" version: "0.3.3"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.7.0" version: "6.11.0"
ansi_styles: ansi_styles:
dependency: transitive dependency: transitive
description: description:
@ -72,10 +72,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.12.0"
audio_session: audio_session:
dependency: transitive dependency: transitive
description: description:
@ -104,10 +104,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: boolean_selector name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
build: build:
dependency: transitive dependency: transitive
description: description:
@ -176,10 +176,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
@ -224,10 +224,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
code_builder: code_builder:
dependency: "direct main" dependency: "direct main"
description: description:
@ -240,10 +240,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.1"
conventional_commit: conventional_commit:
dependency: transitive dependency: transitive
description: description:
@ -375,10 +375,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.2"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@ -391,10 +391,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.0" version: "7.0.1"
file_selector: file_selector:
dependency: "direct main" dependency: "direct main"
description: description:
@ -876,18 +876,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.5" version: "10.0.8"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.9"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@ -924,10 +924,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: macros name: macros
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.2-main.4" version: "0.1.3-main.0"
markdown: markdown:
dependency: "direct main" dependency: "direct main"
description: description:
@ -940,10 +940,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@ -964,10 +964,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -1000,15 +1000,13 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.6.0" version: "3.6.0"
multi_trigger_autocomplete: multi_trigger_autocomplete_plus:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "packages/multi_trigger_autocomplete_plus"
ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70 relative: true
resolved-ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70 source: path
url: "https://github.com/foss42/multi_trigger_autocomplete.git" version: "1.0.2"
source: git
version: "1.0.1"
mustache_template: mustache_template:
dependency: transitive dependency: transitive
description: description:
@ -1069,10 +1067,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
@ -1157,10 +1155,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.5" version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1228,10 +1226,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: process name: process
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.2" version: "5.0.3"
prompts: prompts:
dependency: transitive dependency: transitive
description: description:
@ -1468,7 +1466,7 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
source_gen: source_gen:
dependency: transitive dependency: transitive
description: description:
@ -1505,10 +1503,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.10.1"
spot: spot:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1529,10 +1527,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" version: "1.12.1"
state_notifier: state_notifier:
dependency: transitive dependency: transitive
description: description:
@ -1545,10 +1543,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
stream_transform: stream_transform:
dependency: transitive dependency: transitive
description: description:
@ -1561,10 +1559,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: string_scanner name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.4.1"
sync_http: sync_http:
dependency: transitive dependency: transitive
description: description:
@ -1577,34 +1575,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: term_glyph name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
test: test:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: test name: test
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.25.7" version: "1.25.15"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.7.4"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.4" version: "0.6.8"
textwrap: textwrap:
dependency: transitive dependency: transitive
description: description:
@ -1801,10 +1799,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.5" version: "14.3.1"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -1841,10 +1839,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webdriver name: webdriver
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.4"
webkit_inspection_protocol: webkit_inspection_protocol:
dependency: transitive dependency: transitive
description: description:
@ -1911,5 +1909,5 @@ packages:
source: hosted source: hosted
version: "2.2.1" version: "2.2.1"
sdks: sdks:
dart: ">=3.5.3 <3.999.0" dart: ">=3.7.0-0 <3.999.0"
flutter: ">=3.24.2" flutter: ">=3.24.2"

View File

@ -45,10 +45,8 @@ dependencies:
markdown: ^7.2.2 markdown: ^7.2.2
mime_dart: ^3.0.0 mime_dart: ^3.0.0
multi_split_view: ^3.2.2 multi_split_view: ^3.2.2
multi_trigger_autocomplete: multi_trigger_autocomplete_plus:
git: path: packages/multi_trigger_autocomplete_plus
url: https://github.com/foss42/multi_trigger_autocomplete.git
ref: cb22bab30dd14452d184bc6ad3bb41b612b22c70
package_info_plus: ^8.0.2 package_info_plus: ^8.0.2
path: ^1.8.3 path: ^1.8.3
path_provider: ^2.1.2 path_provider: ^2.1.2