kopia lustrzana https://gitlab.com/mysocialportal/relatica
First cut at auto-complete of profiles
rodzic
2424d9d0a9
commit
e3fa2d4bbd
|
@ -0,0 +1,77 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../globals.dart';
|
||||||
|
import '../../models/connection.dart';
|
||||||
|
import '../../services/connections_manager.dart';
|
||||||
|
|
||||||
|
class MentionAutocompleteOptions extends StatelessWidget {
|
||||||
|
const MentionAutocompleteOptions({
|
||||||
|
Key? key,
|
||||||
|
required this.query,
|
||||||
|
required this.onMentionUserTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String query;
|
||||||
|
final ValueSetter<Connection> onMentionUserTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final users = getIt<ConnectionsManager>().getMyContacts().where((it) {
|
||||||
|
final normalizedHandle = it.handle.toLowerCase();
|
||||||
|
final normalizedName = it.name.toLowerCase();
|
||||||
|
final normalizedQuery = query.toLowerCase();
|
||||||
|
return normalizedHandle.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: CachedNetworkImage(
|
||||||
|
imageUrl: user.avatarUrl.toString(),
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
title: Text(user.name),
|
||||||
|
subtitle: Text('@${user.handle}'),
|
||||||
|
onTap: () {
|
||||||
|
print('tapped');
|
||||||
|
onMentionUserTap(user);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:result_monad/result_monad.dart';
|
import 'package:result_monad/result_monad.dart';
|
||||||
|
|
||||||
|
@ -73,40 +74,42 @@ class App extends StatelessWidget {
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiProvider(
|
return Portal(
|
||||||
providers: [
|
child: MultiProvider(
|
||||||
ChangeNotifierProvider<AuthService>(
|
providers: [
|
||||||
create: (_) => getIt<AuthService>(),
|
ChangeNotifierProvider<AuthService>(
|
||||||
lazy: true,
|
create: (_) => getIt<AuthService>(),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<ConnectionsManager>(
|
||||||
|
create: (_) => getIt<ConnectionsManager>(),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<EntryManagerService>(
|
||||||
|
create: (_) => getIt<EntryManagerService>(),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<GalleryService>(
|
||||||
|
create: (_) => getIt<GalleryService>(),
|
||||||
|
lazy: true,
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<TimelineManager>(
|
||||||
|
create: (_) => getIt<TimelineManager>(),
|
||||||
|
),
|
||||||
|
ChangeNotifierProvider<NotificationsManager>(
|
||||||
|
create: (_) => getIt<NotificationsManager>(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp.router(
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.indigo,
|
||||||
|
),
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
scrollBehavior: AppScrollingBehavior(),
|
||||||
|
routerDelegate: appRouter.routerDelegate,
|
||||||
|
routeInformationProvider: appRouter.routeInformationProvider,
|
||||||
|
routeInformationParser: appRouter.routeInformationParser,
|
||||||
),
|
),
|
||||||
ChangeNotifierProvider<ConnectionsManager>(
|
|
||||||
create: (_) => getIt<ConnectionsManager>(),
|
|
||||||
lazy: true,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<EntryManagerService>(
|
|
||||||
create: (_) => getIt<EntryManagerService>(),
|
|
||||||
lazy: true,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<GalleryService>(
|
|
||||||
create: (_) => getIt<GalleryService>(),
|
|
||||||
lazy: true,
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<TimelineManager>(
|
|
||||||
create: (_) => getIt<TimelineManager>(),
|
|
||||||
),
|
|
||||||
ChangeNotifierProvider<NotificationsManager>(
|
|
||||||
create: (_) => getIt<NotificationsManager>(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: MaterialApp.router(
|
|
||||||
theme: ThemeData(
|
|
||||||
primarySwatch: Colors.indigo,
|
|
||||||
),
|
|
||||||
debugShowCheckedModeBanner: false,
|
|
||||||
scrollBehavior: AppScrollingBehavior(),
|
|
||||||
routerDelegate: appRouter.routerDelegate,
|
|
||||||
routeInformationProvider: appRouter.routeInformationProvider,
|
|
||||||
routeInformationParser: appRouter.routeInformationParser,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ class Connection {
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
|
final String handle;
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
final Uri profileUrl;
|
final Uri profileUrl;
|
||||||
|
@ -11,26 +13,27 @@ class Connection {
|
||||||
|
|
||||||
final Uri avatarUrl;
|
final Uri avatarUrl;
|
||||||
|
|
||||||
Connection(
|
Connection({this.status = ConnectionStatus.unknown,
|
||||||
{this.status = ConnectionStatus.unknown,
|
this.name = '',
|
||||||
this.name = '',
|
this.handle = '',
|
||||||
this.id = '',
|
this.id = '',
|
||||||
Uri? profileUrl,
|
Uri? profileUrl,
|
||||||
this.network = '',
|
this.network = '',
|
||||||
Uri? avatarUrl})
|
Uri? avatarUrl})
|
||||||
: profileUrl = profileUrl ?? Uri(),
|
: profileUrl = profileUrl ?? Uri(),
|
||||||
avatarUrl = avatarUrl ?? Uri();
|
avatarUrl = avatarUrl ?? Uri();
|
||||||
|
|
||||||
Connection copy(
|
Connection copy({ConnectionStatus? status,
|
||||||
{ConnectionStatus? status,
|
String? name,
|
||||||
String? name,
|
String? handle,
|
||||||
String? id,
|
String? id,
|
||||||
Uri? profileUrl,
|
Uri? profileUrl,
|
||||||
String? network,
|
String? network,
|
||||||
Uri? avatarUrl}) =>
|
Uri? avatarUrl}) =>
|
||||||
Connection(
|
Connection(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
|
handle: handle ?? this.handle,
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
profileUrl: profileUrl ?? this.profileUrl,
|
profileUrl: profileUrl ?? this.profileUrl,
|
||||||
network: network ?? this.network,
|
network: network ?? this.network,
|
||||||
|
@ -39,13 +42,14 @@ class Connection {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Connection{status: $status, name: $name, id: $id, profileUrl: $profileUrl, network: $network, avatar: $avatarUrl}';
|
return 'Connection{status: $status, name: $name, id: $id, handle: $handle, profileUrl: $profileUrl, network: $network, avatar: $avatarUrl}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is Connection && runtimeType == other.runtimeType && id == other.id;
|
other is Connection && runtimeType == other.runtimeType &&
|
||||||
|
id == other.id;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => id.hashCode;
|
int get hashCode => id.hashCode;
|
||||||
|
|
|
@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import '../controls/autocomplete/mention_autocomplete_options.dart';
|
||||||
import '../controls/entry_media_attachments/gallery_selector_control.dart';
|
import '../controls/entry_media_attachments/gallery_selector_control.dart';
|
||||||
import '../controls/entry_media_attachments/media_uploads_control.dart';
|
import '../controls/entry_media_attachments/media_uploads_control.dart';
|
||||||
import '../controls/padding.dart';
|
import '../controls/padding.dart';
|
||||||
|
@ -33,6 +35,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
TimelineEntry? parentEntry;
|
TimelineEntry? parentEntry;
|
||||||
final newMediaItems = NewEntryMediaItems();
|
final newMediaItems = NewEntryMediaItems();
|
||||||
final existingMediaItems = <ImageEntry>[];
|
final existingMediaItems = <ImageEntry>[];
|
||||||
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
var isSubmitting = false;
|
var isSubmitting = false;
|
||||||
|
|
||||||
|
@ -120,22 +123,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
TextFormField(
|
buildContentField(context),
|
||||||
readOnly: isSubmitting,
|
|
||||||
enabled: !isSubmitting,
|
|
||||||
maxLines: 10,
|
|
||||||
controller: contentController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: '$statusType Content',
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderSide: BorderSide(
|
|
||||||
color: Theme.of(context).backgroundColor,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(5.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
GallerySelectorControl(entries: existingMediaItems),
|
GallerySelectorControl(entries: existingMediaItems),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
|
@ -181,6 +169,45 @@ class _EditorScreenState extends State<EditorScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildContentField(BuildContext context) {
|
||||||
|
return MultiTriggerAutocomplete(
|
||||||
|
textEditingController: contentController,
|
||||||
|
focusNode: focusNode,
|
||||||
|
optionsAlignment: OptionsAlignment.bottomEnd,
|
||||||
|
autocompleteTriggers: [
|
||||||
|
AutocompleteTrigger(
|
||||||
|
trigger: '@',
|
||||||
|
optionsViewBuilder: (context, autocompleteQuery, controller) {
|
||||||
|
return MentionAutocompleteOptions(
|
||||||
|
query: autocompleteQuery.query,
|
||||||
|
onMentionUserTap: (user) {
|
||||||
|
final autocomplete = MultiTriggerAutocomplete.of(context);
|
||||||
|
return autocomplete.acceptAutocompleteOption(user.handle);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fieldViewBuilder: (context, controller, focusNode) => TextFormField(
|
||||||
|
focusNode: focusNode,
|
||||||
|
readOnly: isSubmitting,
|
||||||
|
enabled: !isSubmitting,
|
||||||
|
maxLines: 10,
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: '$statusType Content',
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).backgroundColor,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(5.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildCommentPreview(BuildContext context, TimelineEntry entry) {
|
Widget buildCommentPreview(BuildContext context, TimelineEntry entry) {
|
||||||
_logger.finest('Build preview');
|
_logger.finest('Build preview');
|
||||||
return Column(
|
return Column(
|
||||||
|
|
|
@ -57,17 +57,18 @@ class _UserProfileScreenState extends State<UserProfileScreen> {
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [],
|
||||||
Text(
|
|
||||||
notMyProfile
|
|
||||||
? profile.name
|
|
||||||
: '${profile.name} (Your Account)',
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
const HorizontalPadding(),
|
|
||||||
Text('( ${profile.status.label()} )'),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
notMyProfile
|
||||||
|
? '${profile.name} (${profile.handle})'
|
||||||
|
: '${profile.name} (Your Account)',
|
||||||
|
softWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const VerticalPadding(),
|
||||||
|
Text('( ${profile.status.label()} )'),
|
||||||
const VerticalPadding(),
|
const VerticalPadding(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import '../../globals.dart';
|
||||||
import '../../models/connection.dart';
|
import '../../models/connection.dart';
|
||||||
|
import '../../services/auth_service.dart';
|
||||||
|
|
||||||
extension ConnectionMastodonExtensions on Connection {
|
extension ConnectionMastodonExtensions on Connection {
|
||||||
static Connection fromJson(Map<String, dynamic> json) {
|
static Connection fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -7,10 +9,20 @@ extension ConnectionMastodonExtensions on Connection {
|
||||||
final profileUrl = Uri.parse(json['url'] ?? '');
|
final profileUrl = Uri.parse(json['url'] ?? '');
|
||||||
const network = 'Unknown';
|
const network = 'Unknown';
|
||||||
final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri();
|
final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri();
|
||||||
|
final String handleFromJson = json['acct'];
|
||||||
|
|
||||||
|
late final String handle;
|
||||||
|
if (handleFromJson.contains('@')) {
|
||||||
|
handle = handleFromJson;
|
||||||
|
} else {
|
||||||
|
final server = getIt<AuthService>().currentServer;
|
||||||
|
handle = '$handleFromJson@$server';
|
||||||
|
}
|
||||||
|
|
||||||
return Connection(
|
return Connection(
|
||||||
name: name,
|
name: name,
|
||||||
id: id,
|
id: id,
|
||||||
|
handle: handle,
|
||||||
profileUrl: profileUrl,
|
profileUrl: profileUrl,
|
||||||
network: network,
|
network: network,
|
||||||
avatarUrl: avatar,
|
avatarUrl: avatar,
|
||||||
|
|
|
@ -13,6 +13,7 @@ class AuthService extends ChangeNotifier {
|
||||||
FriendicaClient? _friendicaClient;
|
FriendicaClient? _friendicaClient;
|
||||||
String _currentId = '';
|
String _currentId = '';
|
||||||
String _currentHandle = '';
|
String _currentHandle = '';
|
||||||
|
String _currentServer = '';
|
||||||
bool _loggedIn = false;
|
bool _loggedIn = false;
|
||||||
|
|
||||||
Result<FriendicaClient, ExecError> get currentClient {
|
Result<FriendicaClient, ExecError> get currentClient {
|
||||||
|
@ -32,6 +33,8 @@ class AuthService extends ChangeNotifier {
|
||||||
|
|
||||||
String get currentHandle => _currentHandle;
|
String get currentHandle => _currentHandle;
|
||||||
|
|
||||||
|
String get currentServer => _currentServer;
|
||||||
|
|
||||||
Future<bool> getStoredLoginState() async {
|
Future<bool> getStoredLoginState() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getBool('logged-in') ?? false;
|
return prefs.getBool('logged-in') ?? false;
|
||||||
|
@ -50,6 +53,7 @@ class AuthService extends ChangeNotifier {
|
||||||
_friendicaClient = client;
|
_friendicaClient = client;
|
||||||
_currentId = result.value.id;
|
_currentId = result.value.id;
|
||||||
_currentHandle = client.credentials.handle;
|
_currentHandle = client.credentials.handle;
|
||||||
|
_currentServer = client.credentials.serverName;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return Result.ok(client);
|
return Result.ok(client);
|
||||||
}
|
}
|
||||||
|
@ -61,6 +65,7 @@ class AuthService extends ChangeNotifier {
|
||||||
_friendicaClient = null;
|
_friendicaClient = null;
|
||||||
_currentId = '';
|
_currentId = '';
|
||||||
_currentHandle = '';
|
_currentHandle = '';
|
||||||
|
_currentServer = '';
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -195,6 +195,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.7"
|
||||||
|
flutter_portal:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_portal
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.3"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -408,6 +415,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.1"
|
version: "0.4.1"
|
||||||
|
multi_trigger_autocomplete:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: multi_trigger_autocomplete
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.1"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: flutter_portal
|
name: friendica_portal
|
||||||
description: Friendica Social Network Application
|
description: Friendica Social Network Application
|
||||||
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
@ -35,6 +35,7 @@ dependencies:
|
||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
image: ^3.2.2
|
image: ^3.2.2
|
||||||
flutter_file_dialog: ^2.3.2
|
flutter_file_dialog: ^2.3.2
|
||||||
|
multi_trigger_autocomplete: ^0.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Ładowanie…
Reference in New Issue