From 52f209da9a24c4df6652a01c79361a3913fd8fb2 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 4 Jun 2024 11:16:35 -0300 Subject: [PATCH 01/17] thumbnail generator poc --- lib/dev_tools/app_dev_tools.dart | 25 +++++++ lib/dev_tools/pick_image_thumbnail_poc.dart | 34 ++++++++++ .../lib/src/ardrive_uploader.dart | 58 +++++++++++++++++ .../lib/src/arfs_upload_metadata.dart | 11 ++++ .../lib/src/data_bundler.dart | 53 +++++++++++++-- .../lib/src/metadata_generator.dart | 43 ++++++++++++ .../lib/src/upload_controller.dart | 65 +++++++++++++++++++ 7 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 lib/dev_tools/pick_image_thumbnail_poc.dart diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index f79540e54c..d00b26e2ee 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -1,10 +1,12 @@ import 'dart:convert'; +import 'package:ardrive/dev_tools/pick_image_thumbnail_poc.dart'; import 'package:ardrive/main.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/services/config/config.dart'; import 'package:ardrive/turbo/topup/blocs/payment_form/payment_form_bloc.dart'; import 'package:ardrive/utils/logger.dart'; +import 'package:ardrive/utils/show_general_dialog.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/material.dart'; @@ -329,6 +331,28 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.button, ); + final ArDriveDevToolOption pickImageAndGenerateThumbnailItem = + ArDriveDevToolOption( + name: 'setDefaultDataOnPaymentForm', + value: '', + onChange: (value) {}, + onInteraction: () { + try { + pickImageAndGenerateThumbnail( + onThumbnailGenerated: (thumbnail) { + showArDriveDialog(context, + content: ArDriveStandardModal( + content: Image.memory(thumbnail), + )); + }, + ); + } catch (e) { + logger.e('Error setting default data on payment form', e); + } + }, + type: ArDriveDevToolOptionType.button, + ); + final ArDriveDevToolOption forceNoFreeThanksToTurbo = ArDriveDevToolOption( name: 'forceNoFreeThanksToTurbo', value: config.forceNoFreeThanksToTurbo, @@ -377,6 +401,7 @@ class AppConfigWindowManagerState extends State { useTurboPaymentOption, defaultTurboPaymentUrlOption, enableSyncFromSnapshotOption, + pickImageAndGenerateThumbnailItem, stripePublishableKey, enableQuickSyncAuthoringOption, enableMultipleFileDownloadOption, diff --git a/lib/dev_tools/pick_image_thumbnail_poc.dart b/lib/dev_tools/pick_image_thumbnail_poc.dart new file mode 100644 index 0000000000..8790904723 --- /dev/null +++ b/lib/dev_tools/pick_image_thumbnail_poc.dart @@ -0,0 +1,34 @@ +import 'dart:typed_data'; + +import 'package:image/image.dart' as img; +import 'package:universal_html/html.dart' as html; + +Future pickImageAndGenerateThumbnail({ + Function(Uint8List thumbnail)? onThumbnailGenerated, +}) async { + html.FileUploadInputElement uploadInput = html.FileUploadInputElement(); + uploadInput.accept = 'image/*'; + uploadInput.click(); + + uploadInput.onChange.listen((e) async { + final files = uploadInput.files; + if (files?.length == 1) { + final reader = html.FileReader(); + reader.readAsArrayBuffer(files![0]); + reader.onLoadEnd.listen((e) async { + final bytes = reader.result as Uint8List; + final thumbnail = generateThumbnail(bytes); + onThumbnailGenerated?.call(thumbnail); + // Use the thumbnail as needed + }); + } + }); +} + +Uint8List generateThumbnail(Uint8List data) { + final image = img.decodeImage(data); + final thumbnail = img.copyResize(image!, + width: + 100); // Resize the image to a width of 100 pixels, maintaining aspect ratio + return Uint8List.fromList(img.encodePng(thumbnail)); +} diff --git a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart index 0a3c2e76c2..41df871ad8 100644 --- a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart +++ b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart @@ -7,6 +7,7 @@ import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart' hide Cipher; import 'package:pst/pst.dart'; +import 'package:uuid/uuid.dart'; enum UploadType { turbo, d2n } @@ -41,6 +42,15 @@ abstract class ArDriveUploader { throw UnimplementedError(); } + Future uploadThumbnail({ + required IOFile file, + required Wallet wallet, + required UploadType type, + required ThumbnailMetadataArgs args, + }) { + throw UnimplementedError(); + } + factory ArDriveUploader({ ARFSUploadMetadataGenerator? metadataGenerator, required Uri turboUploadUri, @@ -297,6 +307,54 @@ class _ArDriveUploader implements ArDriveUploader { return uploadController; } + + @override + Future uploadThumbnail( + {required IOFile file, + required Wallet wallet, + required UploadType type, + required ThumbnailMetadataArgs args}) async { + final thumbnailMetadataGenerator = ThumbnailMetadataGenerator(); + + final thumbnailMetadata = await thumbnailMetadataGenerator.generateMetadata( + file, + arguments: args, + ); + + final dataBundler = _dataBundlerFactory.createDataBundler( + type, + ); + + final UploadFileStrategy uploadFileStrategy = UploadFileUsingDataItemFiles( + streamedUploadFactory: _streamedUploadFactory); + + final uploadController = UploadController( + StreamController(), + UploadDispatcher( + dataBundler: dataBundler, + uploadStrategy: uploadFileStrategy, + uploadFolderStrategy: UploadFolderStructureAsBundleStrategy( + dataBundler: dataBundler, + streamedUploadFactory: _streamedUploadFactory, + ), + ), + numOfWorkers: 1, + maxTasksPerWorker: 1, + ); + + final uploadTask = ThumbnailUploadTask( + file: file, + metadata: thumbnailMetadata, + type: type, + id: Uuid().v4(), + ); + + uploadController.addTask(uploadTask); + + uploadController.sendTasks(wallet); + + return uploadController; + } } class DataResultWithContents { diff --git a/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart b/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart index ae8d85fbfd..eb7cb786bf 100644 --- a/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart +++ b/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart @@ -2,6 +2,17 @@ import 'package:arweave/arweave.dart'; abstract class UploadMetadata {} +class ThumbnailUploadMetadata extends UploadMetadata { + ThumbnailUploadMetadata({ + required this.entityMetadataTags, + required this.thumbnailSize, + }); + + final List entityMetadataTags; + + final int thumbnailSize; +} + class ARFSDriveUploadMetadata extends ARFSUploadMetadata { ARFSDriveUploadMetadata({ required super.entityMetadataTags, diff --git a/packages/ardrive_uploader/lib/src/data_bundler.dart b/packages/ardrive_uploader/lib/src/data_bundler.dart index 519de3c2e8..d2b3023e48 100644 --- a/packages/ardrive_uploader/lib/src/data_bundler.dart +++ b/packages/ardrive_uploader/lib/src/data_bundler.dart @@ -52,6 +52,13 @@ abstract class DataBundler { required Wallet wallet, SecretKey? driveKey, }); + + Future createDataItemForThumbnail({ + required IOFile file, + required ThumbnailUploadMetadata metadata, + required Wallet wallet, + SecretKey? driveKey, + }); } class DataTransactionBundler implements DataBundler { @@ -300,7 +307,7 @@ class DataTransactionBundler implements DataBundler { final dataGenerator = await _dataGenerator( dataStream: file.openReadStream, fileLength: await file.length, - metadata: metadata, + metadataId: metadata.id, wallet: wallet, encryptionKey: key, ); @@ -324,6 +331,16 @@ class DataTransactionBundler implements DataBundler { return [metadataDataItem, fileDataItem]; } + + @override + Future createDataItemForThumbnail( + {required IOFile file, + required ThumbnailUploadMetadata metadata, + required Wallet wallet, + SecretKey? driveKey}) { + // TODO: implement createDataItemForThumbnail + throw UnimplementedError(); + } } class BDIDataBundler implements DataBundler { @@ -544,7 +561,7 @@ class BDIDataBundler implements DataBundler { final dataGenerator = await _dataGenerator( dataStream: file.openReadStream, fileLength: await file.length, - metadata: metadata, + metadataId: metadata.id, wallet: wallet, encryptionKey: key, ); @@ -566,6 +583,34 @@ class BDIDataBundler implements DataBundler { return [metadataDataItem, fileDataItem]; } + + @override + Future createDataItemForThumbnail({ + required IOFile file, + required ThumbnailUploadMetadata metadata, + required Wallet wallet, + SecretKey? driveKey, + }) async { + final dataGenerator = await _dataGenerator( + dataStream: file.openReadStream, + fileLength: metadata.thumbnailSize, + + /// pass the file original file id + metadataId: '', + wallet: wallet, + encryptionKey: driveKey, + ); + + final thumbnailDataItem = DataItemFile( + dataSize: metadata.thumbnailSize, + streamGenerator: dataGenerator.$1, + tags: metadata.entityMetadataTags + .map((e) => createTag(e.name, e.value)) + .toList(), + ); + + return thumbnailDataItem; + } } DataItemFile _generateFileDataItem({ @@ -836,7 +881,7 @@ Future< String? cipher, int fileSize )> _dataGenerator({ - required ARFSUploadMetadata metadata, + required String metadataId, required Stream Function() dataStream, required int fileLength, required Wallet wallet, @@ -844,7 +889,7 @@ Future< }) async { if (encryptionKey != null) { return await handleEncryption( - encryptionKey, dataStream, metadata.id, fileLength, keyByteLength); + encryptionKey, dataStream, metadataId, fileLength, keyByteLength); } else { return ( dataStream, diff --git a/packages/ardrive_uploader/lib/src/metadata_generator.dart b/packages/ardrive_uploader/lib/src/metadata_generator.dart index 84c11f738a..97eee9e921 100644 --- a/packages/ardrive_uploader/lib/src/metadata_generator.dart +++ b/packages/ardrive_uploader/lib/src/metadata_generator.dart @@ -473,3 +473,46 @@ List get _uTags { Tag(EntityTag.contract, uContractId.toString()), ]; } + +class ThumbnailMetadataGenerator + implements + UploadMetadataGenerator { + @override + Future generateMetadata( + IOEntity entity, { + required ThumbnailMetadataArgs arguments, + }) async { + if (entity is IOFile) { + final file = entity; + + final tags = [ + Tag('Relates-To', file.contentType), + Tag(EntityTag.contentType, arguments.contentType), + Tag('width', arguments.width.toString()), + Tag('height', arguments.height.toString()), + ]; + + return ThumbnailUploadMetadata( + entityMetadataTags: tags, + thumbnailSize: arguments.thumbnailSize, + ); + } + + throw Exception('Invalid entity type'); + } +} + +class ThumbnailMetadataArgs { + final int height; + final int width; + final String contentType; + final int thumbnailSize; + + ThumbnailMetadataArgs({ + required this.contentType, + required this.height, + required this.width, + required this.thumbnailSize, + }); +} diff --git a/packages/ardrive_uploader/lib/src/upload_controller.dart b/packages/ardrive_uploader/lib/src/upload_controller.dart index eb9c6648b1..e6b8e20fc5 100644 --- a/packages/ardrive_uploader/lib/src/upload_controller.dart +++ b/packages/ardrive_uploader/lib/src/upload_controller.dart @@ -834,6 +834,71 @@ class FileUploadTask extends UploadTask { UploadType type; } +class ThumbnailUploadTask implements UploadTask { + final IOFile file; + final ThumbnailUploadMetadata metadata; + @override + final UploadItem? uploadItem; + @override + final double progress; + @override + final String id; + @override + final bool isProgressAvailable; + @override + final UploadStatus status; + @override + final SecretKey? encryptionKey; + @override + final UploadTaskCancelToken? cancelToken; + @override + final UploadType type; + + ThumbnailUploadTask({ + required this.file, + required this.metadata, + this.uploadItem, + this.progress = 0, + required this.id, + this.isProgressAvailable = true, + this.status = UploadStatus.notStarted, + this.encryptionKey, + this.cancelToken, + required this.type, + }); + + @override + ThumbnailUploadTask copyWith({ + IOFile? file, + ThumbnailUploadMetadata? metadata, + UploadItem? uploadItem, + List? content, + double? progress, + String? id, + bool? isProgressAvailable, + UploadStatus? status, + SecretKey? encryptionKey, + UploadTaskCancelToken? cancelToken, + UploadType? type, + }) { + return ThumbnailUploadTask( + cancelToken: cancelToken ?? this.cancelToken, + encryptionKey: encryptionKey ?? this.encryptionKey, + metadata: metadata ?? this.metadata, + uploadItem: uploadItem ?? this.uploadItem, + id: id ?? this.id, + isProgressAvailable: isProgressAvailable ?? this.isProgressAvailable, + status: status ?? this.status, + file: file ?? this.file, + progress: progress ?? this.progress, + type: type ?? this.type, + ); + } + + @override + List get content => []; +} + abstract class UploadTask { abstract final String id; abstract final UploadItem? uploadItem; From 1574ea3e2703026965b14c18e1300d53576a7873 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:15:04 -0300 Subject: [PATCH 02/17] feat(thumbnail): wip: - implement thumbnail generation tool on dev tools - implement method to upload thumbnail on uploader API -when uploading images, upload the thumbnail in the sequence --- lib/blocs/upload/upload_cubit.dart | 1 + lib/components/side_bar.dart | 40 +- lib/components/upload_form.dart | 3 + lib/dev_tools/app_dev_tools.dart | 73 ++- lib/dev_tools/drives_health_check.dart | 527 ++++++++++++++++++ lib/dev_tools/thumbnail_generator_poc.dart | 169 ++++++ .../repository/thumbnail_repository.dart | 2 +- lib/entities/file_entity.dart | 3 + lib/models/file_entry.dart | 1 + lib/models/file_revision.dart | 5 + lib/models/tables/file_entries.drift | 2 + lib/models/tables/file_revisions.drift | 1 + .../components/drive_detail_data_list.dart | 7 + .../components/drive_explorer_item_tile.dart | 36 +- lib/pages/drive_detail/drive_detail_page.dart | 137 +++-- .../lib/src/ardrive_uploader.dart | 27 +- .../lib/src/arfs_upload_metadata.dart | 5 +- .../lib/src/data_bundler.dart | 35 +- .../ardrive_uploader/lib/src/exceptions.dart | 12 + .../lib/src/metadata_generator.dart | 29 +- .../lib/src/upload_controller.dart | 81 +++ .../lib/src/upload_strategy.dart | 91 ++- packages/ardrive_utils/lib/ardrive_utils.dart | 1 + .../lib/src/generate_thumbnail.dart | 11 + packages/ardrive_utils/pubspec.yaml | 1 + packages/pst/pubspec.lock | 34 +- pubspec.lock | 4 +- 27 files changed, 1192 insertions(+), 146 deletions(-) create mode 100644 lib/dev_tools/drives_health_check.dart create mode 100644 lib/dev_tools/thumbnail_generator_poc.dart create mode 100644 packages/ardrive_utils/lib/src/generate_thumbnail.dart diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index ba56b66764..dc55262d8b 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -819,6 +819,7 @@ class UploadCubit extends Cubit { name: fileMetadata.name, parentFolderId: fileMetadata.parentFolderId, size: fileMetadata.size, + thumbnailTxId: fileMetadata.thumbnailTxId, // TODO: pinnedDataOwnerAddress ); diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index 083a6446e7..9d49d7fd79 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -281,6 +281,8 @@ class _AppSideBarState extends State { } Widget _buildAccordion(DrivesLoadSuccess state, bool isMobile) { + final typography = ArDriveTypographyNew.of(context); + return ArDriveAccordion( contentPadding: isMobile ? const EdgeInsets.all(4) : null, key: ValueKey(state.userDrives.map((e) => e.name)), @@ -291,9 +293,9 @@ class _AppSideBarState extends State { isExpanded: true, Text( appLocalizationsOf(context).publicDrives, - style: ArDriveTypography.body.buttonLargeBold().copyWith( - fontWeight: FontWeight.w700, - ), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + ), ), state.userDrives .where((element) => element.isPublic) @@ -319,9 +321,9 @@ class _AppSideBarState extends State { isExpanded: true, Text( appLocalizationsOf(context).privateDrives, - style: ArDriveTypography.body - .buttonLargeBold() - .copyWith(fontWeight: FontWeight.w700), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + ), ), state.userDrives .where((element) => element.isPrivate) @@ -342,9 +344,9 @@ class _AppSideBarState extends State { isExpanded: true, Text( appLocalizationsOf(context).sharedDrives, - style: ArDriveTypography.body - .buttonLargeBold() - .copyWith(fontWeight: FontWeight.w700), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + ), ), state.sharedDrives .map( @@ -675,6 +677,8 @@ class DriveListTile extends StatelessWidget { @override Widget build(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + return GestureDetector( key: key, onTap: onTap, @@ -693,19 +697,15 @@ class DriveListTile extends StatelessWidget { child: Text( drive.name, style: isSelected - ? ArDriveTypography.body - .buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ) - .copyWith(fontWeight: FontWeight.w700) - : ArDriveTypography.body.buttonNormalRegular( + ? typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ) + : typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, color: ArDriveTheme.of(context) .themeData - .colors - .themeAccentDisabled, + .colorTokens + .textLow, ), ), ), diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 07aad1342a..3fa036a68b 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -955,6 +955,9 @@ class _UploadFormState extends State { case UploadStatus.creatingBundle: status = 'We are preparing your upload. Preparation step 2/2'; + case UploadStatus.uploadingThumbnail: + status = 'Uploading thumbnail!!!!! WOOOO-HOOOO'; + break; } final statusAvailableForShowingProgress = diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index d00b26e2ee..2e9da08426 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -1,6 +1,7 @@ import 'dart:convert'; -import 'package:ardrive/dev_tools/pick_image_thumbnail_poc.dart'; +import 'package:ardrive/dev_tools/drives_health_check.dart'; +import 'package:ardrive/dev_tools/thumbnail_generator_poc.dart'; import 'package:ardrive/main.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/services/config/config.dart'; @@ -276,6 +277,20 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.button, ); + final ArDriveDevToolOption runHealthCheck = ArDriveDevToolOption( + name: 'Run Health Check', + value: '', + onChange: (value) { + final BuildContext context = ArDriveDevTools().context!; + + showArDriveDialog( + context, + content: const DrivesHealthCheckModal(), + ); + }, + type: ArDriveDevToolOptionType.button, + ); + final ArDriveDevToolOption resetOptions = ArDriveDevToolOption( name: 'Reset options', value: '', @@ -333,19 +348,53 @@ class AppConfigWindowManagerState extends State { final ArDriveDevToolOption pickImageAndGenerateThumbnailItem = ArDriveDevToolOption( - name: 'setDefaultDataOnPaymentForm', + name: 'Generate Thumbnails', value: '', onChange: (value) {}, - onInteraction: () { + onInteraction: () async { try { - pickImageAndGenerateThumbnail( - onThumbnailGenerated: (thumbnail) { - showArDriveDialog(context, - content: ArDriveStandardModal( - content: Image.memory(thumbnail), - )); - }, - ); + final BuildContext context = ArDriveDevTools().context!; + + showArDriveDialog(context, + content: + const ArDriveStandardModal(content: ThumbnailGeneratorPOC())); + + // pickImageAndGenerateThumbnail( + // onThumbnailGenerated: (thumbnail) async { + // final uploader = ArDriveUploader( + // turboUploadUri: Uri.parse(context + // .read() + // .config + // .defaultTurboUploadUrl!)); + + // final file = await IOFileAdapter().fromData(thumbnail, + // name: 'thumbnail', lastModifiedDate: DateTime.now()); + // final thumbnailArgs = ThumbnailMetadataArgs( + // contentType: 'image/png', + // height: 100, + // width: 100, + // thumbnailSize: thumbnail.length, + // relatesTo: 'a92e1181-f2b5-41b7-acc9-fac06128e647', + // ); + + // final controller = await uploader.uploadThumbnail( + // args: thumbnailArgs, + // file: file, + // type: UploadType.turbo, + // // ignore: use_build_context_synchronously + // wallet: context.read().currentUser.wallet, + // ); + + // controller.onDone((task) { + // logger.i('Thumbnail uploaded'); + // }); + + // showArDriveDialog(context, + // content: ArDriveStandardModal( + // content: Image.memory(thumbnail), + // )); + // }, + // ); } catch (e) { logger.e('Error setting default data on payment form', e); } @@ -397,9 +446,9 @@ class AppConfigWindowManagerState extends State { ); final List options = [ + runHealthCheck, useTurboOption, useTurboPaymentOption, - defaultTurboPaymentUrlOption, enableSyncFromSnapshotOption, pickImageAndGenerateThumbnailItem, stripePublishableKey, diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart new file mode 100644 index 0000000000..bcbb2cf722 --- /dev/null +++ b/lib/dev_tools/drives_health_check.dart @@ -0,0 +1,527 @@ +import 'dart:async'; + +import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; +import 'package:ardrive/models/database/database.dart'; +import 'package:ardrive/services/arweave/arweave.dart'; +import 'package:ardrive/utils/logger.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:http/http.dart' as http; + +class DrivesHealthCheckModal extends StatefulWidget { + const DrivesHealthCheckModal({super.key}); + + @override + State createState() => _DrivesHealthCheckModalState(); +} + +class _DrivesHealthCheckModalState extends State { + List statuses = []; + List driveStatuses = []; + List drives = []; + int numberOfFiles = 0; + + @override + initState() { + super.initState(); + + final driveDao = context.read(); + + driveDao.select(driveDao.drives).get().then((drives) { + setState(() { + this.drives = drives; + }); + + processDrivesInSequency(); + }); + } + + Future processDrivesInSequency() async { + final driveDao = context.read(); + + for (final drive in drives) { + final status = DriveHealthCheckStatus( + drive: drive, + files: [], + totalFiles: 0, + ); + + driveStatuses.add(status); + + setState(() {}); + } + + for (final currentStatus in driveStatuses) { + final files = await (driveDao.select(driveDao.fileEntries) + ..where((tbl) => tbl.driveId.equals(currentStatus.drive.id))) + .get(); + if (files.isEmpty) { + currentStatus.isLoading = false; + + setState(() {}); + + continue; + } + + currentStatus.totalFiles = files.length; + + selectedDriveStatus = currentStatus; + setState(() {}); + + await processFiles(files, currentStatus); + + currentStatus.isLoading = false; + + setState(() {}); + } + } + + late DriveHealthCheckStatus selectedDriveStatus; + + @override + Widget build(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + + if (driveStatuses.isNotEmpty) { + return SizedBox( + child: ArDriveModalNew( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.9, + maxWidth: MediaQuery.of(context).size.width * 0.8, + minHeight: MediaQuery.of(context).size.height * 0.9, + ), + content: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 2, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Drives', + style: typography.heading4( + fontWeight: ArFontWeight.bold, + )), + Text('Click on a drive to view details', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + )), + const SizedBox( + height: 8, + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.72, + child: ListView.separated( + itemCount: driveStatuses.length, + addAutomaticKeepAlives: true, + separatorBuilder: (context, index) => + const Divider(), + shrinkWrap: true, + itemBuilder: (context, index) { + final driveStatus = driveStatuses[index]; + + return ArDriveClickArea( + child: GestureDetector( + onTap: () { + setState(() { + selectedDriveStatus = driveStatus; + }); + }, + child: DriveHealthCheckTile( + status: driveStatus, + key: Key(driveStatus.drive.id), + isSelected: + selectedDriveStatus.drive.id == + driveStatus.drive.id, + ), + ), + ); + }), + ), + ], + ), + ), + ), + const SizedBox(width: 20), + Flexible( + flex: 1, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8, + ), + Text( + 'Drive: ${selectedDriveStatus.drive.name}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + Text('Success Files', + style: typography.paragraphLarge()), + const SizedBox( + height: 8, + ), + Flexible( + flex: 1, + child: ArDriveCard( + content: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * 0.32, + ), + child: Builder(builder: (context) { + final successFiles = selectedDriveStatus.files + .where((element) => element.isSuccess) + .toList(); + if (successFiles.isEmpty) { + return const Center( + child: Text('No files found'), + ); + } + + return ListView.builder( + itemCount: successFiles.length, + addAutomaticKeepAlives: true, + shrinkWrap: true, + itemBuilder: (context, index) { + final status = successFiles[index]; + + return FileHealthCheckTile( + status: status, + onFinish: () async {}, + ); + }); + }), + ), + ), + ), + const SizedBox( + height: 20, + ), + Text('Failed Files', + style: typography.paragraphLarge()), + const SizedBox( + height: 8, + ), + Flexible( + flex: 1, + child: ArDriveCard( + content: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * 0.32, + ), + child: Builder(builder: (context) { + final failedFiles = selectedDriveStatus.files + .where((element) => element.isFailed) + .toList(); + if (failedFiles.isEmpty) { + return const Center( + child: Text('No files found'), + ); + } + return ListView.builder( + itemCount: failedFiles.length, + addAutomaticKeepAlives: true, + shrinkWrap: true, + itemBuilder: (context, index) { + final status = failedFiles[index]; + + return FileHealthCheckTile( + status: status, + onFinish: () async {}, + ); + }); + }), + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + Text( + 'Drives Loaded: ${driveStatuses.where((element) => !element.isLoading).length} of ${driveStatuses.length}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + ], + ), + ), + ); + } + + return const Center( + child: CircularProgressIndicator(), + ); + } + + Future processFiles( + List files, DriveHealthCheckStatus driveStatus) async { + const int maxConcurrentTasks = 20; + final StreamController controller = StreamController(); + + // Function to process files + void processNext() { + if (files.isNotEmpty) { + final file = files.removeAt(0); + checkHealth(file, driveStatus).then((_) { + controller.add(null); + setState(() {}); + }); + } else { + controller.close(); + } + } + + // Start initial batch of downloads + for (int i = 0; i < maxConcurrentTasks && files.isNotEmpty; i++) { + processNext(); + setState(() {}); + } + + // Listen for completion events and process next file + await for (final _ in controller.stream) { + processNext(); + setState(() {}); + } + } + + /// checks the health of the file + Future checkHealth( + FileEntry file, DriveHealthCheckStatus driveStatus) async { + try { + final arweave = context.read(); + + final url = + '${arweave.client.api.gatewayUrl.origin}/raw/${file.dataTxId}'; + + final response = await http.head(Uri.parse(url)); + + logger.d( + 'Checking health of ${file.name}. Response: ${response.statusCode}'); + + if (response.statusCode > 400) { + driveStatus.files.add(FileHealthCheckStatus( + file: file, + isSuccess: false, + isFailed: true, + )); + setState(() {}); + return; + } + + driveStatus.files.add(FileHealthCheckStatus( + file: file, + isSuccess: true, + isFailed: false, + )); + + setState(() {}); + } catch (e) { + driveStatus.files.add(FileHealthCheckStatus( + file: file, + isSuccess: false, + isFailed: true, + )); + } + } +} + +class DriveHealthCheckTile extends StatefulWidget { + const DriveHealthCheckTile( + {super.key, required this.status, this.isSelected = false}); + + final DriveHealthCheckStatus status; + final bool isSelected; + + @override + State createState() => _DriveHealthCheckTielState(); +} + +class _DriveHealthCheckTielState extends State { + @override + Widget build(BuildContext context) { + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + final failedFiles = + widget.status.files.where((element) => element.isFailed).toList(); + final progress = widget.status.totalFiles == 0 && widget.status.isLoading + ? 0.0 + : widget.status.totalFiles == 0 && !widget.status.isLoading + ? 1.0 + : widget.status.files.length / widget.status.totalFiles; + return ArDriveCard( + content: Column( + children: [ + Row( + children: [ + Text( + widget.status.drive.name, + style: ArDriveTypographyNew.of(context).paragraphLarge( + fontWeight: ArFontWeight.bold, + color: failedFiles.isNotEmpty ? colorTokens.strokeRed : null, + ), + ), + const Spacer(), + if (failedFiles.isNotEmpty) ...[ + ArDriveIcons.triangle(size: 20, color: colorTokens.strokeRed), + const SizedBox(width: 2), + Text(failedFiles.length.toString(), + style: ArDriveTypographyNew.of(context).paragraphLarge( + fontWeight: ArFontWeight.bold, + color: colorTokens.strokeRed, + )), + const SizedBox(width: 8), + ], + Text( + '${widget.status.files.length}/${widget.status.totalFiles}', + style: ArDriveTypographyNew.of(context).paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + ], + ), + const SizedBox( + height: 8, + ), + ArDriveProgressBar( + percentage: progress, + indicatorColor: progress == 1 + ? failedFiles.isNotEmpty + ? colorTokens.strokeRed + : ArDriveTheme.of(context) + .themeData + .colors + .themeSuccessDefault + : colorTokens.textHigh, + backgroundColor: colorTokens.textLow, + ), + ], + ), + ); + } +} + +class DriveHealthCheckStatus { + final Drive drive; + final List files; + int totalFiles; + bool isLoading; + + DriveHealthCheckStatus({ + required this.drive, + required this.files, + required this.totalFiles, + this.isLoading = true, + }); +} + +class FileHealthCheckStatus { + final FileEntry file; + final bool isSuccess; + final bool isFailed; + + FileHealthCheckStatus({ + required this.file, + required this.isSuccess, + required this.isFailed, + }); +} + +class FileHealthCheckTile extends StatefulWidget { + const FileHealthCheckTile( + {super.key, required this.status, required this.onFinish}); + + final Function onFinish; + final FileHealthCheckStatus status; + + @override + State createState() => _FileHealthCheckTileState(); +} + +class _FileHealthCheckTileState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + if (widget.status.isSuccess) { + return ListTile( + title: Text(widget.status.file.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + color: colorTokens.textHigh, + )), + subtitle: Text( + 'Health check completed', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textMid, + ), + ), + trailing: IconButton( + color: colorTokens.textHigh, + icon: const Icon( + Icons.check, + ), + onPressed: () {}, + ), + ); + } + + if (widget.status.isFailed) { + return ListTile( + title: Text( + widget.status.file.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + color: colorTokens.textHigh, + ), + ), + subtitle: Text( + 'Health check failed', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textLow, + ), + ), + trailing: IconButton( + icon: Icon( + Icons.close, + color: colorTokens.textHigh, + ), + onPressed: () { + widget.onFinish(); + }, + ), + ); + } + + return ListTile( + title: Text(widget.status.file.name, style: typography.paragraphNormal()), + subtitle: Text( + 'Checking health of ${widget.status.file.name}', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + color: colorTokens.buttonDisabled, + ), + ), + ); + } +} diff --git a/lib/dev_tools/thumbnail_generator_poc.dart b/lib/dev_tools/thumbnail_generator_poc.dart new file mode 100644 index 0000000000..77c6e1e22c --- /dev/null +++ b/lib/dev_tools/thumbnail_generator_poc.dart @@ -0,0 +1,169 @@ +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/dev_tools/pick_image_thumbnail_poc.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ardrive/services/arweave/arweave.dart'; +import 'package:ardrive/services/config/config_service.dart'; +import 'package:ardrive/turbo/services/upload_service.dart'; +import 'package:ardrive/utils/file_type_helper.dart'; +import 'package:ardrive/utils/logger.dart'; +import 'package:ardrive_http/ardrive_http.dart'; +import 'package:ardrive_io/ardrive_io.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ardrive_uploader/ardrive_uploader.dart'; +import 'package:drift/drift.dart' as drift; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ThumbnailGeneratorPOC extends StatefulWidget { + const ThumbnailGeneratorPOC({super.key}); + + @override + State createState() => _ThumbnailGeneratorPOCState(); +} + +class _ThumbnailGeneratorPOCState extends State { + List? _files; + final Map _thumbnailsGenerated = {}; + + // load files + Future _generateThumbnailsForFiles({required String driveId}) async { + final driveDao = context.read(); + // final currentDriveId = + // (context.read().state as DriveDetailLoadSuccess) + // .currentDrive + // .id; + + final files = await (driveDao.select(driveDao.fileEntries) + ..where((tbl) => tbl.driveId.equals(driveId))) + .get(); + + setState(() { + _files = files; + }); + // ignore: use_build_context_synchronously + final arweaveService = context.read(); + final turboUploadService = context.read(); + final wallet = context.read().currentUser.wallet; + + for (var f in files) { + if (FileTypeHelper.isImage(f.dataContentType ?? '') == false) { + logger.i('Skipping file'); + continue; + } + + final realImageUrl = + '${arweaveService.client.api.gatewayUrl.origin}/raw/${f.dataTxId}'; + + final ardriveHttp = ArDriveHTTP(); + + final bytes = await ardriveHttp.getAsBytes(realImageUrl); + + final uploader = ArDriveUploader(turboUploadUri: Uri.parse( + // ignore: use_build_context_synchronously + context.read().config.defaultTurboUploadUrl!)); + + final data = generateThumbnail(bytes.data); + + final file = await IOFileAdapter() + .fromData(data, name: 'thumbnail', lastModifiedDate: DateTime.now()); + final thumbnailArgs = ThumbnailMetadataArgs( + contentType: 'image/png', + height: 100, + width: 100, + thumbnailSize: data.length, + relatesTo: f.dataTxId, + ); + + final controller = await uploader.uploadThumbnail( + args: thumbnailArgs, + file: file, + type: UploadType.turbo, + // ignore: use_build_context_synchronously + wallet: context.read().currentUser.wallet, + ); + + controller.onDone((tasks) async { + logger.i('Thumbnail uploaded'); + + setState(() { + _thumbnailsGenerated[f.dataTxId] = true; + }); + + await driveDao.transaction(() async { + f = f.copyWith( + lastUpdated: DateTime.now(), + thumbnailTxId: drift.Value( + (tasks.first as ThumbnailUploadTask).uploadItem!.data.id), + ); + + final fileEntity = f.asEntity(); + + if (turboUploadService.useTurboUpload) { + final fileDataItem = await arweaveService.prepareEntityDataItem( + fileEntity, + wallet, + // key: fileKey, + ); + + await turboUploadService.postDataItem( + dataItem: fileDataItem, + wallet: wallet, + ); + fileEntity.txId = fileDataItem.id; + } else { + // final fileTx = await arweaveService.prepareEntityTx( + // fileEntity, wallet, fileKey); + + // await _arweave.postTx(fileTx); + // fileEntity.txId = fileTx.id; + } + + logger.i( + 'Updating file ${f.id} with txId ${fileEntity.txId}. Data content type: ${fileEntity.dataContentType}'); + + await driveDao.writeToFile(f); + + await driveDao.insertFileRevision(fileEntity.toRevisionCompanion( + performedAction: RevisionAction.rename)); + }); + }); + } + } + + final textController = TextEditingController(); + + @override + Widget build(BuildContext context) { + if (_files == null) { + return Center( + child: Column( + children: [ + ArDriveTextFieldNew( + controller: textController, + ), + ElevatedButton( + onPressed: () => + _generateThumbnailsForFiles(driveId: textController.text), + child: const Text('Generate Thumbnails'), + ), + ], + ), + ); + } else { + return ListView.builder( + itemCount: _files!.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final file = _files![index]; + return ListTile( + title: Text(file.name), + subtitle: Text(file.dataTxId), + trailing: _thumbnailsGenerated[file.dataTxId] == true + ? const Icon(Icons.check) + : const Icon(Icons.close), + ); + }, + ); + } + } +} diff --git a/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart b/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart index e167da2ad3..c474d0eefb 100644 --- a/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart +++ b/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart @@ -39,7 +39,7 @@ class ThumbnailRepository { } final urlString = - '${_arweaveService.client.api.gatewayUrl.origin}/raw/${fileDataTableItem.dataTxId}'; + '${_arweaveService.client.api.gatewayUrl.origin}/raw/${fileDataTableItem.thumbnailUrl}'; return Thumbnail(data: null, url: urlString); } diff --git a/lib/entities/file_entity.dart b/lib/entities/file_entity.dart index 415b561492..53cae59726 100644 --- a/lib/entities/file_entity.dart +++ b/lib/entities/file_entity.dart @@ -38,6 +38,7 @@ class FileEntity extends EntityWithCustomMetadata { String? pinnedDataOwnerAddress; @JsonKey(includeIfNull: false) bool? isHidden; + String? thumbnailTxId; @override @JsonKey(includeFromJson: false, includeToJson: false) @@ -71,6 +72,7 @@ class FileEntity extends EntityWithCustomMetadata { this.dataContentType, this.pinnedDataOwnerAddress, this.isHidden, + this.thumbnailTxId, }) : super(ArDriveCrypto()); FileEntity.withUserProvidedDetails({ @@ -113,6 +115,7 @@ class FileEntity extends EntityWithCustomMetadata { ..bundledIn = transaction.bundledIn?.id ..createdAt = commitTime; + final tags = transaction.tags .map( (t) => Tag.fromJson(t.toJson()), diff --git a/lib/models/file_entry.dart b/lib/models/file_entry.dart index 0895400e46..5abba50372 100644 --- a/lib/models/file_entry.dart +++ b/lib/models/file_entry.dart @@ -17,6 +17,7 @@ extension FileEntryExtensions on FileEntry { dataContentType: dataContentType, pinnedDataOwnerAddress: pinnedDataOwnerAddress, isHidden: isHidden, + thumbnailTxId: thumbnailTxId, ); file.customJsonMetadata = parseCustomJsonMetadata(customJsonMetadata); diff --git a/lib/models/file_revision.dart b/lib/models/file_revision.dart index 0f64df08d5..7ce685b2ea 100644 --- a/lib/models/file_revision.dart +++ b/lib/models/file_revision.dart @@ -25,6 +25,7 @@ extension FileRevisionsCompanionExtensions on FileRevisionsCompanion { isHidden: isHidden, // TODO: path is not used in the app, so it's not necessary to set it path: '', + thumbnailTxId: Value(thumbnailTxId.value), ); /// Returns a list of [NetworkTransactionsCompanion] representing the metadata and data transactions @@ -62,6 +63,7 @@ extension FileEntityExtensions on FileEntity { customJsonMetadata: Value(customJsonMetadataAsString), pinnedDataOwnerAddress: Value(pinnedDataOwnerAddress), isHidden: Value(isHidden ?? false), + thumbnailTxId: Value(thumbnailTxId), ); FileRevision toRevision({ @@ -85,6 +87,7 @@ extension FileEntityExtensions on FileEntity { customJsonMetadata: customJsonMetadataAsString, pinnedDataOwnerAddress: pinnedDataOwnerAddress, isHidden: isHidden ?? false, + thumbnailTxId: thumbnailTxId, ); /// Returns the action performed on the file that lead to the new revision. @@ -104,6 +107,8 @@ extension FileEntityExtensions on FileEntity { return RevisionAction.hide; } else if (isHidden == false && previousRevision.isHidden.value == true) { return RevisionAction.unhide; + } else if (thumbnailTxId != previousRevision.thumbnailTxId.value) { + return RevisionAction.rename; } return null; diff --git a/lib/models/tables/file_entries.drift b/lib/models/tables/file_entries.drift index 9dde437fdd..09a61b1295 100644 --- a/lib/models/tables/file_entries.drift +++ b/lib/models/tables/file_entries.drift @@ -16,6 +16,8 @@ CREATE TABLE file_entries ( bundledIn TEXT, + thumbnailTxId TEXT, + pinnedDataOwnerAddress TEXT, customJsonMetadata TEXT, diff --git a/lib/models/tables/file_revisions.drift b/lib/models/tables/file_revisions.drift index df13ec3dd1..36818b96c2 100644 --- a/lib/models/tables/file_revisions.drift +++ b/lib/models/tables/file_revisions.drift @@ -15,6 +15,7 @@ CREATE TABLE file_revisions ( dataTxId TEXT NOT NULL, licenseTxId TEXT, + thumbnailTxId TEXT, bundledIn TEXT, diff --git a/lib/pages/drive_detail/components/drive_detail_data_list.dart b/lib/pages/drive_detail/components/drive_detail_data_list.dart index 5b9f7b8d67..18902f6d43 100644 --- a/lib/pages/drive_detail/components/drive_detail_data_list.dart +++ b/lib/pages/drive_detail/components/drive_detail_data_list.dart @@ -97,6 +97,7 @@ class FileDataTableItem extends ArDriveDataTableItem { final NetworkTransaction? metadataTx; final NetworkTransaction? dataTx; final String? pinnedDataOwnerAddress; + final String? thumbnailUrl; FileDataTableItem( {required super.driveId, @@ -116,6 +117,7 @@ class FileDataTableItem extends ArDriveDataTableItem { required this.metadataTx, required this.dataTx, required this.pinnedDataOwnerAddress, + this.thumbnailUrl, super.licenseType, this.licenseTxId, this.bundledIn}) @@ -272,8 +274,10 @@ Widget _buildDataListContent( return folders + files; }, buildRow: (row) { + final typography = ArDriveTypographyNew.of(context); return DriveExplorerItemTile( name: row.name, + typography: typography, size: row.size == null ? '-' : filesize(row.size), lastUpdated: yMMdDateFormatter.format(row.lastUpdated), dateCreated: yMMdDateFormatter.format(row.dateCreated), @@ -404,6 +408,7 @@ class DriveDataTableItemMapper { index: index, pinnedDataOwnerAddress: file.pinnedDataOwnerAddress, isHidden: file.isHidden, + thumbnailUrl: file.thumbnailTxId, ); } @@ -430,6 +435,7 @@ class DriveDataTableItemMapper { index: 0, pinnedDataOwnerAddress: fileEntry.pinnedDataOwnerAddress, isHidden: fileEntry.isHidden, + thumbnailUrl: fileEntry.thumbnailTxId, ); } @@ -494,6 +500,7 @@ class DriveDataTableItemMapper { index: 0, pinnedDataOwnerAddress: revision.pinnedDataOwnerAddress, isHidden: revision.isHidden, + thumbnailUrl: revision.thumbnailTxId, ); } } diff --git a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart index 5ec38f4cae..50aead38e3 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -34,32 +34,39 @@ class DriveExplorerItemTile extends TableRowWidget { required String license, required Function() onPressed, required bool isHidden, + required ArdriveTypographyNew typography, }) : super( [ Padding( padding: const EdgeInsets.only(right: 8), child: Text( name, - style: ArDriveTypography.body.buttonNormalBold().copyWith( - color: isHidden ? Colors.grey : null, - ), + style: typography.paragraphNormal( + color: isHidden ? Colors.grey : null, + fontWeight: ArFontWeight.bold, + ), overflow: TextOverflow.fade, maxLines: 1, softWrap: false, ), ), - Text(size, style: _driveExplorerItemTileTextStyle(isHidden)), - Text(lastUpdated, style: _driveExplorerItemTileTextStyle(isHidden)), - Text(dateCreated, style: _driveExplorerItemTileTextStyle(isHidden)), + Text(size, + style: _driveExplorerItemTileTextStyle(isHidden, typography)), + Text(lastUpdated, + style: _driveExplorerItemTileTextStyle(isHidden, typography)), + Text(dateCreated, + style: _driveExplorerItemTileTextStyle(isHidden, typography)), Text(license, style: ArDriveTypography.body.captionRegular()), ], ); } -TextStyle _driveExplorerItemTileTextStyle(bool isHidden) => - ArDriveTypography.body - .captionRegular() - .copyWith(color: isHidden ? Colors.grey : null); +TextStyle _driveExplorerItemTileTextStyle( + bool isHidden, ArdriveTypographyNew typography) => + typography.paragraphNormal( + color: isHidden ? Colors.grey : null, + fontWeight: ArFontWeight.bold, + ); class DriveExplorerItemTileLeading extends StatelessWidget { const DriveExplorerItemTileLeading({ @@ -120,8 +127,15 @@ class DriveExplorerItemTileLeading extends StatelessWidget { state.thumbnail.url!, width: 30, height: 30, - filterQuality: FilterQuality.low, + filterQuality: FilterQuality.high, fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return getIconForContentType( + item.contentType, + ).copyWith( + color: isHidden ? Colors.grey : null, + ); + }, ), ); } diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 9af301be25..c3922f309e 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -16,10 +16,13 @@ import 'package:ardrive/components/details_panel.dart'; import 'package:ardrive/components/drive_detach_dialog.dart'; import 'package:ardrive/components/drive_rename_form.dart'; import 'package:ardrive/components/fs_entry_license_form.dart'; +import 'package:ardrive/components/keyboard_handler.dart'; import 'package:ardrive/components/new_button/new_button.dart'; import 'package:ardrive/components/prompt_to_snapshot_dialog.dart'; import 'package:ardrive/components/side_bar.dart'; import 'package:ardrive/core/activity_tracker.dart'; +import 'package:ardrive/dev_tools/app_dev_tools.dart'; +import 'package:ardrive/dev_tools/shortcut_handler.dart'; import 'package:ardrive/download/multiple_file_download_modal.dart'; import 'package:ardrive/entities/entities.dart' as entities; import 'package:ardrive/l11n/l11n.dart'; @@ -146,30 +149,25 @@ class _DriveDetailPageState extends State { if (driveDetailState is DriveDetailLoadInProgress) { return const Center(child: CircularProgressIndicator()); } else if (driveDetailState is DriveInitialLoading) { - return ScreenTypeLayout.builder( - mobile: (context) { - return Scaffold( - drawerScrimColor: Colors.transparent, - drawer: const AppSideBar(), - appBar: const MobileAppBar(), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Text( - appLocalizationsOf(context) - .driveDoingInitialSetupMessage, - style: ArDriveTypography.body.buttonLargeBold(), - ), - ), - ), - ); - }, - desktop: (context) => Scaffold( - drawerScrimColor: Colors.transparent, - body: Column( - children: [ - const AppTopBar(), - Expanded( + return ArDriveDevToolsShortcuts( + customShortcuts: [ + Shortcut( + modifier: LogicalKeyboardKey.shiftLeft, + key: LogicalKeyboardKey.keyH, + action: () { + ArDriveDevTools.instance + .showDevTools(optionalContext: context); + }, + ), + ], + child: ScreenTypeLayout.builder( + mobile: (context) { + return Scaffold( + drawerScrimColor: Colors.transparent, + drawer: const AppSideBar(), + appBar: const MobileAppBar(), + body: Padding( + padding: const EdgeInsets.all(8.0), child: Center( child: Text( appLocalizationsOf(context) @@ -178,7 +176,24 @@ class _DriveDetailPageState extends State { ), ), ), - ], + ); + }, + desktop: (context) => Scaffold( + drawerScrimColor: Colors.transparent, + body: Column( + children: [ + const AppTopBar(), + Expanded( + child: Center( + child: Text( + appLocalizationsOf(context) + .driveDoingInitialSetupMessage, + style: ArDriveTypography.body.buttonLargeBold(), + ), + ), + ), + ], + ), ), ), ); @@ -209,36 +224,48 @@ class _DriveDetailPageState extends State { final canDownloadMultipleFiles = driveDetailState.multiselect && context.read().selectedItems.isNotEmpty; - return ScreenTypeLayout.builder( - desktop: (context) => _desktopView( - isDriveOwner: isOwner, - driveDetailState: driveDetailState, - hasSubfolders: hasSubfolders, - hasFiles: hasFiles, - canDownloadMultipleFiles: canDownloadMultipleFiles, - ), - mobile: (context) => Scaffold( - resizeToAvoidBottomInset: false, - drawerScrimColor: Colors.transparent, - drawer: const AppSideBar(), - appBar: (driveDetailState.showSelectedItemDetails && - context.read().selectedItem != - null) - ? MobileAppBar( - leading: ArDriveIconButton( - icon: ArDriveIcons.arrowLeft(), - onPressed: () { - context - .read() - .toggleSelectedItemDetails(); - }, - ), - ) - : null, - body: _mobileView( - driveDetailState, - hasSubfolders, - hasFiles, + return ArDriveDevToolsShortcuts( + customShortcuts: [ + Shortcut( + modifier: LogicalKeyboardKey.shiftLeft, + key: LogicalKeyboardKey.keyH, + action: () { + ArDriveDevTools.instance + .showDevTools(optionalContext: context); + }, + ), + ], + child: ScreenTypeLayout.builder( + desktop: (context) => _desktopView( + isDriveOwner: isOwner, + driveDetailState: driveDetailState, + hasSubfolders: hasSubfolders, + hasFiles: hasFiles, + canDownloadMultipleFiles: canDownloadMultipleFiles, + ), + mobile: (context) => Scaffold( + resizeToAvoidBottomInset: false, + drawerScrimColor: Colors.transparent, + drawer: const AppSideBar(), + appBar: (driveDetailState.showSelectedItemDetails && + context.read().selectedItem != + null) + ? MobileAppBar( + leading: ArDriveIconButton( + icon: ArDriveIcons.arrowLeft(), + onPressed: () { + context + .read() + .toggleSelectedItemDetails(); + }, + ), + ) + : null, + body: _mobileView( + driveDetailState, + hasSubfolders, + hasFiles, + ), ), ), ); diff --git a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart index 41df871ad8..ed28ddcbdb 100644 --- a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart +++ b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart @@ -125,6 +125,10 @@ class _ArDriveUploader implements ArDriveUploader { streamedUploadFactory: _streamedUploadFactory, ); + final thumbnailStrategy = UploadThumbnailStrategy( + streamedUploadFactory: _streamedUploadFactory, + dataBundler: dataBundler); + final uploadController = UploadController( StreamController(), UploadDispatcher( @@ -132,6 +136,7 @@ class _ArDriveUploader implements ArDriveUploader { uploadStrategy: _uploadFileStrategyFactory.createUploadStrategy( type: type, ), + uploadThumbnailStrategy: thumbnailStrategy, uploadFolderStrategy: uploadFolderStrategy, ), numOfWorkers: 1, @@ -183,6 +188,9 @@ class _ArDriveUploader implements ArDriveUploader { dataBundler: dataBundler, uploadStrategy: uploadFileStrategy, uploadFolderStrategy: uploadFolderStrategy, + uploadThumbnailStrategy: UploadThumbnailStrategy( + streamedUploadFactory: _streamedUploadFactory, + dataBundler: dataBundler), ); final uploadController = UploadController( @@ -245,6 +253,10 @@ class _ArDriveUploader implements ArDriveUploader { dataBundler: dataBundler, uploadStrategy: uploadStrategy, uploadFolderStrategy: uploadFolderStrategy, + uploadThumbnailStrategy: UploadThumbnailStrategy( + dataBundler: dataBundler, + streamedUploadFactory: _streamedUploadFactory, + ), ); final filesWitMetadatas = <(ARFSFileUploadMetadata, IOFile)>[]; @@ -309,11 +321,12 @@ class _ArDriveUploader implements ArDriveUploader { } @override - Future uploadThumbnail( - {required IOFile file, - required Wallet wallet, - required UploadType type, - required ThumbnailMetadataArgs args}) async { + Future uploadThumbnail({ + required IOFile file, + required Wallet wallet, + required UploadType type, + required ThumbnailMetadataArgs args, + }) async { final thumbnailMetadataGenerator = ThumbnailMetadataGenerator(); final thumbnailMetadata = await thumbnailMetadataGenerator.generateMetadata( @@ -337,6 +350,10 @@ class _ArDriveUploader implements ArDriveUploader { dataBundler: dataBundler, streamedUploadFactory: _streamedUploadFactory, ), + uploadThumbnailStrategy: UploadThumbnailStrategy( + streamedUploadFactory: _streamedUploadFactory, + dataBundler: _dataBundlerFactory.createDataBundler(type), + ), ), numOfWorkers: 1, maxTasksPerWorker: 1, diff --git a/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart b/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart index eb7cb786bf..6d48eb7a44 100644 --- a/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart +++ b/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart @@ -6,10 +6,11 @@ class ThumbnailUploadMetadata extends UploadMetadata { ThumbnailUploadMetadata({ required this.entityMetadataTags, required this.thumbnailSize, + required this.relatesTo, }); final List entityMetadataTags; - + final String relatesTo; final int thumbnailSize; } @@ -60,6 +61,7 @@ class ARFSFileUploadMetadata extends ARFSUploadMetadata { final String driveId; final String parentFolderId; final String? licenseDefinitionTxId; + String? thumbnailTxId; final Map? licenseAdditionalTags; ARFSFileUploadMetadata({ @@ -76,6 +78,7 @@ class ARFSFileUploadMetadata extends ARFSUploadMetadata { required super.isPrivate, required super.dataItemTags, required super.bundleTags, + this.thumbnailTxId, }); String? _dataTxId; diff --git a/packages/ardrive_uploader/lib/src/data_bundler.dart b/packages/ardrive_uploader/lib/src/data_bundler.dart index d2b3023e48..7b1e457760 100644 --- a/packages/ardrive_uploader/lib/src/data_bundler.dart +++ b/packages/ardrive_uploader/lib/src/data_bundler.dart @@ -53,7 +53,7 @@ abstract class DataBundler { SecretKey? driveKey, }); - Future createDataItemForThumbnail({ + Future createDataItemForThumbnail({ required IOFile file, required ThumbnailUploadMetadata metadata, required Wallet wallet, @@ -333,12 +333,12 @@ class DataTransactionBundler implements DataBundler { } @override - Future createDataItemForThumbnail( - {required IOFile file, - required ThumbnailUploadMetadata metadata, - required Wallet wallet, - SecretKey? driveKey}) { - // TODO: implement createDataItemForThumbnail + Future createDataItemForThumbnail({ + required IOFile file, + required ThumbnailUploadMetadata metadata, + required Wallet wallet, + SecretKey? driveKey, + }) async { throw UnimplementedError(); } } @@ -585,7 +585,7 @@ class BDIDataBundler implements DataBundler { } @override - Future createDataItemForThumbnail({ + Future createDataItemForThumbnail({ required IOFile file, required ThumbnailUploadMetadata metadata, required Wallet wallet, @@ -594,22 +594,25 @@ class BDIDataBundler implements DataBundler { final dataGenerator = await _dataGenerator( dataStream: file.openReadStream, fileLength: metadata.thumbnailSize, - - /// pass the file original file id - metadataId: '', + metadataId: metadata.relatesTo, wallet: wallet, encryptionKey: driveKey, ); - final thumbnailDataItem = DataItemFile( - dataSize: metadata.thumbnailSize, - streamGenerator: dataGenerator.$1, + final taskEither = await createDataItemTaskEither( + wallet: wallet, + dataStream: dataGenerator.$1, + dataStreamSize: metadata.thumbnailSize, tags: metadata.entityMetadataTags .map((e) => createTag(e.name, e.value)) .toList(), - ); + ).run(); - return thumbnailDataItem; + return taskEither.match((l) { + throw l; + }, (r) { + return r; + }); } } diff --git a/packages/ardrive_uploader/lib/src/exceptions.dart b/packages/ardrive_uploader/lib/src/exceptions.dart index 479cdb3882..3078bed752 100644 --- a/packages/ardrive_uploader/lib/src/exceptions.dart +++ b/packages/ardrive_uploader/lib/src/exceptions.dart @@ -101,3 +101,15 @@ class UnknownNetworkException implements NetworkException { @override Object? error; } + +class ThumbnailUploadException implements UploadStrategyException { + ThumbnailUploadException({ + required this.message, + this.error, + }); + + @override + final String message; + @override + Object? error; +} diff --git a/packages/ardrive_uploader/lib/src/metadata_generator.dart b/packages/ardrive_uploader/lib/src/metadata_generator.dart index 97eee9e921..d1ddc84e98 100644 --- a/packages/ardrive_uploader/lib/src/metadata_generator.dart +++ b/packages/ardrive_uploader/lib/src/metadata_generator.dart @@ -483,23 +483,18 @@ class ThumbnailMetadataGenerator IOEntity entity, { required ThumbnailMetadataArgs arguments, }) async { - if (entity is IOFile) { - final file = entity; - - final tags = [ - Tag('Relates-To', file.contentType), - Tag(EntityTag.contentType, arguments.contentType), - Tag('width', arguments.width.toString()), - Tag('height', arguments.height.toString()), - ]; - - return ThumbnailUploadMetadata( - entityMetadataTags: tags, - thumbnailSize: arguments.thumbnailSize, - ); - } + final tags = [ + Tag('Relates-To', arguments.relatesTo), + Tag(EntityTag.contentType, arguments.contentType), + Tag('width', arguments.width.toString()), + Tag('height', arguments.height.toString()), + ]; - throw Exception('Invalid entity type'); + return ThumbnailUploadMetadata( + entityMetadataTags: tags, + thumbnailSize: arguments.thumbnailSize, + relatesTo: arguments.relatesTo, + ); } } @@ -508,11 +503,13 @@ class ThumbnailMetadataArgs { final int width; final String contentType; final int thumbnailSize; + final String relatesTo; ThumbnailMetadataArgs({ required this.contentType, required this.height, required this.width, required this.thumbnailSize, + required this.relatesTo, }); } diff --git a/packages/ardrive_uploader/lib/src/upload_controller.dart b/packages/ardrive_uploader/lib/src/upload_controller.dart index e6b8e20fc5..d3068d6c05 100644 --- a/packages/ardrive_uploader/lib/src/upload_controller.dart +++ b/packages/ardrive_uploader/lib/src/upload_controller.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_uploader/src/data_bundler.dart'; import 'package:ardrive_uploader/src/utils/logger.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:cryptography/cryptography.dart'; import 'package:rxdart/rxdart.dart'; @@ -489,6 +490,9 @@ enum UploadStatus { /// The upload has failed failed, + /// uploading thumbnail + uploadingThumbnail, + /// The upload has been canceled canceled, } @@ -934,14 +938,17 @@ class UploadTaskCancelToken { class UploadDispatcher { UploadFileStrategy _uploadFileStrategy; final UploadFolderStructureStrategy _uploadFolderStrategy; + final UploadThumbnailStrategy _uploadThumbnailStrategy; final DataBundler _dataBundler; UploadDispatcher({ required UploadFileStrategy uploadStrategy, required DataBundler dataBundler, required UploadFolderStructureStrategy uploadFolderStrategy, + required UploadThumbnailStrategy uploadThumbnailStrategy, }) : _dataBundler = dataBundler, _uploadFolderStrategy = uploadFolderStrategy, + _uploadThumbnailStrategy = uploadThumbnailStrategy, _uploadFileStrategy = uploadStrategy; Future send({ @@ -983,6 +990,73 @@ class UploadDispatcher { controller: controller, verifyCancel: verifyCancel, ); + + if (task.file.contentType == 'image/jpeg') { + var updatedTask = controller.tasks[task.id]!; + + controller.updateProgress( + task: updatedTask.copyWith( + status: UploadStatus.uploadingThumbnail, + ), + ); + + final data = generateThumbnail(await task.file.readAsBytes()); + + final thumbnailMetadata = ThumbnailUploadMetadata( + thumbnailSize: 0, + relatesTo: + (task.content!.first as ARFSFileUploadMetadata).dataTxId!, + entityMetadataTags: [], + ); + + final thumb = await IOFileAdapter().fromData( + data, + name: 'thumbnail', + lastModifiedDate: DateTime.now(), + contentType: 'image/jpeg', + ); + + final dataItem = await _dataBundler.createDataItemForThumbnail( + file: thumb, metadata: thumbnailMetadata, wallet: wallet); + + final thumbnailTask = ThumbnailUploadTask( + file: thumb, + metadata: thumbnailMetadata, + type: task.type, + uploadItem: DataItemUploadItem( + size: dataItem.dataItemSize, + data: dataItem, + ), + id: Uuid().v4(), + ); + + await _uploadThumbnailStrategy.upload( + task: thumbnailTask, + wallet: wallet, + controller: UploadController(StreamController(), this), + verifyCancel: verifyCancel, + ); + + updatedTask = controller.tasks[task.id]!; + + final uploadContent = task.content!.first as ARFSFileUploadMetadata; + + uploadContent.thumbnailTxId = + (thumbnailTask.uploadItem as DataItemUploadItem).data.id; + + controller.updateProgress( + task: updatedTask.copyWith( + status: UploadStatus.complete, content: [uploadContent]), + ); + } else { + controller.updateProgress( + task: task.copyWith( + status: UploadStatus.complete, + ), + ); + } + + return UploadResult(success: true); } else if (task is FolderUploadTask) { await _uploadFolderStrategy.upload( task: task, @@ -990,6 +1064,13 @@ class UploadDispatcher { controller: controller, verifyCancel: verifyCancel, ); + } else if (task is ThumbnailUploadTask) { + await _uploadThumbnailStrategy.upload( + task: task, + wallet: wallet, + controller: controller, + verifyCancel: verifyCancel, + ); } else { throw Exception('Invalid task type'); } diff --git a/packages/ardrive_uploader/lib/src/upload_strategy.dart b/packages/ardrive_uploader/lib/src/upload_strategy.dart index 0715a93cb1..b673524d19 100644 --- a/packages/ardrive_uploader/lib/src/upload_strategy.dart +++ b/packages/ardrive_uploader/lib/src/upload_strategy.dart @@ -17,6 +17,25 @@ abstract class UploadFileStrategy { }); } +abstract class UploadThumbnailStrategy { + Future upload({ + required ThumbnailUploadTask task, + required Wallet wallet, + required UploadController controller, + required bool Function() verifyCancel, + }); + + factory UploadThumbnailStrategy({ + required StreamedUploadFactory streamedUploadFactory, + required DataBundler dataBundler, + }) { + return _UploadThumbnailStrategy( + streamedUploadFactory: streamedUploadFactory, + dataBundler: dataBundler, + ); + } +} + abstract class UploadFolderStructureStrategy { Future upload({ required FolderUploadTask task, @@ -164,13 +183,13 @@ class UploadFileUsingDataItemFiles extends UploadFileStrategy { ); } - final updatedTask = controller.tasks[task.id]!; + // final updatedTask = controller.tasks[task.id]!; - controller.updateProgress( - task: updatedTask.copyWith( - status: UploadStatus.complete, - ), - ); + // controller.updateProgress( + // task: updatedTask.copyWith( + // status: UploadStatus.complete, + // ), + // ); } } @@ -357,3 +376,63 @@ class UploadFolderStructureAsBundleStrategy ); } } + +class _UploadThumbnailStrategy implements UploadThumbnailStrategy { + final StreamedUploadFactory _streamedUploadFactory; + final DataBundler _dataBundler; + + _UploadThumbnailStrategy({ + required StreamedUploadFactory streamedUploadFactory, + required DataBundler dataBundler, + }) : _streamedUploadFactory = streamedUploadFactory, + _dataBundler = dataBundler; + + @override + Future upload({ + required ThumbnailUploadTask task, + required Wallet wallet, + required UploadController controller, + required bool Function() verifyCancel, + }) async { + final dataItem = await _dataBundler.createDataItemForThumbnail( + wallet: wallet, + file: task.file, + metadata: task.metadata, + driveKey: task.encryptionKey, + ); + + task = task.copyWith( + uploadItem: DataItemUploadItem( + size: dataItem.dataItemSize, + data: dataItem, + ), + ); + + final streamedUpload = _streamedUploadFactory.fromUploadType(task.type); + + final result = await streamedUpload.send( + task.uploadItem!, + wallet, + (progress) { + controller.updateProgress( + task: task.copyWith( + progress: progress, + ), + ); + }, + ); + + if (!result.success) { + throw ThumbnailUploadException( + message: 'Failed to upload thumbnail', + error: result.error, + ); + } + + controller.updateProgress( + task: task.copyWith( + status: UploadStatus.complete, + ), + ); + } +} diff --git a/packages/ardrive_utils/lib/ardrive_utils.dart b/packages/ardrive_utils/lib/ardrive_utils.dart index a1e7a4cfca..020ebe417a 100644 --- a/packages/ardrive_utils/lib/ardrive_utils.dart +++ b/packages/ardrive_utils/lib/ardrive_utils.dart @@ -6,6 +6,7 @@ export 'src/app_platform.dart'; export 'src/base2_size.dart'; export 'src/convert_to_usd.dart'; export 'src/entity_tag.dart'; +export 'src/generate_thumbnail.dart'; export 'src/get_first_future_result.dart'; export 'src/html/html.dart'; export 'src/license_tag.dart'; diff --git a/packages/ardrive_utils/lib/src/generate_thumbnail.dart b/packages/ardrive_utils/lib/src/generate_thumbnail.dart new file mode 100644 index 0000000000..2fb2edb473 --- /dev/null +++ b/packages/ardrive_utils/lib/src/generate_thumbnail.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +import 'package:image/image.dart' as img; + +Uint8List generateThumbnail(Uint8List data) { + final image = img.decodeImage(data); + final thumbnail = img.copyResize(image!, + width: + 100); // Resize the image to a width of 100 pixels, maintaining aspect ratio + return Uint8List.fromList(img.encodePng(thumbnail)); +} diff --git a/packages/ardrive_utils/pubspec.yaml b/packages/ardrive_utils/pubspec.yaml index 9f1b348e99..710bf7c11b 100644 --- a/packages/ardrive_utils/pubspec.yaml +++ b/packages/ardrive_utils/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: ref: v3.9.0 js: ^0.6.7 equatable: ^2.0.5 + image: ^4.2.0 dev_dependencies: flutter_test: diff --git a/packages/pst/pubspec.lock b/packages/pst/pubspec.lock index 9b3227d25a..3356b91fc9 100644 --- a/packages/pst/pubspec.lock +++ b/packages/pst/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" ardrive_http: dependency: "direct main" description: @@ -314,6 +322,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" isolated_worker: dependency: transitive description: @@ -442,6 +458,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" platform: dependency: transitive description: @@ -599,6 +623,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.2.0 <4.0.0" flutter: ">=3.7.12" diff --git a/pubspec.lock b/pubspec.lock index 899c762433..950657359c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1217,10 +1217,10 @@ packages: dependency: transitive description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" image_picker: dependency: transitive description: From 5ec4506e349daf1c7be9140f5acfc4bb5b2d5147 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:16:21 -0300 Subject: [PATCH 03/17] add license --- lib/entities/license_assertion.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/entities/license_assertion.dart b/lib/entities/license_assertion.dart index 038fd2cfdb..35eb05986b 100644 --- a/lib/entities/license_assertion.dart +++ b/lib/entities/license_assertion.dart @@ -35,8 +35,8 @@ class LicenseAssertionEntity with TransactionPropertiesMixin { TransactionCommonMixin transaction, ) { try { - // assert(transaction.getTag(LicenseTag.appName) == - // LicenseTag.appNameLicenseAssertion); + assert(transaction.getTag(LicenseTag.appName) == + LicenseTag.appNameLicenseAssertion); final additionalTags = Map.fromEntries(transaction.tags .where((tag) => !licenseAssertionTxBaseTagKeys.contains(tag.name)) .map((tag) => MapEntry(tag.name, tag.value))); From 92fb49785b4d21fda9bda5b3736fc89b5d72576a Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:22:56 -0300 Subject: [PATCH 04/17] fix lint warnings --- lib/dev_tools/thumbnail_generator_poc.dart | 24 ++++++------------- .../lib/src/ardrive_uploader.dart | 10 -------- .../lib/src/data_bundler.dart | 10 ++++++++ 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/lib/dev_tools/thumbnail_generator_poc.dart b/lib/dev_tools/thumbnail_generator_poc.dart index 59c69264d6..50ea64500c 100644 --- a/lib/dev_tools/thumbnail_generator_poc.dart +++ b/lib/dev_tools/thumbnail_generator_poc.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/services/arweave/arweave.dart'; @@ -27,10 +29,6 @@ class _ThumbnailGeneratorPOCState extends State { // load files Future _generateThumbnailsForFiles({required String driveId}) async { final driveDao = context.read(); - // final currentDriveId = - // (context.read().state as DriveDetailLoadSuccess) - // .currentDrive - // .id; final files = await (driveDao.select(driveDao.fileEntries) ..where((tbl) => tbl.driveId.equals(driveId))) @@ -39,7 +37,6 @@ class _ThumbnailGeneratorPOCState extends State { setState(() { _files = files; }); - // ignore: use_build_context_synchronously final arweaveService = context.read(); final turboUploadService = context.read(); final wallet = context.read().currentUser.wallet; @@ -57,9 +54,9 @@ class _ThumbnailGeneratorPOCState extends State { final bytes = await ardriveHttp.getAsBytes(realImageUrl); - final uploader = ArDriveUploader(turboUploadUri: Uri.parse( - // ignore: use_build_context_synchronously - context.read().config.defaultTurboUploadUrl!)); + final uploader = ArDriveUploader( + turboUploadUri: Uri.parse( + context.read().config.defaultTurboUploadUrl!)); final data = generateThumbnail(bytes.data); @@ -77,7 +74,6 @@ class _ThumbnailGeneratorPOCState extends State { args: thumbnailArgs, file: file, type: UploadType.turbo, - // ignore: use_build_context_synchronously wallet: context.read().currentUser.wallet, ); @@ -109,13 +105,7 @@ class _ThumbnailGeneratorPOCState extends State { wallet: wallet, ); fileEntity.txId = fileDataItem.id; - } else { - // final fileTx = await arweaveService.prepareEntityTx( - // fileEntity, wallet, fileKey); - - // await _arweave.postTx(fileTx); - // fileEntity.txId = fileTx.id; - } + } else {} logger.i( 'Updating file ${f.id} with txId ${fileEntity.txId}. Data content type: ${fileEntity.dataContentType}'); @@ -130,7 +120,7 @@ class _ThumbnailGeneratorPOCState extends State { } final textController = TextEditingController(); - +r @override Widget build(BuildContext context) { if (_files == null) { diff --git a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart index fecd6f4078..d75f4fe3f8 100644 --- a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart +++ b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart @@ -384,13 +384,3 @@ class _ArDriveUploader implements ArDriveUploader { return uploadController; } } - -class DataResultWithContents { - final T dataItemResult; - final List contents; - - DataResultWithContents({ - required this.dataItemResult, - required this.contents, - }); -} diff --git a/packages/ardrive_uploader/lib/src/data_bundler.dart b/packages/ardrive_uploader/lib/src/data_bundler.dart index ce8c3d52b3..411896a7e3 100644 --- a/packages/ardrive_uploader/lib/src/data_bundler.dart +++ b/packages/ardrive_uploader/lib/src/data_bundler.dart @@ -822,3 +822,13 @@ Future< ); } } + +class DataResultWithContents { + final T dataItemResult; + final List contents; + + DataResultWithContents({ + required this.dataItemResult, + required this.contents, + }); +} From f28b054f776dbf1335ab714f76620f8bb723d5ac Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:24:40 -0300 Subject: [PATCH 05/17] fix lint warnings --- lib/dev_tools/app_dev_tools.dart | 51 ---------------------- lib/dev_tools/thumbnail_generator_poc.dart | 2 +- 2 files changed, 1 insertion(+), 52 deletions(-) diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index 2e9da08426..919e0128a3 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -158,20 +158,6 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.text, ); - final ArDriveDevToolOption defaultTurboPaymentUrlOption = - ArDriveDevToolOption( - name: 'defaultTurboUrl', - value: config.defaultTurboPaymentUrl, - onChange: (value) { - setState(() { - configService.updateAppConfig( - config.copyWith(defaultTurboPaymentUrl: value), - ); - }); - }, - type: ArDriveDevToolOptionType.text, - ); - final ArDriveDevToolOption stripePublishableKey = ArDriveDevToolOption( name: 'stripePublishableKey', value: config.stripePublishableKey, @@ -358,43 +344,6 @@ class AppConfigWindowManagerState extends State { showArDriveDialog(context, content: const ArDriveStandardModal(content: ThumbnailGeneratorPOC())); - - // pickImageAndGenerateThumbnail( - // onThumbnailGenerated: (thumbnail) async { - // final uploader = ArDriveUploader( - // turboUploadUri: Uri.parse(context - // .read() - // .config - // .defaultTurboUploadUrl!)); - - // final file = await IOFileAdapter().fromData(thumbnail, - // name: 'thumbnail', lastModifiedDate: DateTime.now()); - // final thumbnailArgs = ThumbnailMetadataArgs( - // contentType: 'image/png', - // height: 100, - // width: 100, - // thumbnailSize: thumbnail.length, - // relatesTo: 'a92e1181-f2b5-41b7-acc9-fac06128e647', - // ); - - // final controller = await uploader.uploadThumbnail( - // args: thumbnailArgs, - // file: file, - // type: UploadType.turbo, - // // ignore: use_build_context_synchronously - // wallet: context.read().currentUser.wallet, - // ); - - // controller.onDone((task) { - // logger.i('Thumbnail uploaded'); - // }); - - // showArDriveDialog(context, - // content: ArDriveStandardModal( - // content: Image.memory(thumbnail), - // )); - // }, - // ); } catch (e) { logger.e('Error setting default data on payment form', e); } diff --git a/lib/dev_tools/thumbnail_generator_poc.dart b/lib/dev_tools/thumbnail_generator_poc.dart index 50ea64500c..f255952fb1 100644 --- a/lib/dev_tools/thumbnail_generator_poc.dart +++ b/lib/dev_tools/thumbnail_generator_poc.dart @@ -120,7 +120,7 @@ class _ThumbnailGeneratorPOCState extends State { } final textController = TextEditingController(); -r + @override Widget build(BuildContext context) { if (_files == null) { From 0435ff98bb20e99a50278607f2fe17539d8f7132 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:48:39 -0300 Subject: [PATCH 06/17] upload thumbnails --- lib/blocs/upload/upload_cubit.dart | 17 +- lib/blocs/upload/upload_state.dart | 1 - lib/components/upload_form.dart | 28 ++ lib/dev_tools/pick_image_thumbnail_poc.dart | 26 -- lib/dev_tools/thumbnail_generator_poc.dart | 189 ++++---- lib/entities/file_entity.dart | 68 ++- lib/models/daos/drive_dao/drive_dao.dart | 5 +- lib/models/file_entry.dart | 1 - lib/models/file_revision.dart | 11 +- lib/models/tables/file_entries.drift | 2 +- lib/models/tables/file_revisions.drift | 2 +- .../components/drive_detail_data_list.dart | 83 ++-- .../drive_detail/components/hover_widget.dart | 3 + lib/pages/drive_detail/drive_detail_page.dart | 2 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- packages/ardrive_ui/pubspec.yaml | 2 +- .../Flutter/GeneratedPluginRegistrant.swift | 2 + .../lib/src/ardrive_uploader.dart | 27 +- .../lib/src/arfs_upload_metadata.dart | 61 ++- .../lib/src/data_bundler.dart | 419 +++++++++--------- .../lib/src/metadata_generator.dart | 40 -- .../lib/src/upload_dispatcher.dart | 116 ++--- .../lib/src/upload_strategy.dart | 8 +- .../ardrive_uploader/lib/src/upload_task.dart | 5 + .../test/arfs_upload_metadata_test.dart | 5 +- .../test/upload_task_test.dart | 4 + .../lib/src/generate_thumbnail.dart | 50 ++- packages/ardrive_utils/pubspec.yaml | 1 + packages/pst/pubspec.lock | 66 ++- pubspec.lock | 64 +-- .../domain/manifest_repository_test.dart | 2 + web/index.html | 3 + 32 files changed, 746 insertions(+), 571 deletions(-) delete mode 100644 lib/dev_tools/pick_image_thumbnail_poc.dart diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index bf032853f3..751bf01569 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -54,6 +54,11 @@ class UploadCubit extends Cubit { late Drive _targetDrive; late FolderEntry _targetFolder; UploadMethod? _uploadMethod; + bool _uploadThumbnail = true; + + void changeUploadThumbnailOption(bool uploadThumbnail) { + _uploadThumbnail = uploadThumbnail; + } void setUploadMethod( UploadMethod? method, @@ -601,6 +606,7 @@ class UploadCubit extends Cubit { final uploadController = await ardriveUploader.uploadEntities( entities: entities, wallet: _auth.currentUser.wallet, + uploadThumbnail: _uploadThumbnail, type: _uploadMethod == UploadMethod.ar ? UploadType.d2n : UploadType.turbo, driveKey: driveKey, @@ -722,6 +728,7 @@ class UploadCubit extends Cubit { files: uploadFiles, wallet: _auth.currentUser.wallet, driveKey: driveKey, + uploadThumbnail: _uploadThumbnail, type: _uploadMethod == UploadMethod.ar ? UploadType.d2n : UploadType.turbo, ); @@ -817,6 +824,14 @@ class UploadCubit extends Cubit { ? RevisionAction.uploadNewVersion : RevisionAction.create; + Thumbnail? thumbnail; + + if (fileMetadata.thumbnailInfo != null) { + thumbnail = Thumbnail(variants: [ + Variant.fromJson(fileMetadata.thumbnailInfo!.first.toJson()) + ]); + } + final entity = FileEntity( dataContentType: fileMetadata.dataContentType, dataTxId: fileMetadata.dataTxId, @@ -827,7 +842,7 @@ class UploadCubit extends Cubit { name: fileMetadata.name, parentFolderId: fileMetadata.parentFolderId, size: fileMetadata.size, - thumbnailTxId: fileMetadata.thumbnailTxId, + thumbnail: thumbnail, // TODO: pinnedDataOwnerAddress ); diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index c588b84299..b0ec1169bf 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -80,7 +80,6 @@ class UploadReady extends UploadState { final bool isDragNDrop; final bool uploadIsPublic; final int numberOfFiles; - final UploadParams params; final bool isArConnect; diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 767f71083d..fa9e502259 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -419,6 +419,7 @@ class _UploadFormState extends State { ), ); } else if (state is UploadReady) { + final typography = ArDriveTypographyNew.of(context); return ReactiveForm( formGroup: context.watch().licenseCategoryForm, child: ReactiveFormConsumer(builder: (_, form, __) { @@ -478,6 +479,33 @@ class _UploadFormState extends State { }, ), ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + ArDriveCheckBox( + title: 'Upload with thumbnails', + checked: true, + titleStyle: typography.paragraphLarge( + fontWeight: ArFontWeight.semiBold, + ), + onChange: (value) { + context + .read() + .changeUploadThumbnailOption(value); + }, + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ArDriveIconButton( + icon: ArDriveIcons.info(), + tooltip: + 'Uploading with thumbnails is free, but may make your upload take longer.\nYou can always attach a thumbnail later.', + ), + ) + ], + ), + ), SizedBox( child: ReactiveForm( formGroup: diff --git a/lib/dev_tools/pick_image_thumbnail_poc.dart b/lib/dev_tools/pick_image_thumbnail_poc.dart deleted file mode 100644 index f729182997..0000000000 --- a/lib/dev_tools/pick_image_thumbnail_poc.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:typed_data'; - -import 'package:ardrive_utils/ardrive_utils.dart'; -import 'package:universal_html/html.dart' as html; - -Future pickImageAndGenerateThumbnail({ - Function(Uint8List thumbnail)? onThumbnailGenerated, -}) async { - html.FileUploadInputElement uploadInput = html.FileUploadInputElement(); - uploadInput.accept = 'image/*'; - uploadInput.click(); - - uploadInput.onChange.listen((e) async { - final files = uploadInput.files; - if (files?.length == 1) { - final reader = html.FileReader(); - reader.readAsArrayBuffer(files![0]); - reader.onLoadEnd.listen((e) async { - final bytes = reader.result as Uint8List; - final thumbnail = generateThumbnail(bytes); - onThumbnailGenerated?.call(thumbnail); - // Use the thumbnail as needed - }); - } - }); -} diff --git a/lib/dev_tools/thumbnail_generator_poc.dart b/lib/dev_tools/thumbnail_generator_poc.dart index f255952fb1..3baf137260 100644 --- a/lib/dev_tools/thumbnail_generator_poc.dart +++ b/lib/dev_tools/thumbnail_generator_poc.dart @@ -1,19 +1,8 @@ // ignore_for_file: use_build_context_synchronously -import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/models/models.dart'; -import 'package:ardrive/services/arweave/arweave.dart'; -import 'package:ardrive/services/config/config_service.dart'; -import 'package:ardrive/turbo/services/upload_service.dart'; -import 'package:ardrive/utils/logger.dart'; -import 'package:ardrive_http/ardrive_http.dart'; -import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; -import 'package:ardrive_uploader/ardrive_uploader.dart'; -import 'package:ardrive_utils/ardrive_utils.dart'; -import 'package:drift/drift.dart' as drift; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class ThumbnailGeneratorPOC extends StatefulWidget { const ThumbnailGeneratorPOC({super.key}); @@ -28,95 +17,95 @@ class _ThumbnailGeneratorPOCState extends State { // load files Future _generateThumbnailsForFiles({required String driveId}) async { - final driveDao = context.read(); - - final files = await (driveDao.select(driveDao.fileEntries) - ..where((tbl) => tbl.driveId.equals(driveId))) - .get(); - - setState(() { - _files = files; - }); - final arweaveService = context.read(); - final turboUploadService = context.read(); - final wallet = context.read().currentUser.wallet; - - for (var f in files) { - if (FileTypeHelper.isImage(f.dataContentType ?? '') == false) { - logger.i('Skipping file'); - continue; - } - - final realImageUrl = - '${arweaveService.client.api.gatewayUrl.origin}/raw/${f.dataTxId}'; - - final ardriveHttp = ArDriveHTTP(); - - final bytes = await ardriveHttp.getAsBytes(realImageUrl); - - final uploader = ArDriveUploader( - turboUploadUri: Uri.parse( - context.read().config.defaultTurboUploadUrl!)); - - final data = generateThumbnail(bytes.data); - - final file = await IOFileAdapter() - .fromData(data, name: 'thumbnail', lastModifiedDate: DateTime.now()); - final thumbnailArgs = ThumbnailMetadataArgs( - contentType: 'image/png', - height: 100, - width: 100, - thumbnailSize: data.length, - relatesTo: f.dataTxId, - ); - - final controller = await uploader.uploadThumbnail( - args: thumbnailArgs, - file: file, - type: UploadType.turbo, - wallet: context.read().currentUser.wallet, - ); - - controller.onDone((tasks) async { - logger.i('Thumbnail uploaded'); - - setState(() { - _thumbnailsGenerated[f.dataTxId] = true; - }); - - await driveDao.transaction(() async { - f = f.copyWith( - lastUpdated: DateTime.now(), - thumbnailTxId: drift.Value( - (tasks.first as ThumbnailUploadTask).uploadItem!.data.id), - ); - - final fileEntity = f.asEntity(); - - if (turboUploadService.useTurboUpload) { - final fileDataItem = await arweaveService.prepareEntityDataItem( - fileEntity, - wallet, - // key: fileKey, - ); - - await turboUploadService.postDataItem( - dataItem: fileDataItem, - wallet: wallet, - ); - fileEntity.txId = fileDataItem.id; - } else {} - - logger.i( - 'Updating file ${f.id} with txId ${fileEntity.txId}. Data content type: ${fileEntity.dataContentType}'); - - await driveDao.writeToFile(f); - - await driveDao.insertFileRevision(fileEntity.toRevisionCompanion( - performedAction: RevisionAction.rename)); - }); - }); - } + // final driveDao = context.read(); + + // final files = await (driveDao.select(driveDao.fileEntries) + // ..where((tbl) => tbl.driveId.equals(driveId))) + // .get(); + + // setState(() { + // _files = files; + // }); + // final arweaveService = context.read(); + // final turboUploadService = context.read(); + // final wallet = context.read().currentUser.wallet; + + // for (var f in files) { + // if (FileTypeHelper.isImage(f.dataContentType ?? '') == false) { + // logger.i('Skipping file'); + // continue; + // } + + // final realImageUrl = + // '${arweaveService.client.api.gatewayUrl.origin}/raw/${f.dataTxId}'; + + // final ardriveHttp = ArDriveHTTP(); + + // final bytes = await ardriveHttp.getAsBytes(realImageUrl); + + // final uploader = ArDriveUploader( + // turboUploadUri: Uri.parse( + // context.read().config.defaultTurboUploadUrl!)); + + // final data = generateThumbnail(bytes.data); + + // final file = await IOFileAdapter() + // .fromData(data, name: 'thumbnail', lastModifiedDate: DateTime.now()); + // final thumbnailArgs = ThumbnailMetadataArgs( + // contentType: 'image/png', + // height: 100, + // width: 100, + // thumbnailSize: data.length, + // relatesTo: f.dataTxId, + // ); + + // final controller = await uploader.uploadThumbnail( + // args: thumbnailArgs, + // file: file, + // type: UploadType.turbo, + // wallet: context.read().currentUser.wallet, + // ); + + // controller.onDone((tasks) async { + // logger.i('Thumbnail uploaded'); + + // setState(() { + // _thumbnailsGenerated[f.dataTxId] = true; + // }); + + // await driveDao.transaction(() async { + // f = f.copyWith( + // lastUpdated: DateTime.now(), + // thumbnailTxId: drift.Value( + // (tasks.first as ThumbnailUploadTask).uploadItem!.data.id), + // ); + + // final fileEntity = f.asEntity(); + + // if (turboUploadService.useTurboUpload) { + // final fileDataItem = await arweaveService.prepareEntityDataItem( + // fileEntity, + // wallet, + // // key: fileKey, + // ); + + // await turboUploadService.postDataItem( + // dataItem: fileDataItem, + // wallet: wallet, + // ); + // fileEntity.txId = fileDataItem.id; + // } else {} + + // logger.i( + // 'Updating file ${f.id} with txId ${fileEntity.txId}. Data content type: ${fileEntity.dataContentType}'); + + // await driveDao.writeToFile(f); + + // await driveDao.insertFileRevision(fileEntity.toRevisionCompanion( + // performedAction: RevisionAction.rename)); + // }); + // }); + // } } final textController = TextEditingController(); diff --git a/lib/entities/file_entity.dart b/lib/entities/file_entity.dart index 53cae59726..d5fdde5665 100644 --- a/lib/entities/file_entity.dart +++ b/lib/entities/file_entity.dart @@ -38,7 +38,8 @@ class FileEntity extends EntityWithCustomMetadata { String? pinnedDataOwnerAddress; @JsonKey(includeIfNull: false) bool? isHidden; - String? thumbnailTxId; + + Thumbnail? thumbnail; @override @JsonKey(includeFromJson: false, includeToJson: false) @@ -72,7 +73,7 @@ class FileEntity extends EntityWithCustomMetadata { this.dataContentType, this.pinnedDataOwnerAddress, this.isHidden, - this.thumbnailTxId, + this.thumbnail, }) : super(ArDriveCrypto()); FileEntity.withUserProvidedDetails({ @@ -115,7 +116,6 @@ class FileEntity extends EntityWithCustomMetadata { ..bundledIn = transaction.bundledIn?.id ..createdAt = commitTime; - final tags = transaction.tags .map( (t) => Tag.fromJson(t.toJson()), @@ -165,3 +165,65 @@ class FileEntity extends EntityWithCustomMetadata { return merged; } } + +class Thumbnail { + List variants; + + Thumbnail({required this.variants}); + + factory Thumbnail.fromJson(Map json) { + var variantsJson = json['variants'] as List; + List variantsList = + variantsJson.map((i) => Variant.fromJson(i)).toList(); + + return Thumbnail( + variants: variantsList, + ); + } + + Map toJson() { + return { + 'variants': variants.map((variant) => variant.toJson()).toList(), + }; + } +} + +class Variant { + String name; + String txId; + int size; + int width; + int height; + double aspectRatio; + + Variant({ + required this.name, + required this.txId, + required this.size, + required this.width, + required this.height, + required this.aspectRatio, + }); + + factory Variant.fromJson(Map json) { + return Variant( + name: json['name'], + txId: json['txId'], + size: json['size'], + width: json['width'], + height: json['height'], + aspectRatio: json['aspectRatio'], + ); + } + + Map toJson() { + return { + 'name': name, + 'txId': txId, + 'size': size, + 'width': width, + 'height': height, + 'aspectRatio': aspectRatio, + }; + } +} diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index 4e83343bfd..2945b2c3f5 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/entities/entities.dart'; @@ -591,7 +592,9 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { isHidden: Value(entity.isHidden ?? false), // TODO: path is not used in the app, so it's not necessary to set it path: '', - thumbnailTxId: Value(entity.thumbnailTxId), + thumbnail: entity.thumbnail != null + ? Value(jsonEncode(entity.thumbnail!.toJson())) + : const Value(null), ); return into(fileEntries).insert( diff --git a/lib/models/file_entry.dart b/lib/models/file_entry.dart index 5abba50372..0895400e46 100644 --- a/lib/models/file_entry.dart +++ b/lib/models/file_entry.dart @@ -17,7 +17,6 @@ extension FileEntryExtensions on FileEntry { dataContentType: dataContentType, pinnedDataOwnerAddress: pinnedDataOwnerAddress, isHidden: isHidden, - thumbnailTxId: thumbnailTxId, ); file.customJsonMetadata = parseCustomJsonMetadata(customJsonMetadata); diff --git a/lib/models/file_revision.dart b/lib/models/file_revision.dart index 7ce685b2ea..9d77975bc1 100644 --- a/lib/models/file_revision.dart +++ b/lib/models/file_revision.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:ardrive/entities/entities.dart'; import 'package:drift/drift.dart'; @@ -25,7 +27,7 @@ extension FileRevisionsCompanionExtensions on FileRevisionsCompanion { isHidden: isHidden, // TODO: path is not used in the app, so it's not necessary to set it path: '', - thumbnailTxId: Value(thumbnailTxId.value), + thumbnail: Value(thumbnail.value), ); /// Returns a list of [NetworkTransactionsCompanion] representing the metadata and data transactions @@ -63,7 +65,7 @@ extension FileEntityExtensions on FileEntity { customJsonMetadata: Value(customJsonMetadataAsString), pinnedDataOwnerAddress: Value(pinnedDataOwnerAddress), isHidden: Value(isHidden ?? false), - thumbnailTxId: Value(thumbnailTxId), + thumbnail: Value(jsonEncode(thumbnail?.toJson())), ); FileRevision toRevision({ @@ -87,7 +89,7 @@ extension FileEntityExtensions on FileEntity { customJsonMetadata: customJsonMetadataAsString, pinnedDataOwnerAddress: pinnedDataOwnerAddress, isHidden: isHidden ?? false, - thumbnailTxId: thumbnailTxId, + thumbnail: jsonEncode(thumbnail?.toJson()), ); /// Returns the action performed on the file that lead to the new revision. @@ -107,7 +109,8 @@ extension FileEntityExtensions on FileEntity { return RevisionAction.hide; } else if (isHidden == false && previousRevision.isHidden.value == true) { return RevisionAction.unhide; - } else if (thumbnailTxId != previousRevision.thumbnailTxId.value) { + } else if (jsonEncode(thumbnail?.toJson()) != + previousRevision.thumbnail.value) { return RevisionAction.rename; } diff --git a/lib/models/tables/file_entries.drift b/lib/models/tables/file_entries.drift index 09a61b1295..996a1f5f11 100644 --- a/lib/models/tables/file_entries.drift +++ b/lib/models/tables/file_entries.drift @@ -16,7 +16,7 @@ CREATE TABLE file_entries ( bundledIn TEXT, - thumbnailTxId TEXT, + thumbnail TEXT, pinnedDataOwnerAddress TEXT, diff --git a/lib/models/tables/file_revisions.drift b/lib/models/tables/file_revisions.drift index 36818b96c2..c4533d9dbf 100644 --- a/lib/models/tables/file_revisions.drift +++ b/lib/models/tables/file_revisions.drift @@ -15,7 +15,7 @@ CREATE TABLE file_revisions ( dataTxId TEXT NOT NULL, licenseTxId TEXT, - thumbnailTxId TEXT, + thumbnail TEXT, bundledIn TEXT, diff --git a/lib/pages/drive_detail/components/drive_detail_data_list.dart b/lib/pages/drive_detail/components/drive_detail_data_list.dart index 18902f6d43..052a2f018d 100644 --- a/lib/pages/drive_detail/components/drive_detail_data_list.dart +++ b/lib/pages/drive_detail/components/drive_detail_data_list.dart @@ -158,36 +158,41 @@ Widget _buildDataListContent( index: 0, canHide: false, ), - if (constraints.maxWidth > 500) - TableColumn( - appLocalizationsOf(context).size, - 3, - index: 1, - canHide: false, - ), - if (constraints.maxWidth > 640) - TableColumn( - appLocalizationsOf(context).lastUpdated, - 3, - index: 2, - isVisible: columnVisibility[2] ?? true, - ), - if (constraints.maxWidth > 700) - TableColumn( - appLocalizationsOf(context).dateCreated, - 3, - index: 3, - isVisible: columnVisibility[3] ?? true, - ), - if (constraints.maxWidth > 820) - TableColumn( - // TODO: Localize - // appLocalizationsOf(context).licenseType, - 'License', - 2, - index: 4, - isVisible: columnVisibility[4] ?? true, - ), + // if (constraints.maxWidth > 500) + TableColumn( + appLocalizationsOf(context).size, + 3, + index: 1, + canHide: false, + isVisible: + (constraints.maxWidth > 500 && (columnVisibility[1] ?? true)), + ), + // if (constraints.maxWidth > 640) + TableColumn( + appLocalizationsOf(context).lastUpdated, + 3, + index: 2, + isVisible: + (constraints.maxWidth > 640 && (columnVisibility[2] ?? true)), + ), + // if (constraints.maxWidth > 700) + TableColumn( + appLocalizationsOf(context).dateCreated, + 3, + index: 3, + isVisible: + (constraints.maxWidth > 700 && (columnVisibility[3] ?? true)), + ), + // if (constraints.maxWidth > 820) + TableColumn( + // TODO: Localize + // appLocalizationsOf(context).licenseType, + 'License', + 2, + index: 4, + isVisible: + (constraints.maxWidth > 820 && (columnVisibility[4] ?? true)), + ), ]; final driveDetailCubitState = context.read().state; @@ -408,7 +413,9 @@ class DriveDataTableItemMapper { index: index, pinnedDataOwnerAddress: file.pinnedDataOwnerAddress, isHidden: file.isHidden, - thumbnailUrl: file.thumbnailTxId, + thumbnailUrl: file.thumbnail != null + ? Thumbnail.fromJson(jsonDecode(file.thumbnail!)).variants.first.txId + : null, ); } @@ -435,7 +442,12 @@ class DriveDataTableItemMapper { index: 0, pinnedDataOwnerAddress: fileEntry.pinnedDataOwnerAddress, isHidden: fileEntry.isHidden, - thumbnailUrl: fileEntry.thumbnailTxId, + // thumbnailUrl: fileEntry.thumbnail != null + // ? Thumbnail.fromJson(jsonDecode(fileEntry.thumbnail!)) + // .variants + // .first + // .txId + // : null, ); } @@ -500,7 +512,12 @@ class DriveDataTableItemMapper { index: 0, pinnedDataOwnerAddress: revision.pinnedDataOwnerAddress, isHidden: revision.isHidden, - thumbnailUrl: revision.thumbnailTxId, + // thumbnailUrl: revision.thumbnail != null + // ? Thumbnail.fromJson(jsonDecode(revision.thumbnail!)) + // .variants + // .first + // .txId + // : null, ); } } diff --git a/lib/pages/drive_detail/components/hover_widget.dart b/lib/pages/drive_detail/components/hover_widget.dart index e805ed7652..6f7c62ab5e 100644 --- a/lib/pages/drive_detail/components/hover_widget.dart +++ b/lib/pages/drive_detail/components/hover_widget.dart @@ -99,12 +99,14 @@ class ArDriveIconButton extends StatelessWidget { this.onPressed, this.size = 16, this.tooltip, + this.scale, }); final ArDriveIcon icon; final Function()? onPressed; final double size; final String? tooltip; + final bool? scale; @override Widget build(BuildContext context) { @@ -112,6 +114,7 @@ class ArDriveIconButton extends StatelessWidget { onTap: onPressed, child: HoverWidget( tooltip: tooltip, + hoverScale: scale == true ? 1.1 : 1.0, child: icon, ), ); diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index f395d4340e..e80d37799e 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:math'; import 'package:ardrive/app_shell.dart'; @@ -27,6 +28,7 @@ import 'package:ardrive/dev_tools/app_dev_tools.dart'; import 'package:ardrive/dev_tools/shortcut_handler.dart'; import 'package:ardrive/download/multiple_file_download_modal.dart'; import 'package:ardrive/entities/entities.dart' as entities; +import 'package:ardrive/entities/file_entity.dart'; import 'package:ardrive/l11n/l11n.dart'; import 'package:ardrive/misc/resources.dart'; import 'package:ardrive/models/license.dart'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index dcb6ac3b90..1b25475c4a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,7 +13,7 @@ import file_saver import file_selector_macos import firebase_core import firebase_crashlytics -import flutter_inappwebview_macos +import flutter_image_compress_macos import flutter_secure_storage_macos import just_audio import package_info_plus @@ -36,7 +36,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) - InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/packages/ardrive_ui/pubspec.yaml b/packages/ardrive_ui/pubspec.yaml index e8ee4c49e4..16dc5f6a55 100644 --- a/packages/ardrive_ui/pubspec.yaml +++ b/packages/ardrive_ui/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: flutter_portal: ^1.1.2 flutter_svg: ^2.0.10+1 percent_indicator: ^4.2.2 - flutter_svg_image: ^1.1.0+1 + flutter_svg_image: provider: ^6.0.5 equatable: ^2.0.5 diff --git a/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift index dd517499b2..5ea29635ed 100644 --- a/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/ardrive_uploader/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import device_info_plus import file_saver import file_selector_macos +import flutter_image_compress_macos import package_info_plus import path_provider_foundation import sentry_flutter @@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) diff --git a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart index d75f4fe3f8..f8d8c66b3d 100644 --- a/packages/ardrive_uploader/lib/src/ardrive_uploader.dart +++ b/packages/ardrive_uploader/lib/src/ardrive_uploader.dart @@ -19,6 +19,7 @@ abstract class ArDriveUploader { required Wallet wallet, SecretKey? driveKey, required UploadType type, + bool uploadThumbnail = true, }) { throw UnimplementedError(); } @@ -28,6 +29,7 @@ abstract class ArDriveUploader { required Wallet wallet, SecretKey? driveKey, required UploadType type, + bool uploadThumbnail = true, }) { throw UnimplementedError(); } @@ -39,6 +41,7 @@ abstract class ArDriveUploader { Function(ARFSUploadMetadata)? skipMetadataUpload, Function(ARFSUploadMetadata)? onCreateMetadata, required UploadType type, + bool uploadThumbnail = true, }) { throw UnimplementedError(); } @@ -47,7 +50,7 @@ abstract class ArDriveUploader { required IOFile file, required Wallet wallet, required UploadType type, - required ThumbnailMetadataArgs args, + required ThumbnailUploadMetadata thumbnailMetadata, }) { throw UnimplementedError(); } @@ -116,6 +119,7 @@ class _ArDriveUploader implements ArDriveUploader { required Wallet wallet, SecretKey? driveKey, required UploadType type, + bool uploadThumbnail = true, }) async { final dataBundler = _dataBundlerFactory.createDataBundler( type, @@ -155,6 +159,8 @@ class _ArDriveUploader implements ArDriveUploader { content: [metadata], encryptionKey: driveKey, type: type, + uploadThumbnail: + FileTypeHelper.isImage(file.contentType) && uploadThumbnail, ); uploadController.addTask(uploadTask); @@ -169,6 +175,7 @@ class _ArDriveUploader implements ArDriveUploader { required List<(ARFSUploadMetadataArgs, IOFile)> files, required Wallet wallet, SecretKey? driveKey, + bool uploadThumbnail = true, required UploadType type, }) async { logger.i('Creating a new upload controller using the upload type $type'); @@ -197,8 +204,8 @@ class _ArDriveUploader implements ArDriveUploader { final uploadController = UploadController( StreamController(), uploadSender, - numOfWorkers: driveKey != null ? 3 : 5, - maxTasksPerWorker: driveKey != null ? 1 : 3, + numOfWorkers: driveKey != null ? 2 : 5, + maxTasksPerWorker: driveKey != null ? 3 : 3, ); for (var f in files) { @@ -216,6 +223,8 @@ class _ArDriveUploader implements ArDriveUploader { content: [metadata], encryptionKey: driveKey, type: type, + uploadThumbnail: + FileTypeHelper.isImage(f.$2.contentType) && uploadThumbnail, ); uploadController.addTask(fileTask); @@ -237,6 +246,7 @@ class _ArDriveUploader implements ArDriveUploader { Function(ARFSUploadMetadata p1)? skipMetadataUpload, Function(ARFSUploadMetadata p1)? onCreateMetadata, UploadType type = UploadType.turbo, + bool uploadThumbnail = true, }) async { final dataBundler = _dataBundlerFactory.createDataBundler( type, @@ -304,6 +314,8 @@ class _ArDriveUploader implements ArDriveUploader { encryptionKey: driveKey, content: [f.$1], type: type, + uploadThumbnail: + FileTypeHelper.isImage(f.$2.contentType) && uploadThumbnail, ); uploadController.addTask(fileTask); @@ -336,15 +348,8 @@ class _ArDriveUploader implements ArDriveUploader { required IOFile file, required Wallet wallet, required UploadType type, - required ThumbnailMetadataArgs args, + required ThumbnailUploadMetadata thumbnailMetadata, }) async { - final thumbnailMetadataGenerator = ThumbnailMetadataGenerator(); - - final thumbnailMetadata = await thumbnailMetadataGenerator.generateMetadata( - file, - arguments: args, - ); - final dataBundler = _dataBundlerFactory.createDataBundler( type, ); diff --git a/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart b/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart index cdf55a0d22..e20b60d006 100644 --- a/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart +++ b/packages/ardrive_uploader/lib/src/arfs_upload_metadata.dart @@ -5,14 +5,50 @@ abstract class UploadMetadata {} class ThumbnailUploadMetadata extends UploadMetadata { ThumbnailUploadMetadata({ - required this.entityMetadataTags, - required this.thumbnailSize, + required this.size, required this.relatesTo, + required this.height, + required this.width, + required this.aspectRatio, + required this.name, + required this.contentType, }); - final List entityMetadataTags; + List thumbnailTags() { + final tags = [ + Tag('Relates-To', relatesTo), + Tag(EntityTag.contentType, contentType), + Tag('Width', width.toString()), + Tag('Height', height.toString()), + Tag('Version', '1.0'), + // Implement cipher tags + ]; + + return tags; + } + final String relatesTo; - final int thumbnailSize; + final int size; + final int height; + final int width; + final int aspectRatio; + final String name; + final String contentType; + String? _txId; + + set setTxId(String txId) => _txId = txId; + get txId => _txId; + + Map toJson() { + return { + 'name': name, + 'txId': _txId, + 'size': size, + 'height': height, + 'width': width, + 'aspectRatio': aspectRatio, + }; + } } class ARFSDriveUploadMetadata extends ARFSUploadMetadata { @@ -151,19 +187,19 @@ class ARFSFileUploadMetadata extends ARFSUploadMetadata with ARFSUploadData { // Getter for licenseTxId String? get licenseTxId => _licenseTxId; - // Thumbnail TxId - String? _thumbnailTxId; + /// Additional Thumbnail tags for the file. + List? _thumbnailInfo; // Getter for thumbnailTxId - String? get thumbnailTxId => _thumbnailTxId; + List? get thumbnailInfo => _thumbnailInfo; // Public method to set licenseTxId with validation or additional logic void updateLicenseTxId(String licenseTxId) { _licenseTxId = licenseTxId; } - void updateThumbnailTxId(String thumbnailTxId) { - _thumbnailTxId = thumbnailTxId; + void updateThumbnailInfo(List thumbnailInfo) { + _thumbnailInfo = thumbnailInfo; } @override @@ -178,7 +214,12 @@ class ARFSFileUploadMetadata extends ARFSUploadMetadata with ARFSUploadData { 'lastModifiedDate': lastModifiedDate.millisecondsSinceEpoch, 'dataContentType': dataContentType, 'dataTxId': dataTxId, - if (_thumbnailTxId != null) 'thumbnailTxId': _thumbnailTxId, + if (_thumbnailInfo != null) + 'thumbnail': { + 'variants': [ + for (var variant in _thumbnailInfo!) variant.toJson(), + ], + }, if (licenseTxId != null) 'licenseTxId': licenseTxId, }; } diff --git a/packages/ardrive_uploader/lib/src/data_bundler.dart b/packages/ardrive_uploader/lib/src/data_bundler.dart index 411896a7e3..d65385c09a 100644 --- a/packages/ardrive_uploader/lib/src/data_bundler.dart +++ b/packages/ardrive_uploader/lib/src/data_bundler.dart @@ -17,17 +17,6 @@ import 'package:pst/pst.dart'; import 'package:uuid/uuid.dart'; abstract class DataBundler { - Future> createDataItemsForFile({ - required IOFile file, - required ARFSFileUploadMetadata metadata, - required Wallet wallet, - SecretKey? driveKey, - Function? onStartMetadataCreation, - Function? onFinishMetadataCreation, - Function? onStartBundleCreation, - Function? onFinishBundleCreation, - }); - Future createDataBundle({ required IOFile file, required ARFSFileUploadMetadata metadata, @@ -47,13 +36,6 @@ abstract class DataBundler { List? customBundleTags, SecretKey? driveKey, }); - - Future createDataItemForThumbnail({ - required IOFile file, - required ThumbnailUploadMetadata metadata, - required Wallet wallet, - SecretKey? driveKey, - }); } class DataTransactionBundler implements DataBundler { @@ -82,17 +64,17 @@ class DataTransactionBundler implements DataBundler { Function? onStartBundleCreation, Function? onFinishBundleCreation, }) async { - dataItemFiles ??= await createDataItemsForFile( + final uploadPreparation = await prepareDataItems( file: file, metadata: metadata, wallet: wallet, driveKey: driveKey, onStartMetadataCreation: onStartMetadataCreation, onFinishMetadataCreation: onFinishMetadataCreation, - onStartBundleCreation: onStartBundleCreation, - onFinishBundleCreation: onFinishBundleCreation, ); + dataItemFiles ??= uploadPreparation.dataItemFiles; + onStartBundleCreation?.call(); final transactionResult = await createDataBundleTransaction( @@ -228,69 +210,6 @@ class DataTransactionBundler implements DataBundler { (r) => r, ).run(); } - - @override - Future> createDataItemsForFile({ - required IOFile file, - required ARFSFileUploadMetadata metadata, - required Wallet wallet, - SecretKey? driveKey, - Function? onStartMetadataCreation, - Function? onFinishMetadataCreation, - Function? onStartBundleCreation, - Function? onFinishBundleCreation, - }) async { - SecretKey? key; - - if (driveKey != null) { - key = await deriveFileKey( - driveKey, - metadata.id, - keyByteLength, - ); - // onStartEncryption?.call(); - } else { - // onStartBundling?.call(); - } - - // returns the encrypted or not file read stream and the cipherIv if it was encrypted - final dataGenerator = await _dataGenerator( - dataStream: file.openReadStream, - fileLength: await file.length, - metadataId: metadata.id, - wallet: wallet, - encryptionKey: key, - ); - - onStartMetadataCreation?.call(); - - final metadataDataItem = await _generateMetadataDataItemForFile( - metadata: metadata, - dataStream: dataGenerator, - wallet: wallet, - driveKey: driveKey, - ); - - final fileDataItem = _generateFileDataItem( - metadata: metadata, - dataStream: dataGenerator.$1, - fileLength: dataGenerator.$4, - ); - - onFinishMetadataCreation?.call(); - - return [metadataDataItem, fileDataItem]; - } - - @override - Future createDataItemForThumbnail({ - required IOFile file, - required ThumbnailUploadMetadata metadata, - required Wallet wallet, - SecretKey? driveKey, - }) async { - throw UnimplementedError(); - } } class BDIDataBundler implements DataBundler { @@ -337,15 +256,13 @@ class BDIDataBundler implements DataBundler { Function? onStartBundleCreation, Function? onFinishBundleCreation, }) async { - final dataItemsFiles = await createDataItemsForFile( + final preparation = await prepareDataItems( file: file, metadata: metadata, wallet: wallet, driveKey: driveKey, onStartMetadataCreation: onStartMetadataCreation, onFinishMetadataCreation: onFinishMetadataCreation, - onStartBundleCreation: onStartBundleCreation, - onFinishBundleCreation: onFinishBundleCreation, ); onStartBundleCreation?.call(); @@ -356,7 +273,7 @@ class BDIDataBundler implements DataBundler { ); final createBundledDataItem = createBundledDataItemTaskEither( - dataItemFiles: dataItemsFiles, + dataItemFiles: preparation.dataItemFiles, wallet: wallet, tags: bundleTags.map((e) => createTag(e.name, e.value)).toList(), ); @@ -446,93 +363,9 @@ class BDIDataBundler implements DataBundler { ...dataItemsResult ]; } - - @override - Future> createDataItemsForFile({ - required IOFile file, - required ARFSFileUploadMetadata metadata, - required Wallet wallet, - SecretKey? driveKey, - Function? onStartMetadataCreation, - Function? onFinishMetadataCreation, - Function? onStartBundleCreation, - Function? onFinishBundleCreation, - }) async { - onStartMetadataCreation?.call(); - - SecretKeyData? key; - - if (driveKey != null) { - key = await deriveFileKey( - driveKey, - metadata.id, - keyByteLength, - ); - } - - // returns the encrypted or not file read stream and the cipherIv if it was encrypted - final dataGenerator = await _dataGenerator( - dataStream: file.openReadStream, - fileLength: await file.length, - metadataId: metadata.id, - wallet: wallet, - encryptionKey: key, - ); - - final metadataDataItem = await _generateMetadataDataItemForFile( - metadata: metadata, - dataStream: dataGenerator, - wallet: wallet, - driveKey: driveKey, - ); - - onFinishMetadataCreation?.call(); - - final fileDataItem = _generateFileDataItem( - metadata: metadata, - dataStream: dataGenerator.$1, - fileLength: dataGenerator.$4, - ); - - logger.d('Metadata tags length: ${metadataDataItem.tags.length}'); - logger.d('DataItem tags length: ${fileDataItem.tags.length}'); - - return [metadataDataItem, fileDataItem]; - } - - @override - Future createDataItemForThumbnail({ - required IOFile file, - required ThumbnailUploadMetadata metadata, - required Wallet wallet, - SecretKey? driveKey, - }) async { - final dataGenerator = await _dataGenerator( - dataStream: file.openReadStream, - fileLength: metadata.thumbnailSize, - metadataId: metadata.relatesTo, - wallet: wallet, - encryptionKey: driveKey, - ); - - final taskEither = await createDataItemTaskEither( - wallet: wallet, - dataStream: dataGenerator.$1, - dataStreamSize: metadata.thumbnailSize, - tags: metadata.entityMetadataTags - .map((e) => createTag(e.name, e.value)) - .toList(), - ).run(); - - return taskEither.match((l) { - throw l; - }, (r) { - return r; - }); - } } -DataItemFile _generateFileDataItem({ +DataItemFile _generateDataDataItem({ required ARFSFileUploadMetadata metadata, required Stream Function() dataStream, required int fileLength, @@ -623,49 +456,16 @@ Future _generateMetadataDataItem({ ); } -Future _generateMetadataDataItemForFile({ +Future _generateFileMetadataDataItem({ required ARFSFileUploadMetadata metadata, - required ( - Stream Function(), - Uint8List? dataStream, - String? cipher, - int fileLength - ) dataStream, + required DataItemResult dataItemResult, required Wallet wallet, SecretKey? driveKey, }) async { - if (driveKey != null) { - final cipher = dataStream.$3; - final cipherIv = dataStream.$2; - - metadata.setDataCipher( - cipher: cipher!, cipherIv: encodeBytesToBase64(cipherIv!)); + if (metadata.licenseDefinitionTxId != null) { + metadata.updateLicenseTxId(dataItemResult.id); } - final dataStreamGenerator = dataStream.$1; - final dataStreamSize = dataStream.$4; - - logger.d('Data tags: ${getJsonFromListOfTags(metadata.getDataTags())}'); - - final fileDataItemEither = createDataItemTaskEither( - wallet: wallet, - dataStream: dataStreamGenerator, - dataStreamSize: dataStreamSize, - tags: - metadata.getDataTags().map((e) => createTag(e.name, e.value)).toList(), - ); - - final fileDataItemResult = await fileDataItemEither.run(); - - fileDataItemResult.match((l) { - throw l; - }, (fileDataItem) { - metadata.updateDataTxId(fileDataItem.id); - if (metadata.licenseDefinitionTxId != null) { - metadata.updateLicenseTxId(fileDataItem.id); - } - }); - int metadataLength; final metadataBytes = utf8 @@ -832,3 +632,202 @@ class DataResultWithContents { required this.contents, }); } + +Future prepareDataItems({ + required IOFile file, + required ARFSFileUploadMetadata metadata, + required Wallet wallet, + SecretKey? driveKey, + Function? onStartMetadataCreation, + Function? onFinishMetadataCreation, + + /// pass down to the thumbnail creation + bool addThumbnail = true, +}) async { + SecretKey? key; + + if (driveKey != null) { + key = await deriveFileKey( + driveKey, + metadata.id, + keyByteLength, + ); + } + + // returns the encrypted or not file read stream and the cipherIv if it was encrypted + final dataGenerator = await _dataGenerator( + dataStream: file.openReadStream, + fileLength: await file.length, + metadataId: metadata.id, + wallet: wallet, + encryptionKey: key, + ); + + /// Gets the Data DataItem result + final dataDataItemResult = await _getDataItemResult( + wallet: wallet, + metadata: metadata, + dataStream: dataGenerator, + fileLength: dataGenerator.$4, + driveKey: driveKey, + ); + + DataItemFile? thumbnailDataItem; + IOFile? thumbnailFile; + + /// Thumbnail generation + if (addThumbnail) { + final thumbnailGenerationResult = await generateThumbnail( + await file.readAsBytes(), + ThumbnailSize.small, + ); + + thumbnailFile = await IOFile.fromData( + thumbnailGenerationResult.thumbnail, + name: 'thumbnail', + lastModifiedDate: DateTime.now(), + ); + + final thumbnailMetadata = ThumbnailUploadMetadata( + aspectRatio: thumbnailGenerationResult.aspectRatio, + height: thumbnailGenerationResult.height, + width: thumbnailGenerationResult.width, + size: thumbnailGenerationResult.size, + name: thumbnailGenerationResult.name, + relatesTo: metadata.dataTxId!, + contentType: file.contentType, + ); + + final thumbnailDataItemResult = await createDataItemForThumbnail( + file: thumbnailFile, + metadata: thumbnailMetadata, + wallet: wallet, + encryptionKey: driveKey, + ); + + thumbnailMetadata.setTxId = thumbnailDataItemResult.id; + + thumbnailDataItem = DataItemFile( + dataSize: thumbnailDataItemResult.dataSize, + streamGenerator: thumbnailDataItemResult.streamGenerator, + tags: thumbnailMetadata + .thumbnailTags() + .map((e) => createTag(e.name, e.value)) + .toList(), + ); + + /// needs to be `variants` + metadata.updateThumbnailInfo([thumbnailMetadata]); + } + + onStartMetadataCreation?.call(); + + final metadataDataItem = await _generateFileMetadataDataItem( + metadata: metadata, + dataItemResult: dataDataItemResult, + wallet: wallet, + driveKey: driveKey, + ); + + final dataDataItem = _generateDataDataItem( + metadata: metadata, + dataStream: dataGenerator.$1, + fileLength: dataGenerator.$4, + ); + + onFinishMetadataCreation?.call(); + + return UploadFilePreparation( + dataItemFiles: [metadataDataItem, dataDataItem], + thumbnailFile: thumbnailFile, + thumbnailDataItem: thumbnailDataItem, + ); +} + +class UploadFilePreparation { + final List dataItemFiles; + final IOFile? thumbnailFile; + final DataItemFile? thumbnailDataItem; + + UploadFilePreparation({ + required this.dataItemFiles, + this.thumbnailFile, + this.thumbnailDataItem, + }); +} + +Future _getDataItemResult({ + required Wallet wallet, + required ARFSFileUploadMetadata metadata, + required ( + Stream Function(), + Uint8List? dataStream, + String? cipher, + int fileLength + ) dataStream, + required int fileLength, + required SecretKey? driveKey, +}) async { + if (driveKey != null) { + final cipher = dataStream.$3; + final cipherIv = dataStream.$2; + + metadata.setDataCipher( + cipher: cipher!, cipherIv: encodeBytesToBase64(cipherIv!)); + } + + final dataStreamGenerator = dataStream.$1; + final dataStreamSize = dataStream.$4; + + logger.d('Data tags: ${getJsonFromListOfTags(metadata.getDataTags())}'); + + final fileDataItemEither = createDataItemTaskEither( + wallet: wallet, + dataStream: dataStreamGenerator, + dataStreamSize: dataStreamSize, + tags: + metadata.getDataTags().map((e) => createTag(e.name, e.value)).toList(), + ); + + final fileDataItemResult = await fileDataItemEither.run(); + + return fileDataItemResult.match((l) { + throw l; + }, (r) { + metadata.updateDataTxId(r.id); + + return r; + }); +} + +@override +Future createDataItemForThumbnail({ + required IOFile file, + required ThumbnailUploadMetadata metadata, + required Wallet wallet, + SecretKey? encryptionKey, +}) async { + final dataGenerator = await _dataGenerator( + dataStream: file.openReadStream, + fileLength: metadata.size, + metadataId: metadata.relatesTo, + wallet: wallet, + encryptionKey: encryptionKey, + ); + + final taskEither = await createDataItemTaskEither( + wallet: wallet, + dataStream: dataGenerator.$1, + dataStreamSize: metadata.size, + tags: metadata + .thumbnailTags() + .map((e) => createTag(e.name, e.value)) + .toList(), + ).run(); + + return taskEither.match((l) { + throw l; + }, (r) { + return r; + }); +} diff --git a/packages/ardrive_uploader/lib/src/metadata_generator.dart b/packages/ardrive_uploader/lib/src/metadata_generator.dart index ffc5728937..78f101747d 100644 --- a/packages/ardrive_uploader/lib/src/metadata_generator.dart +++ b/packages/ardrive_uploader/lib/src/metadata_generator.dart @@ -411,43 +411,3 @@ class ARFSTagsArgs extends Equatable { customBundleTags, ]; } - -class ThumbnailMetadataGenerator - implements - UploadMetadataGenerator { - @override - Future generateMetadata( - IOEntity entity, { - required ThumbnailMetadataArgs arguments, - }) async { - final tags = [ - Tag('Relates-To', arguments.relatesTo), - Tag(EntityTag.contentType, arguments.contentType), - Tag('width', arguments.width.toString()), - Tag('height', arguments.height.toString()), - ]; - - return ThumbnailUploadMetadata( - entityMetadataTags: tags, - thumbnailSize: arguments.thumbnailSize, - relatesTo: arguments.relatesTo, - ); - } -} - -class ThumbnailMetadataArgs { - final int height; - final int width; - final String contentType; - final int thumbnailSize; - final String relatesTo; - - ThumbnailMetadataArgs({ - required this.contentType, - required this.height, - required this.width, - required this.thumbnailSize, - required this.relatesTo, - }); -} diff --git a/packages/ardrive_uploader/lib/src/upload_dispatcher.dart b/packages/ardrive_uploader/lib/src/upload_dispatcher.dart index 1a430c6f50..1dc1634235 100644 --- a/packages/ardrive_uploader/lib/src/upload_dispatcher.dart +++ b/packages/ardrive_uploader/lib/src/upload_dispatcher.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_uploader/src/data_bundler.dart'; import 'package:ardrive_uploader/src/utils/logger.dart'; @@ -12,15 +11,13 @@ class UploadDispatcher { UploadFileStrategy _uploadFileStrategy; final UploadFolderStructureStrategy _uploadFolderStrategy; final UploadThumbnailStrategy _uploadThumbnailStrategy; - final DataBundler _dataBundler; UploadDispatcher({ required UploadFileStrategy uploadStrategy, required DataBundler dataBundler, required UploadFolderStructureStrategy uploadFolderStrategy, required UploadThumbnailStrategy uploadThumbnailStrategy, - }) : _dataBundler = dataBundler, - _uploadFolderStrategy = uploadFolderStrategy, + }) : _uploadFolderStrategy = uploadFolderStrategy, _uploadThumbnailStrategy = uploadThumbnailStrategy, _uploadFileStrategy = uploadStrategy; @@ -32,18 +29,12 @@ class UploadDispatcher { }) async { try { if (task is FileUploadTask) { - final dataItems = await _dataBundler.createDataItemsForFile( + final uploadPreparation = await prepareDataItems( file: task.file, metadata: task.metadata, wallet: wallet, + addThumbnail: task.uploadThumbnail, driveKey: task.encryptionKey, - onStartBundleCreation: () { - controller.updateProgress( - task: task.copyWith( - status: UploadStatus.creatingBundle, - ), - ); - }, onStartMetadataCreation: () { controller.updateProgress( task: task.copyWith( @@ -57,71 +48,56 @@ class UploadDispatcher { 'Uploading task ${task.id} with strategy: ${_uploadFileStrategy.runtimeType}'); await _uploadFileStrategy.upload( - dataItems: dataItems, + dataItems: uploadPreparation.dataItemFiles, task: task, wallet: wallet, controller: controller, verifyCancel: verifyCancel, ); - var updatedTask = controller.tasks[task.id]!; - - if (task.file.contentType == 'image/jpeg') { - controller.updateProgress( - task: updatedTask.copyWith( - status: UploadStatus.uploadingThumbnail, - ), - ); - - final data = generateThumbnail(await task.file.readAsBytes()); - - final thumbnailMetadata = ThumbnailUploadMetadata( - thumbnailSize: 0, - relatesTo: - (task.content!.first as ARFSFileUploadMetadata).dataTxId!, - entityMetadataTags: [], - ); - - final thumb = await IOFileAdapter().fromData( - data, - name: 'thumbnail', - lastModifiedDate: DateTime.now(), - contentType: 'image/jpeg', - ); - - final dataItem = await _dataBundler.createDataItemForThumbnail( - file: thumb, metadata: thumbnailMetadata, wallet: wallet); - - final thumbnailTask = ThumbnailUploadTask( - file: thumb, - metadata: thumbnailMetadata, - type: task.type, - uploadItem: DataItemUploadItem( - size: dataItem.dataItemSize, - data: dataItem, - ), - id: Uuid().v4(), - ); - - await _uploadThumbnailStrategy.upload( - task: thumbnailTask, - wallet: wallet, - controller: UploadController(StreamController(), this), - verifyCancel: verifyCancel, - ); - - updatedTask = controller.tasks[task.id]!; - - final uploadContent = task.content!.first as ARFSFileUploadMetadata; - - uploadContent.updateThumbnailTxId( - (thumbnailTask.uploadItem as DataItemUploadItem).data.id); - - controller.updateProgress( - task: updatedTask.copyWith( - status: UploadStatus.complete, content: [uploadContent]), - ); + var updatedTask = controller.tasks[task.id]! as FileUploadTask; + + /// Verify supported extentions + if (FileTypeHelper.isImage(updatedTask.metadata.dataContentType) && + task.uploadThumbnail) { + try { + controller.updateProgress( + task: updatedTask.copyWith( + status: UploadStatus.uploadingThumbnail, + ), + ); + + final fileMetadata = updatedTask.metadata; + + final thumbnailMetadata = fileMetadata.thumbnailInfo?.first; + + final thumbnailTask = ThumbnailUploadTask( + file: uploadPreparation.thumbnailFile!, + metadata: thumbnailMetadata!, + type: task.type, + id: Uuid().v4(), + + /// same encryption key as the file + encryptionKey: task.encryptionKey, + ); + + await _uploadThumbnailStrategy.upload( + task: thumbnailTask, + wallet: wallet, + controller: UploadController(StreamController(), this), + verifyCancel: verifyCancel, + ); + } catch (e) { + logger.e('Error uploading thumbnail: $e'); + } } + + updatedTask = controller.tasks[task.id]! as FileUploadTask; + + controller.updateProgress( + task: updatedTask.copyWith( + status: UploadStatus.complete, content: [updatedTask.metadata]), + ); } else if (task is FolderUploadTask) { await _uploadFolderStrategy.upload( task: task, diff --git a/packages/ardrive_uploader/lib/src/upload_strategy.dart b/packages/ardrive_uploader/lib/src/upload_strategy.dart index cee81aca1e..2598efd7b3 100644 --- a/packages/ardrive_uploader/lib/src/upload_strategy.dart +++ b/packages/ardrive_uploader/lib/src/upload_strategy.dart @@ -386,13 +386,11 @@ class UploadFolderStructureAsBundleStrategy class _UploadThumbnailStrategy implements UploadThumbnailStrategy { final StreamedUploadFactory _streamedUploadFactory; - final DataBundler _dataBundler; _UploadThumbnailStrategy({ required StreamedUploadFactory streamedUploadFactory, required DataBundler dataBundler, - }) : _streamedUploadFactory = streamedUploadFactory, - _dataBundler = dataBundler; + }) : _streamedUploadFactory = streamedUploadFactory; @override Future upload({ @@ -401,11 +399,11 @@ class _UploadThumbnailStrategy implements UploadThumbnailStrategy { required UploadController controller, required bool Function() verifyCancel, }) async { - final dataItem = await _dataBundler.createDataItemForThumbnail( + final dataItem = await createDataItemForThumbnail( wallet: wallet, file: task.file, metadata: task.metadata, - driveKey: task.encryptionKey, + encryptionKey: task.encryptionKey, ); task = task.copyWith( diff --git a/packages/ardrive_uploader/lib/src/upload_task.dart b/packages/ardrive_uploader/lib/src/upload_task.dart index 911b937a79..dad7782e7f 100644 --- a/packages/ardrive_uploader/lib/src/upload_task.dart +++ b/packages/ardrive_uploader/lib/src/upload_task.dart @@ -37,6 +37,8 @@ class FileUploadTask extends UploadTask { @override UploadType type; + final bool uploadThumbnail; + FileUploadTask({ this.uploadItem, this.isProgressAvailable = true, @@ -51,6 +53,7 @@ class FileUploadTask extends UploadTask { required this.type, this.metadataUploaded = false, this.error, + required this.uploadThumbnail, }) : id = id ?? const Uuid().v4(); @override @@ -71,6 +74,7 @@ class FileUploadTask extends UploadTask { bool? metadataUploaded, Object? error, IOFile? file, + bool? uploadThumbnail, }) { return FileUploadTask( cancelToken: cancelToken ?? this.cancelToken, @@ -86,6 +90,7 @@ class FileUploadTask extends UploadTask { type: type ?? this.type, metadataUploaded: metadataUploaded ?? this.metadataUploaded, error: error ?? this.error, + uploadThumbnail: uploadThumbnail ?? this.uploadThumbnail, ); } } diff --git a/packages/ardrive_uploader/test/arfs_upload_metadata_test.dart b/packages/ardrive_uploader/test/arfs_upload_metadata_test.dart index 42404c2c55..dd2afc9564 100644 --- a/packages/ardrive_uploader/test/arfs_upload_metadata_test.dart +++ b/packages/ardrive_uploader/test/arfs_upload_metadata_test.dart @@ -273,7 +273,7 @@ void main() { 'licenseTxId': licenseTxId, }); }); - + // TODO: update test to include the correct thumbnail object test( 'toJson returns correct map when dataTxId and licenseTxId and thumbnailTxId are set', () { @@ -281,7 +281,7 @@ void main() { const licenseTxId = 'licenseTxId'; metadata.updateDataTxId(dataTxId); metadata.updateLicenseTxId(licenseTxId); - metadata.updateThumbnailTxId('thumbnailTxId'); + // metadata.updateThumbnailTxId('thumbnailTxId'); final json = metadata.toJson(); expect(json, { @@ -291,7 +291,6 @@ void main() { 'dataContentType': dataContentType, 'dataTxId': dataTxId, 'licenseTxId': licenseTxId, - 'thumbnailTxId': 'thumbnailTxId', }); }); }); diff --git a/packages/ardrive_uploader/test/upload_task_test.dart b/packages/ardrive_uploader/test/upload_task_test.dart index 25977b62b4..a85c81aeff 100644 --- a/packages/ardrive_uploader/test/upload_task_test.dart +++ b/packages/ardrive_uploader/test/upload_task_test.dart @@ -40,6 +40,7 @@ void main() { file: mockFile, metadata: mockMetadata, type: UploadType.turbo, + uploadThumbnail: true, ); expect(task.file, mockFile); @@ -66,6 +67,7 @@ void main() { cancelToken: mockCancelToken, metadataUploaded: true, uploadItem: mockUploadItem, + uploadThumbnail: true, ); expect(task.file, mockFile); @@ -86,6 +88,7 @@ void main() { file: mockFile, metadata: mockMetadata, type: UploadType.turbo, + uploadThumbnail: true, ); final newTask = task.copyWith( @@ -107,6 +110,7 @@ void main() { file: mockFile, metadata: mockMetadata, type: UploadType.turbo, + uploadThumbnail: true, ); final errorInfo = task.errorInfo(); diff --git a/packages/ardrive_utils/lib/src/generate_thumbnail.dart b/packages/ardrive_utils/lib/src/generate_thumbnail.dart index 2fb2edb473..e8f60decb0 100644 --- a/packages/ardrive_utils/lib/src/generate_thumbnail.dart +++ b/packages/ardrive_utils/lib/src/generate_thumbnail.dart @@ -1,11 +1,49 @@ import 'dart:typed_data'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:image/image.dart' as img; -Uint8List generateThumbnail(Uint8List data) { - final image = img.decodeImage(data); - final thumbnail = img.copyResize(image!, - width: - 100); // Resize the image to a width of 100 pixels, maintaining aspect ratio - return Uint8List.fromList(img.encodePng(thumbnail)); +enum ThumbnailSize { + small, + medium, + large, +} + +Future generateThumbnail( + Uint8List data, + ThumbnailSize size, +) async { + var result = await FlutterImageCompress.compressWithList( + data, + minHeight: 75, + minWidth: 75, + ); + final thumbnail = img.decodeImage(result)!; + + return ThumbnailGenerationResult( + thumbnail: img.encodeJpg(thumbnail), + size: thumbnail.length, + height: thumbnail.height, + width: thumbnail.width, + aspectRatio: thumbnail.width ~/ thumbnail.height, + name: size.name, + ); +} + +class ThumbnailGenerationResult { + final Uint8List thumbnail; + final int size; + final int height; + final int width; + final int aspectRatio; + final String name; + + ThumbnailGenerationResult({ + required this.thumbnail, + required this.size, + required this.height, + required this.width, + required this.aspectRatio, + required this.name, + }); } diff --git a/packages/ardrive_utils/pubspec.yaml b/packages/ardrive_utils/pubspec.yaml index 710bf7c11b..1f0c78e0a1 100644 --- a/packages/ardrive_utils/pubspec.yaml +++ b/packages/ardrive_utils/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: js: ^0.6.7 equatable: ^2.0.5 image: ^4.2.0 + flutter_image_compress: ^2.3.0 dev_dependencies: flutter_test: diff --git a/packages/pst/pubspec.lock b/packages/pst/pubspec.lock index 3356b91fc9..8c4d163660 100644 --- a/packages/pst/pubspec.lock +++ b/packages/pst/pubspec.lock @@ -130,6 +130,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -248,6 +256,54 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_image_compress: + dependency: transitive + description: + name: flutter_image_compress + sha256: "45a3071868092a61b11044c70422b04d39d4d9f2ef536f3c5b11fb65a1e7dd90" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "26df6385512e92b3789dc76b613b54b55c457a7f1532e59078b04bf189782d47" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: f02fe352b17f82b72f481de45add240db062a2585850bea1667e82cc4cd6c311 + url: "https://pub.dev" + source: hosted + version: "0.1.4+1" flutter_lints: dependency: "direct dev" description: @@ -607,6 +663,14 @@ packages: url: "https://pub.dev" source: hosted version: "13.0.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" win32: dependency: transitive description: @@ -632,5 +696,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.7.12" diff --git a/pubspec.lock b/pubspec.lock index 950657359c..41e25d086a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -850,66 +850,50 @@ packages: dependency: transitive description: name: flutter_image_compress - sha256: "37f1b26399098e5f97b74c1483f534855e7dff68ead6ddaccf747029fb03f29f" + sha256: "45a3071868092a61b11044c70422b04d39d4d9f2ef536f3c5b11fb65a1e7dd90" url: "https://pub.dev" source: hosted - version: "1.1.3" - flutter_inappwebview: - dependency: transitive - description: - name: flutter_inappwebview - sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_inappwebview_android: - dependency: transitive - description: - name: flutter_inappwebview_android - sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 - url: "https://pub.dev" - source: hosted - version: "1.0.13" - flutter_inappwebview_internal_annotations: + version: "2.3.0" + flutter_image_compress_common: dependency: transitive description: - name: flutter_inappwebview_internal_annotations - sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + name: flutter_image_compress_common + sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee" url: "https://pub.dev" source: hosted - version: "1.1.1" - flutter_inappwebview_ios: + version: "1.0.5" + flutter_image_compress_macos: dependency: transitive description: - name: flutter_inappwebview_ios - sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + name: flutter_image_compress_macos + sha256: "26df6385512e92b3789dc76b613b54b55c457a7f1532e59078b04bf189782d47" url: "https://pub.dev" source: hosted - version: "1.0.13" - flutter_inappwebview_macos: + version: "1.0.2" + flutter_image_compress_ohos: dependency: transitive description: - name: flutter_inappwebview_macos - sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 url: "https://pub.dev" source: hosted - version: "1.0.11" - flutter_inappwebview_platform_interface: + version: "0.0.3" + flutter_image_compress_platform_interface: dependency: transitive description: - name: flutter_inappwebview_platform_interface - sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" url: "https://pub.dev" source: hosted - version: "1.0.10" - flutter_inappwebview_web: + version: "1.0.5" + flutter_image_compress_web: dependency: transitive description: - name: flutter_inappwebview_web - sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + name: flutter_image_compress_web + sha256: f02fe352b17f82b72f481de45add240db062a2585850bea1667e82cc4cd6c311 url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "0.1.4+1" flutter_launcher_icons: dependency: "direct main" description: @@ -1042,10 +1026,10 @@ packages: dependency: transitive description: name: flutter_svg_image - sha256: "32d5970cbcba9a3436f614a34450c14bc91806d561776aec0b5b7ccbe21467cd" + sha256: "8f50ace0234d5d2d349cafe1224379933990f3e4145b9f02f22ba977131396d0" url: "https://pub.dev" source: hosted - version: "1.1.0+1" + version: "1.0.0" flutter_test: dependency: "direct dev" description: flutter diff --git a/test/manifest/domain/manifest_repository_test.dart b/test/manifest/domain/manifest_repository_test.dart index 31ffd8404c..98d845bf62 100644 --- a/test/manifest/domain/manifest_repository_test.dart +++ b/test/manifest/domain/manifest_repository_test.dart @@ -169,6 +169,7 @@ void main() async { file: mockManifestFile, metadata: mockMetadata, type: UploadType.turbo, + uploadThumbnail: true, ); uploadTaskAR = FileUploadTask( @@ -176,6 +177,7 @@ void main() async { file: mockManifestFile, metadata: mockMetadata, type: UploadType.d2n, + uploadThumbnail: true, ); when(() => mockUploader.upload( diff --git a/web/index.html b/web/index.html index 314187769c..04207a08f8 100644 --- a/web/index.html +++ b/web/index.html @@ -24,6 +24,9 @@ + + +