mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 20:07:51 -06:00
feat: bugfixes, finished go_router migration, implemented better visibility of states
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class GoRouterRefreshStream extends ChangeNotifier {
|
||||
GoRouterRefreshStream(Stream<dynamic> stream) {
|
||||
notifyListeners();
|
||||
_subscription = stream.asBroadcastStream().listen(
|
||||
(dynamic _) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
|
||||
late final StreamSubscription<dynamic> _subscription;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_mobile/core/model/document_processing_status.dart';
|
||||
|
||||
class DocumentStatusCubit extends Cubit<DocumentProcessingStatus?> {
|
||||
DocumentStatusCubit() : super(null);
|
||||
|
||||
void updateStatus(DocumentProcessingStatus? status) => emit(status);
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_account.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart';
|
||||
import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
import 'package:paperless_mobile/core/repository/user_repository.dart';
|
||||
import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart';
|
||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_label_page.dart';
|
||||
import 'package:paperless_mobile/features/document_bulk_action/view/widgets/fullscreen_bulk_edit_tags_widget.dart';
|
||||
import 'package:paperless_mobile/features/home/view/model/api_version.dart';
|
||||
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view_details/cubit/saved_view_details_cubit.dart';
|
||||
import 'package:paperless_mobile/features/saved_view_details/view/saved_view_details_page.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Future<void> pushSavedViewDetailsRoute(
|
||||
BuildContext context, {
|
||||
required SavedView savedView,
|
||||
}) {
|
||||
final apiVersion = context.read<ApiVersion>();
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: apiVersion),
|
||||
if (context.watch<LocalUserAccount>().hasMultiUserSupport)
|
||||
Provider.value(value: context.read<UserRepository>()),
|
||||
Provider.value(value: context.read<LabelRepository>()),
|
||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
||||
Provider.value(value: context.read<CacheManager>()),
|
||||
Provider.value(value: context.read<ConnectivityCubit>()),
|
||||
],
|
||||
builder: (_, child) {
|
||||
return BlocProvider(
|
||||
create: (context) => SavedViewDetailsCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
LocalUserAppState.current,
|
||||
context.read(),
|
||||
savedView: savedView,
|
||||
),
|
||||
child: SavedViewDetailsPage(
|
||||
onDelete: context.read<SavedViewCubit>().remove,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pushBulkEditCorrespondentRoute(
|
||||
BuildContext context, {
|
||||
required List<DocumentModel> selection,
|
||||
}) {
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
..._getRequiredBulkEditProviders(context),
|
||||
],
|
||||
builder: (_, __) => BlocProvider(
|
||||
create: (_) => DocumentBulkActionCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
selection: selection,
|
||||
),
|
||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||
builder: (context, state) {
|
||||
return FullscreenBulkEditLabelPage(
|
||||
options: state.correspondents,
|
||||
selection: state.selection,
|
||||
labelMapper: (document) => document.correspondent,
|
||||
leadingIcon: const Icon(Icons.person_outline),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
onSubmit: context
|
||||
.read<DocumentBulkActionCubit>()
|
||||
.bulkModifyCorrespondent,
|
||||
assignMessageBuilder: (int count, String name) {
|
||||
return S.of(context)!.bulkEditCorrespondentAssignMessage(
|
||||
name,
|
||||
count,
|
||||
);
|
||||
},
|
||||
removeMessageBuilder: (int count) {
|
||||
return S
|
||||
.of(context)!
|
||||
.bulkEditCorrespondentRemoveMessage(count);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pushBulkEditStoragePathRoute(
|
||||
BuildContext context, {
|
||||
required List<DocumentModel> selection,
|
||||
}) {
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
..._getRequiredBulkEditProviders(context),
|
||||
],
|
||||
builder: (_, __) => BlocProvider(
|
||||
create: (_) => DocumentBulkActionCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
selection: selection,
|
||||
),
|
||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||
builder: (context, state) {
|
||||
return FullscreenBulkEditLabelPage(
|
||||
options: state.storagePaths,
|
||||
selection: state.selection,
|
||||
labelMapper: (document) => document.storagePath,
|
||||
leadingIcon: const Icon(Icons.folder_outlined),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
onSubmit: context
|
||||
.read<DocumentBulkActionCubit>()
|
||||
.bulkModifyStoragePath,
|
||||
assignMessageBuilder: (int count, String name) {
|
||||
return S.of(context)!.bulkEditStoragePathAssignMessage(
|
||||
count,
|
||||
name,
|
||||
);
|
||||
},
|
||||
removeMessageBuilder: (int count) {
|
||||
return S.of(context)!.bulkEditStoragePathRemoveMessage(count);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pushBulkEditTagsRoute(
|
||||
BuildContext context, {
|
||||
required List<DocumentModel> selection,
|
||||
}) {
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
..._getRequiredBulkEditProviders(context),
|
||||
],
|
||||
builder: (_, __) => BlocProvider(
|
||||
create: (_) => DocumentBulkActionCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
selection: selection,
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
return const FullscreenBulkEditTagsWidget();
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pushBulkEditDocumentTypeRoute(BuildContext context,
|
||||
{required List<DocumentModel> selection}) {
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MultiProvider(
|
||||
providers: [
|
||||
..._getRequiredBulkEditProviders(context),
|
||||
],
|
||||
builder: (_, __) => BlocProvider(
|
||||
create: (_) => DocumentBulkActionCubit(
|
||||
context.read(),
|
||||
context.read(),
|
||||
context.read(),
|
||||
selection: selection,
|
||||
),
|
||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||
builder: (context, state) {
|
||||
return FullscreenBulkEditLabelPage(
|
||||
options: state.documentTypes,
|
||||
selection: state.selection,
|
||||
labelMapper: (document) => document.documentType,
|
||||
leadingIcon: const Icon(Icons.description_outlined),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
onSubmit: context
|
||||
.read<DocumentBulkActionCubit>()
|
||||
.bulkModifyDocumentType,
|
||||
assignMessageBuilder: (int count, String name) {
|
||||
return S.of(context)!.bulkEditDocumentTypeAssignMessage(
|
||||
count,
|
||||
name,
|
||||
);
|
||||
},
|
||||
removeMessageBuilder: (int count) {
|
||||
return S
|
||||
.of(context)!
|
||||
.bulkEditDocumentTypeRemoveMessage(count);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Provider> _getRequiredBulkEditProviders(BuildContext context) {
|
||||
return [
|
||||
Provider.value(value: context.read<PaperlessDocumentsApi>()),
|
||||
Provider.value(value: context.read<LabelRepository>()),
|
||||
Provider.value(value: context.read<DocumentChangedNotifier>()),
|
||||
];
|
||||
}
|
||||
@@ -12,6 +12,10 @@ class DocumentChangedNotifier {
|
||||
|
||||
final Map<dynamic, List<StreamSubscription>> _subscribers = {};
|
||||
|
||||
Stream<DocumentModel> get $updated => _updated.asBroadcastStream();
|
||||
|
||||
Stream<DocumentModel> get $deleted => _deleted.asBroadcastStream();
|
||||
|
||||
void notifyUpdated(DocumentModel updated) {
|
||||
debugPrint("Notifying updated document ${updated.id}");
|
||||
_updated.add(updated);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
class FileDescription {
|
||||
final String filename;
|
||||
final String extension;
|
||||
|
||||
FileDescription({
|
||||
required this.filename,
|
||||
required this.extension,
|
||||
});
|
||||
|
||||
factory FileDescription.fromPath(String path) {
|
||||
final filename = path.split(RegExp(r"/")).last;
|
||||
final fragments = filename.split(".");
|
||||
final ext = fragments.removeLast();
|
||||
final name = fragments.join(".");
|
||||
return FileDescription(
|
||||
filename: name,
|
||||
extension: ext,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -14,9 +13,6 @@ class FileService {
|
||||
String filename,
|
||||
) async {
|
||||
final dir = await documentsDirectory;
|
||||
if (dir == null) {
|
||||
throw const PaperlessApiException.unknown(); //TODO: better handling
|
||||
}
|
||||
File file = File("${dir.path}/$filename");
|
||||
return file..writeAsBytes(bytes);
|
||||
}
|
||||
@@ -43,7 +39,7 @@ class FileService {
|
||||
|
||||
static Future<Directory> get temporaryDirectory => getTemporaryDirectory();
|
||||
|
||||
static Future<Directory?> get documentsDirectory async {
|
||||
static Future<Directory> get documentsDirectory async {
|
||||
if (Platform.isAndroid) {
|
||||
return (await getExternalStorageDirectories(
|
||||
type: StorageDirectory.documents,
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
|
||||
// import 'package:web_socket_channel/io.dart';
|
||||
|
||||
abstract class StatusService {
|
||||
Future<void> startListeningBeforeDocumentUpload(
|
||||
String httpUrl, UserCredentials credentials, String documentFileName);
|
||||
}
|
||||
|
||||
class WebSocketStatusService implements StatusService {
|
||||
late WebSocket? socket;
|
||||
// late IOWebSocketChannel? _channel;
|
||||
|
||||
WebSocketStatusService();
|
||||
|
||||
@override
|
||||
Future<void> startListeningBeforeDocumentUpload(
|
||||
String httpUrl,
|
||||
UserCredentials credentials,
|
||||
String documentFileName,
|
||||
) async {
|
||||
// socket = await WebSocket.connect(
|
||||
// httpUrl.replaceFirst("http", "ws") + "/ws/status/",
|
||||
// customClient: getIt<HttpClient>(),
|
||||
// headers: {
|
||||
// 'Authorization': 'Token ${credentials.token}',
|
||||
// },
|
||||
// ).catchError((_) {
|
||||
// // Use long polling if connection could not be established
|
||||
// });
|
||||
|
||||
// if (socket != null) {
|
||||
// socket!.where(isNotNull).listen((event) {
|
||||
// final status = DocumentProcessingStatus.fromJson(event);
|
||||
// getIt<DocumentStatusCubit>().updateStatus(status);
|
||||
// if (status.currentProgress == 100) {
|
||||
// socket!.close();
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
class LongPollingStatusService implements StatusService {
|
||||
final Dio client;
|
||||
const LongPollingStatusService(this.client);
|
||||
|
||||
@override
|
||||
Future<void> startListeningBeforeDocumentUpload(
|
||||
String httpUrl,
|
||||
UserCredentials credentials,
|
||||
String documentFileName,
|
||||
) async {
|
||||
// final today = DateTime.now();
|
||||
// bool consumptionFinished = false;
|
||||
// int retryCount = 0;
|
||||
|
||||
// getIt<DocumentStatusCubit>().updateStatus(
|
||||
// DocumentProcessingStatus(
|
||||
// currentProgress: 0,
|
||||
// filename: documentFileName,
|
||||
// maxProgress: 100,
|
||||
// message: ProcessingMessage.new_file,
|
||||
// status: ProcessingStatus.working,
|
||||
// taskId: DocumentProcessingStatus.unknownTaskId,
|
||||
// documentId: null,
|
||||
// isApproximated: true,
|
||||
// ),
|
||||
// );
|
||||
|
||||
// do {
|
||||
// final response = await httpClient.get(
|
||||
// Uri.parse(
|
||||
// '$httpUrl/api/documents/?query=$documentFileName added:${formatDate(today)}'),
|
||||
// );
|
||||
// final data = await compute(
|
||||
// PagedSearchResult.fromJson,
|
||||
// PagedSearchResultJsonSerializer(
|
||||
// jsonDecode(response.body), DocumentModel.fromJson),
|
||||
// );
|
||||
// if (data.count > 0) {
|
||||
// consumptionFinished = true;
|
||||
// final docId = data.results[0].id;
|
||||
// getIt<DocumentStatusCubit>().updateStatus(
|
||||
// DocumentProcessingStatus(
|
||||
// currentProgress: 100,
|
||||
// filename: documentFileName,
|
||||
// maxProgress: 100,
|
||||
// message: ProcessingMessage.finished,
|
||||
// status: ProcessingStatus.success,
|
||||
// taskId: DocumentProcessingStatus.unknownTaskId,
|
||||
// documentId: docId,
|
||||
// isApproximated: true,
|
||||
// ),
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
// sleep(const Duration(seconds: 1));
|
||||
// } while (!consumptionFinished && retryCount < maxRetries);
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
// import 'package:paperless_mobile/constants.dart';
|
||||
// import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
|
||||
// import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
|
||||
// import 'package:paperless_mobile/features/settings/model/view_type.dart';
|
||||
// import 'package:paperless_mobile/features/settings/view/settings_page.dart';
|
||||
// import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
// import 'package:url_launcher/link.dart';
|
||||
// import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
// /// Declares selectable actions in menu.
|
||||
// enum AppPopupMenuEntries {
|
||||
// // Documents preview
|
||||
// documentsSelectListView,
|
||||
// documentsSelectGridView,
|
||||
// // Generic actions
|
||||
// openAboutThisAppDialog,
|
||||
// reportBug,
|
||||
// openSettings,
|
||||
// // Adds a divider
|
||||
// divider;
|
||||
// }
|
||||
|
||||
// class AppOptionsPopupMenu extends StatelessWidget {
|
||||
// final List<AppPopupMenuEntries> displayedActions;
|
||||
// const AppOptionsPopupMenu({
|
||||
// super.key,
|
||||
// required this.displayedActions,
|
||||
// });
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return PopupMenuButton<AppPopupMenuEntries>(
|
||||
// position: PopupMenuPosition.under,
|
||||
// icon: const Icon(Icons.more_vert),
|
||||
// onSelected: (action) {
|
||||
// switch (action) {
|
||||
// case AppPopupMenuEntries.documentsSelectListView:
|
||||
// context.read<ApplicationSettingsCubit>().setViewType(ViewType.list);
|
||||
// break;
|
||||
// case AppPopupMenuEntries.documentsSelectGridView:
|
||||
// context.read<ApplicationSettingsCubit>().setViewType(ViewType.grid);
|
||||
// break;
|
||||
// case AppPopupMenuEntries.openAboutThisAppDialog:
|
||||
// _showAboutDialog(context);
|
||||
// break;
|
||||
// case AppPopupMenuEntries.openSettings:
|
||||
// Navigator.of(context).push(
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => BlocProvider.value(
|
||||
// value: context.read<ApplicationSettingsCubit>(),
|
||||
// child: const SettingsPage(),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// break;
|
||||
// case AppPopupMenuEntries.reportBug:
|
||||
// launchUrlString(
|
||||
// 'https://github.com/astubenbord/paperless-mobile/issues/new',
|
||||
// );
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// itemBuilder: _buildEntries,
|
||||
// );
|
||||
// }
|
||||
|
||||
// PopupMenuItem<AppPopupMenuEntries> _buildReportBugTile(BuildContext context) {
|
||||
// return PopupMenuItem(
|
||||
// value: AppPopupMenuEntries.reportBug,
|
||||
// padding: EdgeInsets.zero,
|
||||
// child: ListTile(
|
||||
// leading: const Icon(Icons.bug_report),
|
||||
// title: Text(S.of(context)!.reportABug),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// PopupMenuItem<AppPopupMenuEntries> _buildSettingsTile(BuildContext context) {
|
||||
// return PopupMenuItem(
|
||||
// padding: EdgeInsets.zero,
|
||||
// value: AppPopupMenuEntries.openSettings,
|
||||
// child: ListTile(
|
||||
// leading: const Icon(Icons.settings_outlined),
|
||||
// title: Text(S.of(context)!.settings),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// PopupMenuItem<AppPopupMenuEntries> _buildAboutTile(BuildContext context) {
|
||||
// return PopupMenuItem(
|
||||
// padding: EdgeInsets.zero,
|
||||
// value: AppPopupMenuEntries.openAboutThisAppDialog,
|
||||
// child: ListTile(
|
||||
// leading: const Icon(Icons.info_outline),
|
||||
// title: Text(S.of(context)!.aboutThisApp),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// PopupMenuItem<AppPopupMenuEntries> _buildListViewTile() {
|
||||
// return PopupMenuItem(
|
||||
// padding: EdgeInsets.zero,
|
||||
// child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
// builder: (context, state) {
|
||||
// return ListTile(
|
||||
// leading: const Icon(Icons.list),
|
||||
// title: const Text("List"),
|
||||
// trailing: state.preferredViewType == ViewType.list
|
||||
// ? const Icon(Icons.check)
|
||||
// : null,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// value: AppPopupMenuEntries.documentsSelectListView,
|
||||
// );
|
||||
// }
|
||||
|
||||
// PopupMenuItem<AppPopupMenuEntries> _buildGridViewTile() {
|
||||
// return PopupMenuItem(
|
||||
// value: AppPopupMenuEntries.documentsSelectGridView,
|
||||
// padding: EdgeInsets.zero,
|
||||
// child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
|
||||
// builder: (context, state) {
|
||||
// return ListTile(
|
||||
// leading: const Icon(Icons.grid_view_rounded),
|
||||
// title: const Text("Grid"),
|
||||
// trailing: state.preferredViewType == ViewType.grid
|
||||
// ? const Icon(Icons.check)
|
||||
// : null,
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// void _showAboutDialog(BuildContext context) {
|
||||
// showAboutDialog(
|
||||
// context: context,
|
||||
// applicationIcon: const ImageIcon(
|
||||
// AssetImage('assets/logos/paperless_logo_green.png'),
|
||||
// ),
|
||||
// applicationName: 'Paperless Mobile',
|
||||
// applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber,
|
||||
// children: [
|
||||
// Text(S.of(context)!.developedBy('Anton Stubenbord')),
|
||||
// Link(
|
||||
// uri: Uri.parse('https://github.com/astubenbord/paperless-mobile'),
|
||||
// builder: (context, followLink) => GestureDetector(
|
||||
// onTap: followLink,
|
||||
// child: Text(
|
||||
// 'https://github.com/astubenbord/paperless-mobile',
|
||||
// style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
// Text(
|
||||
// 'Credits',
|
||||
// style: Theme.of(context).textTheme.titleMedium,
|
||||
// ),
|
||||
// _buildOnboardingImageCredits(),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// Widget _buildOnboardingImageCredits() {
|
||||
// return Link(
|
||||
// uri: Uri.parse(
|
||||
// 'https://www.freepik.com/free-vector/business-team-working-cogwheel-mechanism-together_8270974.htm#query=setting&position=4&from_view=author'),
|
||||
// builder: (context, followLink) => Wrap(
|
||||
// children: [
|
||||
// const Text('Onboarding images by '),
|
||||
// GestureDetector(
|
||||
// onTap: followLink,
|
||||
// child: Text(
|
||||
// 'pch.vector',
|
||||
// style: TextStyle(color: Theme.of(context).colorScheme.tertiary),
|
||||
// ),
|
||||
// ),
|
||||
// const Text(' on Freepik.')
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// List<PopupMenuEntry<AppPopupMenuEntries>> _buildEntries(
|
||||
// BuildContext context) {
|
||||
// List<PopupMenuEntry<AppPopupMenuEntries>> items = [];
|
||||
// for (final entry in displayedActions) {
|
||||
// switch (entry) {
|
||||
// case AppPopupMenuEntries.documentsSelectListView:
|
||||
// items.add(_buildListViewTile());
|
||||
// break;
|
||||
// case AppPopupMenuEntries.documentsSelectGridView:
|
||||
// items.add(_buildGridViewTile());
|
||||
// break;
|
||||
// case AppPopupMenuEntries.openAboutThisAppDialog:
|
||||
// items.add(_buildAboutTile(context));
|
||||
// break;
|
||||
// case AppPopupMenuEntries.reportBug:
|
||||
// items.add(_buildReportBugTile(context));
|
||||
// break;
|
||||
// case AppPopupMenuEntries.openSettings:
|
||||
// items.add(_buildSettingsTile(context));
|
||||
// break;
|
||||
// case AppPopupMenuEntries.divider:
|
||||
// items.add(const PopupMenuDivider());
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// return items;
|
||||
// }
|
||||
// }
|
||||
@@ -1,430 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
|
||||
typedef SelectionToTextTransformer<T> = String Function(T suggestion);
|
||||
|
||||
/// Text field that auto-completes user input from a list of items
|
||||
class FormBuilderTypeAhead<T> extends FormBuilderField<T> {
|
||||
/// Called with the search pattern to get the search suggestions.
|
||||
///
|
||||
/// This callback must not be null. It is be called by the TypeAhead widget
|
||||
/// and provided with the search pattern. It should return a [List](https://api.dartlang.org/stable/2.0.0/dart-core/List-class.html)
|
||||
/// of suggestions either synchronously, or asynchronously (as the result of a
|
||||
/// [Future](https://api.dartlang.org/stable/dart-async/Future-class.html)).
|
||||
/// Typically, the list of suggestions should not contain more than 4 or 5
|
||||
/// entries. These entries will then be provided to [itemBuilder] to display
|
||||
/// the suggestions.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// suggestionsCallback: (pattern) async {
|
||||
/// return await _getSuggestions(pattern);
|
||||
/// }
|
||||
/// ```
|
||||
final SuggestionsCallback<T> suggestionsCallback;
|
||||
|
||||
/// Called when a suggestion is tapped.
|
||||
///
|
||||
/// This callback must not be null. It is called by the TypeAhead widget and
|
||||
/// provided with the value of the tapped suggestion.
|
||||
///
|
||||
/// For example, you might want to navigate to a specific view when the user
|
||||
/// tabs a suggestion:
|
||||
/// ```dart
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// Navigator.of(context).push(MaterialPageRoute(
|
||||
/// builder: (context) => SearchResult(
|
||||
/// searchItem: suggestion
|
||||
/// )
|
||||
/// ));
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Or to set the value of the text field:
|
||||
/// ```dart
|
||||
/// onSuggestionSelected: (suggestion) {
|
||||
/// _controller.text = suggestion['name'];
|
||||
/// }
|
||||
/// ```
|
||||
final SuggestionSelectionCallback<T>? onSuggestionSelected;
|
||||
|
||||
/// Called for each suggestion returned by [suggestionsCallback] to build the
|
||||
/// corresponding widget.
|
||||
///
|
||||
/// This callback must not be null. It is called by the TypeAhead widget for
|
||||
/// each suggestion, and expected to build a widget to display this
|
||||
/// suggestion's info. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// itemBuilder: (context, suggestion) {
|
||||
/// return ListTile(
|
||||
/// title: Text(suggestion['name']),
|
||||
/// subtitle: Text('USD' + suggestion['price'].toString())
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
final ItemBuilder<T> itemBuilder;
|
||||
|
||||
/// The decoration of the material sheet that contains the suggestions.
|
||||
///
|
||||
/// If null, default decoration with an elevation of 4.0 is used
|
||||
final SuggestionsBoxDecoration suggestionsBoxDecoration;
|
||||
|
||||
/// Used to control the `_SuggestionsBox`. Allows manual control to
|
||||
/// open, close, toggle, or resize the `_SuggestionsBox`.
|
||||
final SuggestionsBoxController? suggestionsBoxController;
|
||||
|
||||
/// The duration to wait after the user stops typing before calling
|
||||
/// [suggestionsCallback]
|
||||
///
|
||||
/// This is useful, because, if not set, a request for suggestions will be
|
||||
/// sent for every character that the user types.
|
||||
///
|
||||
/// This duration is set by default to 300 milliseconds
|
||||
final Duration debounceDuration;
|
||||
|
||||
/// Called when waiting for [suggestionsCallback] to return.
|
||||
///
|
||||
/// It is expected to return a widget to display while waiting.
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context) {
|
||||
/// return Text('Loading...');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, a [CircularProgressIndicator](https://docs.flutter.io/flutter/material/CircularProgressIndicator-class.html) is shown
|
||||
final WidgetBuilder? loadingBuilder;
|
||||
|
||||
/// Called when [suggestionsCallback] returns an empty array.
|
||||
///
|
||||
/// It is expected to return a widget to display when no suggestions are
|
||||
/// available.
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context) {
|
||||
/// return Text('No Items Found!');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, a simple text is shown
|
||||
final WidgetBuilder? noItemsFoundBuilder;
|
||||
|
||||
/// Called when [suggestionsCallback] throws an exception.
|
||||
///
|
||||
/// It is called with the error object, and expected to return a widget to
|
||||
/// display when an exception is thrown
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// (BuildContext context, error) {
|
||||
/// return Text('$error');
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// If not specified, the error is shown in [ThemeData.errorColor](https://docs.flutter.io/flutter/material/ThemeData/errorColor.html)
|
||||
final ErrorBuilder? errorBuilder;
|
||||
|
||||
/// Called to display animations when [suggestionsCallback] returns suggestions
|
||||
///
|
||||
/// It is provided with the suggestions box instance and the animation
|
||||
/// controller, and expected to return some animation that uses the controller
|
||||
/// to display the suggestion box.
|
||||
///
|
||||
/// For example:
|
||||
/// ```dart
|
||||
/// transitionBuilder: (context, suggestionsBox, animationController) {
|
||||
/// return FadeTransition(
|
||||
/// child: suggestionsBox,
|
||||
/// opacity: CurvedAnimation(
|
||||
/// parent: animationController,
|
||||
/// curve: Curves.fastOutSlowIn
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// This argument is best used with [animationDuration] and [animationStart]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// To fully remove the animation, just return `suggestionsBox`
|
||||
///
|
||||
/// If not specified, a [SizeTransition](https://docs.flutter.io/flutter/widgets/SizeTransition-class.html) is shown.
|
||||
final AnimationTransitionBuilder? transitionBuilder;
|
||||
|
||||
/// The duration that [transitionBuilder] animation takes.
|
||||
///
|
||||
/// This argument is best used with [transitionBuilder] and [animationStart]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// Defaults to 500 milliseconds.
|
||||
final Duration animationDuration;
|
||||
|
||||
/// Determine the [SuggestionBox]'s direction.
|
||||
///
|
||||
/// If [AxisDirection.down], the [SuggestionBox] will be below the [TextField]
|
||||
/// and the [_SuggestionsList] will grow **down**.
|
||||
///
|
||||
/// If [AxisDirection.up], the [SuggestionBox] will be above the [TextField]
|
||||
/// and the [_SuggestionsList] will grow **up**.
|
||||
///
|
||||
/// [AxisDirection.left] and [AxisDirection.right] are not allowed.
|
||||
final AxisDirection direction;
|
||||
|
||||
/// The value at which the [transitionBuilder] animation starts.
|
||||
///
|
||||
/// This argument is best used with [transitionBuilder] and [animationDuration]
|
||||
/// to fully control the animation.
|
||||
///
|
||||
/// Defaults to 0.25.
|
||||
final double animationStart;
|
||||
|
||||
/// The configuration of the [TextField](https://docs.flutter.io/flutter/material/TextField-class.html)
|
||||
/// that the TypeAhead widget displays
|
||||
final TextFieldConfiguration textFieldConfiguration;
|
||||
|
||||
/// How far below the text field should the suggestions box be
|
||||
///
|
||||
/// Defaults to 5.0
|
||||
final double suggestionsBoxVerticalOffset;
|
||||
|
||||
/// If set to true, suggestions will be fetched immediately when the field is
|
||||
/// added to the view.
|
||||
///
|
||||
/// But the suggestions box will only be shown when the field receives focus.
|
||||
/// To make the field receive focus immediately, you can set the `autofocus`
|
||||
/// property in the [textFieldConfiguration] to true
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool getImmediateSuggestions;
|
||||
|
||||
/// If set to true, no loading box will be shown while suggestions are
|
||||
/// being fetched. [loadingBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnLoading;
|
||||
|
||||
/// If set to true, nothing will be shown if there are no results.
|
||||
/// [noItemsFoundBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnEmpty;
|
||||
|
||||
/// If set to true, nothing will be shown if there is an error.
|
||||
/// [errorBuilder] will also be ignored.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool hideOnError;
|
||||
|
||||
/// If set to false, the suggestions box will stay opened after
|
||||
/// the keyboard is closed.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool hideSuggestionsOnKeyboardHide;
|
||||
|
||||
/// If set to false, the suggestions box will show a circular
|
||||
/// progress indicator when retrieving suggestions.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool keepSuggestionsOnLoading;
|
||||
|
||||
/// If set to true, the suggestions box will remain opened even after
|
||||
/// selecting a suggestion.
|
||||
///
|
||||
/// Note that if this is enabled, the only way
|
||||
/// to close the suggestions box is either manually via the
|
||||
/// `SuggestionsBoxController` or when the user closes the software
|
||||
/// keyboard if `hideSuggestionsOnKeyboardHide` is set to true. Users
|
||||
/// with a physical keyboard will be unable to close the
|
||||
/// box without a manual way via `SuggestionsBoxController`.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool keepSuggestionsOnSuggestionSelected;
|
||||
|
||||
/// If set to true, in the case where the suggestions box has less than
|
||||
/// _SuggestionsBoxController.minOverlaySpace to grow in the desired [direction], the direction axis
|
||||
/// will be temporarily flipped if there's more room available in the opposite
|
||||
/// direction.
|
||||
///
|
||||
/// Defaults to false
|
||||
final bool autoFlipDirection;
|
||||
|
||||
final SelectionToTextTransformer<T>? selectionToTextTransformer;
|
||||
|
||||
/// Controls the text being edited.
|
||||
///
|
||||
/// If null, this widget will create its own [TextEditingController].
|
||||
final TextEditingController? controller;
|
||||
|
||||
final bool hideKeyboard;
|
||||
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// Creates text field that auto-completes user input from a list of items
|
||||
FormBuilderTypeAhead({
|
||||
Key? key,
|
||||
//From Super
|
||||
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
|
||||
bool enabled = true,
|
||||
FocusNode? focusNode,
|
||||
FormFieldSetter<T>? onSaved,
|
||||
FormFieldValidator<T>? validator,
|
||||
InputDecoration decoration = const InputDecoration(),
|
||||
required String name,
|
||||
required this.itemBuilder,
|
||||
required this.suggestionsCallback,
|
||||
T? initialValue,
|
||||
ValueChanged<T?>? onChanged,
|
||||
ValueTransformer<T?>? valueTransformer,
|
||||
VoidCallback? onReset,
|
||||
this.animationDuration = const Duration(milliseconds: 500),
|
||||
this.animationStart = 0.25,
|
||||
this.autoFlipDirection = false,
|
||||
this.controller,
|
||||
this.debounceDuration = const Duration(milliseconds: 300),
|
||||
this.direction = AxisDirection.down,
|
||||
this.errorBuilder,
|
||||
this.getImmediateSuggestions = false,
|
||||
this.hideKeyboard = false,
|
||||
this.hideOnEmpty = false,
|
||||
this.hideOnError = false,
|
||||
this.hideOnLoading = false,
|
||||
this.hideSuggestionsOnKeyboardHide = true,
|
||||
this.keepSuggestionsOnLoading = true,
|
||||
this.keepSuggestionsOnSuggestionSelected = false,
|
||||
this.loadingBuilder,
|
||||
this.noItemsFoundBuilder,
|
||||
this.onSuggestionSelected,
|
||||
this.scrollController,
|
||||
this.selectionToTextTransformer,
|
||||
this.suggestionsBoxController,
|
||||
this.suggestionsBoxDecoration = const SuggestionsBoxDecoration(),
|
||||
this.suggestionsBoxVerticalOffset = 5.0,
|
||||
this.textFieldConfiguration = const TextFieldConfiguration(),
|
||||
this.transitionBuilder,
|
||||
}) : assert(T == String || selectionToTextTransformer != null),
|
||||
super(
|
||||
key: key,
|
||||
initialValue: initialValue,
|
||||
name: name,
|
||||
validator: validator,
|
||||
valueTransformer: valueTransformer,
|
||||
onChanged: onChanged,
|
||||
autovalidateMode: autovalidateMode,
|
||||
onSaved: onSaved,
|
||||
enabled: enabled,
|
||||
onReset: onReset,
|
||||
decoration: decoration,
|
||||
focusNode: focusNode,
|
||||
builder: (FormFieldState<T?> field) {
|
||||
final state = field as FormBuilderTypeAheadState<T>;
|
||||
final theme = Theme.of(state.context);
|
||||
|
||||
return TypeAheadField<T>(
|
||||
textFieldConfiguration: textFieldConfiguration.copyWith(
|
||||
enabled: state.enabled,
|
||||
controller: state._typeAheadController,
|
||||
style: state.enabled
|
||||
? textFieldConfiguration.style
|
||||
: theme.textTheme.titleMedium!.copyWith(
|
||||
color: theme.disabledColor,
|
||||
),
|
||||
focusNode: state.effectiveFocusNode,
|
||||
decoration: state.decoration,
|
||||
),
|
||||
// TODO HACK to satisfy strictness
|
||||
suggestionsCallback: suggestionsCallback,
|
||||
itemBuilder: itemBuilder,
|
||||
transitionBuilder: (context, suggestionsBox, controller) =>
|
||||
suggestionsBox,
|
||||
onSuggestionSelected: (T suggestion) {
|
||||
state.didChange(suggestion);
|
||||
onSuggestionSelected?.call(suggestion);
|
||||
},
|
||||
getImmediateSuggestions: getImmediateSuggestions,
|
||||
errorBuilder: errorBuilder,
|
||||
noItemsFoundBuilder: noItemsFoundBuilder,
|
||||
loadingBuilder: loadingBuilder,
|
||||
debounceDuration: debounceDuration,
|
||||
suggestionsBoxDecoration: suggestionsBoxDecoration,
|
||||
suggestionsBoxVerticalOffset: suggestionsBoxVerticalOffset,
|
||||
animationDuration: animationDuration,
|
||||
animationStart: animationStart,
|
||||
direction: direction,
|
||||
hideOnLoading: hideOnLoading,
|
||||
hideOnEmpty: hideOnEmpty,
|
||||
hideOnError: hideOnError,
|
||||
hideSuggestionsOnKeyboardHide: hideSuggestionsOnKeyboardHide,
|
||||
keepSuggestionsOnLoading: keepSuggestionsOnLoading,
|
||||
autoFlipDirection: autoFlipDirection,
|
||||
suggestionsBoxController: suggestionsBoxController,
|
||||
keepSuggestionsOnSuggestionSelected:
|
||||
keepSuggestionsOnSuggestionSelected,
|
||||
hideKeyboard: hideKeyboard,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
FormBuilderTypeAheadState<T> createState() => FormBuilderTypeAheadState<T>();
|
||||
}
|
||||
|
||||
class FormBuilderTypeAheadState<T>
|
||||
extends FormBuilderFieldState<FormBuilderTypeAhead<T>, T> {
|
||||
late TextEditingController _typeAheadController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_typeAheadController = widget.controller ??
|
||||
TextEditingController(text: _getTextString(initialValue));
|
||||
// _typeAheadController.addListener(_handleControllerChanged);
|
||||
}
|
||||
|
||||
// void _handleControllerChanged() {
|
||||
// Suppress changes that originated from within this class.
|
||||
//
|
||||
// In the case where a controller has been passed in to this widget, we
|
||||
// register this change listener. In these cases, we'll also receive change
|
||||
// notifications for changes originating from within this class -- for
|
||||
// example, the reset() method. In such cases, the FormField value will
|
||||
// already have been set.
|
||||
// if (_typeAheadController.text != value) {
|
||||
// didChange(_typeAheadController.text as T);
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
void didChange(T? value) {
|
||||
super.didChange(value);
|
||||
var text = _getTextString(value);
|
||||
|
||||
if (_typeAheadController.text != text) {
|
||||
_typeAheadController.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose the _typeAheadController when initState created it
|
||||
super.dispose();
|
||||
_typeAheadController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
super.reset();
|
||||
|
||||
_typeAheadController.text = _getTextString(initialValue);
|
||||
}
|
||||
|
||||
String _getTextString(T? value) {
|
||||
var text = value == null
|
||||
? ''
|
||||
: widget.selectionToTextTransformer != null
|
||||
? widget.selectionToTextTransformer!(value)
|
||||
: value.toString();
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2019 Simon Lightfoot
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
|
||||
typedef ChipSelected<T> = void Function(T data, bool selected);
|
||||
typedef ChipsBuilder<T> = Widget Function(
|
||||
BuildContext context, ChipsInputState<T> state, T data);
|
||||
|
||||
class ChipsInput<T> extends StatefulWidget {
|
||||
const ChipsInput({
|
||||
super.key,
|
||||
this.decoration = const InputDecoration(),
|
||||
required this.chipBuilder,
|
||||
required this.suggestionBuilder,
|
||||
required this.findSuggestions,
|
||||
required this.onChanged,
|
||||
this.onChipTapped,
|
||||
});
|
||||
|
||||
final InputDecoration decoration;
|
||||
final ChipsInputSuggestions<T> findSuggestions;
|
||||
final ValueChanged<List<T>> onChanged;
|
||||
final ValueChanged<T>? onChipTapped;
|
||||
final ChipsBuilder<T> chipBuilder;
|
||||
final ChipsBuilder<T> suggestionBuilder;
|
||||
|
||||
@override
|
||||
ChipsInputState<T> createState() => ChipsInputState<T>();
|
||||
}
|
||||
|
||||
class ChipsInputState<T> extends State<ChipsInput<T>> {
|
||||
static const kObjectReplacementChar = 0xFFFC;
|
||||
|
||||
Set<T> _chips = {};
|
||||
List<T> _suggestions = [];
|
||||
int _searchId = 0;
|
||||
|
||||
FocusNode _focusNode = FocusNode();
|
||||
TextEditingValue _value = const TextEditingValue();
|
||||
TextInputConnection? _connection;
|
||||
|
||||
String get text {
|
||||
return String.fromCharCodes(
|
||||
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
|
||||
);
|
||||
}
|
||||
|
||||
TextEditingValue get currentTextEditingValue => _value;
|
||||
|
||||
bool get _hasInputConnection =>
|
||||
_connection != null && (_connection?.attached ?? false);
|
||||
|
||||
void requestKeyboard() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_openInputConnection();
|
||||
} else {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
}
|
||||
}
|
||||
|
||||
void selectSuggestion(T data) {
|
||||
setState(() {
|
||||
_chips.add(data);
|
||||
_updateTextInputState();
|
||||
_suggestions = [];
|
||||
});
|
||||
widget.onChanged(_chips.toList(growable: false));
|
||||
}
|
||||
|
||||
void deleteChip(T data) {
|
||||
setState(() {
|
||||
_chips.remove(data);
|
||||
_updateTextInputState();
|
||||
});
|
||||
widget.onChanged(_chips.toList(growable: false));
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode();
|
||||
_focusNode.addListener(_onFocusChanged);
|
||||
}
|
||||
|
||||
void _onFocusChanged() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_openInputConnection();
|
||||
} else {
|
||||
_closeInputConnectionIfNeeded();
|
||||
}
|
||||
setState(() {
|
||||
// rebuild so that _TextCursor is hidden.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
_closeInputConnectionIfNeeded();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _openInputConnection() {
|
||||
if (!_hasInputConnection) {
|
||||
_connection?.setEditingState(_value);
|
||||
}
|
||||
_connection?.show();
|
||||
}
|
||||
|
||||
void _closeInputConnectionIfNeeded() {
|
||||
if (_hasInputConnection) {
|
||||
_connection?.close();
|
||||
_connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var chipsChildren = _chips
|
||||
.map<Widget>(
|
||||
(data) => widget.chipBuilder(context, this, data),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
chipsChildren.add(
|
||||
SizedBox(
|
||||
height: 32.0,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
text,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
_TextCaret(
|
||||
resumed: _focusNode.hasFocus,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
//mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: requestKeyboard,
|
||||
child: InputDecorator(
|
||||
decoration: widget.decoration,
|
||||
isFocused: _focusNode.hasFocus,
|
||||
isEmpty: _value.text.isEmpty,
|
||||
child: Wrap(
|
||||
children: chipsChildren,
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _suggestions.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return widget.suggestionBuilder(
|
||||
context, this, _suggestions[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
final oldCount = _countReplacements(_value);
|
||||
final newCount = _countReplacements(value);
|
||||
setState(() {
|
||||
if (newCount < oldCount) {
|
||||
_chips = Set.from(_chips.take(newCount));
|
||||
}
|
||||
_value = value;
|
||||
});
|
||||
_onSearchChanged(text);
|
||||
}
|
||||
|
||||
int _countReplacements(TextEditingValue value) {
|
||||
return value.text.codeUnits
|
||||
.where((ch) => ch == kObjectReplacementChar)
|
||||
.length;
|
||||
}
|
||||
|
||||
void _updateTextInputState() {
|
||||
final text =
|
||||
String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
|
||||
_value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
composing: TextRange(start: 0, end: text.length),
|
||||
);
|
||||
_connection?.setEditingState(_value);
|
||||
}
|
||||
|
||||
void _onSearchChanged(String value) async {
|
||||
final localId = ++_searchId;
|
||||
final results = await widget.findSuggestions(value);
|
||||
if (_searchId == localId && mounted) {
|
||||
setState(() => _suggestions = results
|
||||
.where((profile) => !_chips.contains(profile))
|
||||
.toList(growable: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TextCaret extends StatefulWidget {
|
||||
const _TextCaret({
|
||||
this.resumed = false,
|
||||
});
|
||||
|
||||
final bool resumed;
|
||||
|
||||
@override
|
||||
_TextCursorState createState() => _TextCursorState();
|
||||
}
|
||||
|
||||
class _TextCursorState extends State<_TextCaret>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _displayed = false;
|
||||
late Timer _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _onTimer(Timer timer) {
|
||||
setState(() => _displayed = !_displayed);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return FractionallySizedBox(
|
||||
heightFactor: 0.7,
|
||||
child: Opacity(
|
||||
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
width: 2.0,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user