upload_data.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import 'dart:async';
  2. import 'package:file_picker/file_picker.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:google_fonts/google_fonts.dart';
  6. import 'package:image_picker/image_picker.dart';
  7. import 'package:mime_type/mime_type.dart';
  8. import 'package:video_player/video_player.dart';
  9. import '/flutter_flow/flutter_flow_theme.dart';
  10. import 'flutter_flow_util.dart';
  11. const allowedFormats = {'image/png', 'image/jpeg', 'video/mp4', 'image/gif'};
  12. class SelectedFile {
  13. const SelectedFile({
  14. this.storagePath = '',
  15. this.filePath,
  16. required this.bytes,
  17. this.dimensions,
  18. this.blurHash,
  19. this.originalFilename = '',
  20. });
  21. final String storagePath;
  22. final String? filePath;
  23. final Uint8List bytes;
  24. final MediaDimensions? dimensions;
  25. final String? blurHash;
  26. final String originalFilename;
  27. }
  28. class MediaDimensions {
  29. const MediaDimensions({
  30. this.height,
  31. this.width,
  32. });
  33. final double? height;
  34. final double? width;
  35. }
  36. enum MediaSource {
  37. photoGallery,
  38. videoGallery,
  39. camera,
  40. }
  41. Future<List<SelectedFile>?> selectMediaWithSourceBottomSheet({
  42. required BuildContext context,
  43. String? storageFolderPath,
  44. double? maxWidth,
  45. double? maxHeight,
  46. int? imageQuality,
  47. required bool allowPhoto,
  48. bool allowVideo = false,
  49. String pickerFontFamily = 'Roboto',
  50. Color textColor = const Color(0xFF111417),
  51. Color backgroundColor = const Color(0xFFF5F5F5),
  52. bool includeDimensions = false,
  53. bool includeBlurHash = false,
  54. }) async {
  55. final createUploadMediaListTile =
  56. (String label, MediaSource mediaSource) => ListTile(
  57. title: Text(
  58. label,
  59. textAlign: TextAlign.center,
  60. style: GoogleFonts.getFont(
  61. pickerFontFamily,
  62. color: textColor,
  63. fontWeight: FontWeight.w600,
  64. fontSize: 20,
  65. ),
  66. ),
  67. tileColor: backgroundColor,
  68. dense: false,
  69. onTap: () => Navigator.pop(
  70. context,
  71. mediaSource,
  72. ),
  73. );
  74. final mediaSource = await showModalBottomSheet<MediaSource>(
  75. context: context,
  76. backgroundColor: backgroundColor,
  77. builder: (context) {
  78. return SafeArea(
  79. top: false,
  80. child: Column(
  81. mainAxisSize: MainAxisSize.min,
  82. children: [
  83. if (!kIsWeb) ...[
  84. Padding(
  85. padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
  86. child: ListTile(
  87. title: Text(
  88. 'Choose Source',
  89. textAlign: TextAlign.center,
  90. style: GoogleFonts.getFont(
  91. pickerFontFamily,
  92. color: textColor.applyAlpha(0.65),
  93. fontWeight: FontWeight.w500,
  94. fontSize: 20,
  95. ),
  96. ),
  97. tileColor: backgroundColor,
  98. dense: false,
  99. ),
  100. ),
  101. const Divider(),
  102. ],
  103. if (allowPhoto && allowVideo) ...[
  104. createUploadMediaListTile(
  105. 'Gallery (Photo)',
  106. MediaSource.photoGallery,
  107. ),
  108. const Divider(),
  109. createUploadMediaListTile(
  110. 'Gallery (Video)',
  111. MediaSource.videoGallery,
  112. ),
  113. ] else if (allowPhoto)
  114. createUploadMediaListTile(
  115. 'Gallery',
  116. MediaSource.photoGallery,
  117. )
  118. else
  119. createUploadMediaListTile(
  120. 'Gallery',
  121. MediaSource.videoGallery,
  122. ),
  123. if (!kIsWeb) ...[
  124. const Divider(),
  125. createUploadMediaListTile('Camera', MediaSource.camera),
  126. const Divider(),
  127. ],
  128. const SizedBox(height: 10),
  129. ],
  130. ),
  131. );
  132. });
  133. if (mediaSource == null) {
  134. return null;
  135. }
  136. return selectMedia(
  137. storageFolderPath: storageFolderPath,
  138. maxWidth: maxWidth,
  139. maxHeight: maxHeight,
  140. imageQuality: imageQuality,
  141. isVideo: mediaSource == MediaSource.videoGallery ||
  142. (mediaSource == MediaSource.camera && allowVideo && !allowPhoto),
  143. mediaSource: mediaSource,
  144. includeDimensions: includeDimensions,
  145. includeBlurHash: includeBlurHash,
  146. );
  147. }
  148. Future<List<SelectedFile>?> selectMedia({
  149. String? storageFolderPath,
  150. double? maxWidth,
  151. double? maxHeight,
  152. int? imageQuality,
  153. bool isVideo = false,
  154. MediaSource mediaSource = MediaSource.camera,
  155. bool multiImage = false,
  156. bool includeDimensions = false,
  157. bool includeBlurHash = false,
  158. }) async {
  159. final picker = ImagePicker();
  160. if (multiImage) {
  161. final pickedMediaFuture = picker.pickMultiImage(
  162. maxWidth: maxWidth,
  163. maxHeight: maxHeight,
  164. imageQuality: imageQuality,
  165. );
  166. final pickedMedia = await pickedMediaFuture;
  167. if (pickedMedia.isEmpty) {
  168. return null;
  169. }
  170. return Future.wait(pickedMedia.asMap().entries.map((e) async {
  171. final index = e.key;
  172. final media = e.value;
  173. final mediaBytes = await media.readAsBytes();
  174. final path = _getStoragePath(storageFolderPath, media.name, false, index);
  175. final dimensions = includeDimensions
  176. ? isVideo
  177. ? _getVideoDimensions(media.path)
  178. : _getImageDimensions(mediaBytes)
  179. : null;
  180. return SelectedFile(
  181. storagePath: path,
  182. filePath: media.path,
  183. bytes: mediaBytes,
  184. dimensions: await dimensions,
  185. originalFilename: media.name,
  186. );
  187. }));
  188. }
  189. final source = mediaSource == MediaSource.camera
  190. ? ImageSource.camera
  191. : ImageSource.gallery;
  192. final pickedMediaFuture = isVideo
  193. ? picker.pickVideo(source: source)
  194. : picker.pickImage(
  195. maxWidth: maxWidth,
  196. maxHeight: maxHeight,
  197. imageQuality: imageQuality,
  198. source: source,
  199. );
  200. final pickedMedia = await pickedMediaFuture;
  201. final mediaBytes = await pickedMedia?.readAsBytes();
  202. if (mediaBytes == null) {
  203. return null;
  204. }
  205. final path = _getStoragePath(storageFolderPath, pickedMedia!.name, isVideo);
  206. final dimensions = includeDimensions
  207. ? isVideo
  208. ? _getVideoDimensions(pickedMedia.path)
  209. : _getImageDimensions(mediaBytes)
  210. : null;
  211. return [
  212. SelectedFile(
  213. storagePath: path,
  214. filePath: pickedMedia.path,
  215. bytes: mediaBytes,
  216. dimensions: await dimensions,
  217. originalFilename: pickedMedia.name,
  218. ),
  219. ];
  220. }
  221. bool validateFileFormat(String filePath, BuildContext context) {
  222. if (allowedFormats.contains(mime(filePath))) {
  223. return true;
  224. }
  225. ScaffoldMessenger.of(context)
  226. ..hideCurrentSnackBar()
  227. ..showSnackBar(SnackBar(
  228. content: Text('Invalid file format: ${mime(filePath)}'),
  229. ));
  230. return false;
  231. }
  232. Future<SelectedFile?> selectFile({
  233. String? storageFolderPath,
  234. List<String>? allowedExtensions,
  235. }) =>
  236. selectFiles(
  237. storageFolderPath: storageFolderPath,
  238. allowedExtensions: allowedExtensions,
  239. multiFile: false,
  240. ).then((value) => value?.first);
  241. Future<List<SelectedFile>?> selectFiles({
  242. String? storageFolderPath,
  243. List<String>? allowedExtensions,
  244. bool multiFile = false,
  245. }) async {
  246. final pickedFiles = await FilePicker.platform.pickFiles(
  247. type: allowedExtensions != null ? FileType.custom : FileType.any,
  248. allowedExtensions: allowedExtensions,
  249. withData: true,
  250. allowMultiple: multiFile,
  251. );
  252. if (pickedFiles == null || pickedFiles.files.isEmpty) {
  253. return null;
  254. }
  255. if (multiFile) {
  256. return Future.wait(pickedFiles.files.asMap().entries.map((e) async {
  257. final index = e.key;
  258. final file = e.value;
  259. final storagePath =
  260. _getStoragePath(storageFolderPath, file.name, false, index);
  261. return SelectedFile(
  262. storagePath: storagePath,
  263. filePath: isWeb ? null : file.path,
  264. bytes: file.bytes!,
  265. originalFilename: file.name,
  266. );
  267. }));
  268. }
  269. final file = pickedFiles.files.first;
  270. if (file.bytes == null) {
  271. return null;
  272. }
  273. final storagePath = _getStoragePath(storageFolderPath, file.name, false);
  274. return [
  275. SelectedFile(
  276. storagePath: storagePath,
  277. filePath: isWeb ? null : file.path,
  278. bytes: file.bytes!,
  279. originalFilename: file.name,
  280. )
  281. ];
  282. }
  283. List<SelectedFile> selectedFilesFromUploadedFiles(
  284. List<FFUploadedFile> uploadedFiles, {
  285. String? storageFolderPath,
  286. bool isMultiData = false,
  287. }) =>
  288. uploadedFiles.asMap().entries.map(
  289. (entry) {
  290. final index = entry.key;
  291. final file = entry.value;
  292. return SelectedFile(
  293. storagePath: _getStoragePath(
  294. storageFolderPath != null ? storageFolderPath : null,
  295. file.name!,
  296. false,
  297. isMultiData ? index : null,
  298. ),
  299. bytes: file.bytes!,
  300. originalFilename: file.originalFilename);
  301. },
  302. ).toList();
  303. Future<MediaDimensions> _getImageDimensions(Uint8List mediaBytes) async {
  304. final image = await decodeImageFromList(mediaBytes);
  305. return MediaDimensions(
  306. width: image.width.toDouble(),
  307. height: image.height.toDouble(),
  308. );
  309. }
  310. Future<MediaDimensions> _getVideoDimensions(String path) async {
  311. final VideoPlayerController videoPlayerController =
  312. VideoPlayerController.asset(path);
  313. await videoPlayerController.initialize();
  314. final size = videoPlayerController.value.size;
  315. return MediaDimensions(width: size.width, height: size.height);
  316. }
  317. String _getStoragePath(
  318. String? pathPrefix,
  319. String filePath,
  320. bool isVideo, [
  321. int? index,
  322. ]) {
  323. pathPrefix = _removeTrailingSlash(pathPrefix);
  324. final timestamp = DateTime.now().microsecondsSinceEpoch;
  325. // Workaround fixed by https://github.com/flutter/plugins/pull/3685
  326. // (not yet in stable).
  327. final ext = isVideo ? 'mp4' : filePath.split('.').last;
  328. final indexStr = index != null ? '_$index' : '';
  329. return '$pathPrefix/$timestamp$indexStr.$ext';
  330. }
  331. String getSignatureStoragePath([String? pathPrefix]) {
  332. pathPrefix = _removeTrailingSlash(pathPrefix);
  333. final timestamp = DateTime.now().microsecondsSinceEpoch;
  334. return '$pathPrefix/signature_$timestamp.png';
  335. }
  336. void showUploadMessage(
  337. BuildContext context,
  338. String message, {
  339. bool showLoading = false,
  340. }) {
  341. ScaffoldMessenger.of(context)
  342. ..hideCurrentSnackBar()
  343. ..showSnackBar(
  344. SnackBar(
  345. content: Row(
  346. children: [
  347. if (showLoading)
  348. Padding(
  349. padding: EdgeInsetsDirectional.only(end: 10.0),
  350. child: CircularProgressIndicator(
  351. valueColor: Theme.of(context).brightness == Brightness.dark
  352. ? AlwaysStoppedAnimation<Color>(
  353. FlutterFlowTheme.of(context).accent4)
  354. : null,
  355. ),
  356. ),
  357. Text(message),
  358. ],
  359. ),
  360. duration: showLoading ? Duration(days: 1) : Duration(seconds: 4),
  361. ),
  362. );
  363. }
  364. String? _removeTrailingSlash(String? path) => path != null && path.endsWith('/')
  365. ? path.substring(0, path.length - 1)
  366. : path;