From a8f3fd62150f8ce980ad8cc5a488fe7cf0cc52ba Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 28 May 2024 19:30:55 -0300 Subject: [PATCH 01/34] thumbnail poc --- lib/download/ardrive_downloader.dart | 42 +++++++++ .../repository/thumbnail_repository.dart | 73 +++++++++++++++ lib/drive_explorer/thumbnail/thumbnail.dart | 8 ++ .../thumbnail/thumbnail_bloc.dart | 31 +++++++ .../thumbnail/thumbnail_event.dart | 17 ++++ .../thumbnail/thumbnail_state.dart | 23 +++++ lib/entities/license_assertion.dart | 4 +- .../components/drive_explorer_item_tile.dart | 92 +++++++++++++++++++ 8 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart create mode 100644 lib/drive_explorer/thumbnail/thumbnail.dart create mode 100644 lib/drive_explorer/thumbnail/thumbnail_bloc.dart create mode 100644 lib/drive_explorer/thumbnail/thumbnail_event.dart create mode 100644 lib/drive_explorer/thumbnail/thumbnail_state.dart diff --git a/lib/download/ardrive_downloader.dart b/lib/download/ardrive_downloader.dart index f6b99f6018..e17804a71c 100644 --- a/lib/download/ardrive_downloader.dart +++ b/lib/download/ardrive_downloader.dart @@ -26,6 +26,19 @@ abstract class ArDriveDownloader { String? cipherIvString, }); + Future downloadToMemory({ + required TransactionCommonMixin dataTx, + required int fileSize, + required String fileName, + required DateTime lastModifiedDate, + required String contentType, + required bool isManifest, + Completer? cancelWithReason, + SecretKey? fileKey, + String? cipher, + String? cipherIvString, + }); + Future abortDownload(); factory ArDriveDownloader({ @@ -272,4 +285,33 @@ class _ArDriveDownloader implements ArDriveDownloader { return; } + + @override + Future downloadToMemory({ + required TransactionCommonMixin dataTx, + required int fileSize, + required String fileName, + required DateTime lastModifiedDate, + required String contentType, + required bool isManifest, + Completer? cancelWithReason, + SecretKey? fileKey, + String? cipher, + String? cipherIvString, + }) async { + final stream = await _getFileStream( + dataTx: dataTx, + fileSize: fileSize, + fileName: fileName, + lastModifiedDate: lastModifiedDate, + contentType: contentType, + fileKey: fileKey, + cipher: cipher, + cipherIvString: cipherIvString, + ); + + final data = await stream.toList(); + + return Uint8List.fromList(data.expand((element) => element).toList()); + } } diff --git a/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart b/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart new file mode 100644 index 0000000000..e167da2ad3 --- /dev/null +++ b/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart @@ -0,0 +1,73 @@ +import 'dart:typed_data'; + +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/download/ardrive_downloader.dart'; +import 'package:ardrive/drive_explorer/thumbnail/thumbnail.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; +import 'package:ardrive/services/arweave/arweave.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; + +class ThumbnailRepository { + final ArweaveService _arweaveService; + final ArDriveDownloader _arDriveDownloader; + final DriveDao _driveDao; + final ArDriveAuth _arDriveAuth; + + ThumbnailRepository({ + required ArweaveService arweaveService, + required ArDriveDownloader arDriveDownloader, + required DriveDao driveDao, + required ArDriveAuth arDriveAuth, + }) : _driveDao = driveDao, + _arDriveDownloader = arDriveDownloader, + _arweaveService = arweaveService, + _arDriveAuth = arDriveAuth; + + Future getThumbnail({ + FileDataTableItem? fileDataTableItem, + bool returnData = false, + }) async { + final drive = await _driveDao + .driveById(driveId: fileDataTableItem!.driveId) + .getSingle(); + + if (drive.isPrivate) { + return Thumbnail( + data: await _getThumbnailData(fileDataTableItem: fileDataTableItem), + url: null); + } + + final urlString = + '${_arweaveService.client.api.gatewayUrl.origin}/raw/${fileDataTableItem.dataTxId}'; + + return Thumbnail(data: null, url: urlString); + } + + Future _getThumbnailData({ + FileDataTableItem? fileDataTableItem, + }) async { + final dataTx = await _arweaveService.getTransactionDetails( + fileDataTableItem!.dataTxId, + ); + + if (dataTx == null) { + throw Exception('Data transaction not found'); + } + + final driveKey = await _driveDao.getDriveKey( + fileDataTableItem.driveId, _arDriveAuth.currentUser.cipherKey); + + return await _arDriveDownloader.downloadToMemory( + dataTx: dataTx, + fileSize: fileDataTableItem.size!, + fileName: fileDataTableItem.name, + lastModifiedDate: fileDataTableItem.lastModifiedDate, + contentType: fileDataTableItem.contentType, + isManifest: false, + cipher: dataTx.getTag(EntityTag.cipher), + cipherIvString: dataTx.getTag(EntityTag.cipherIv), + fileKey: await _driveDao.getFileKey(fileDataTableItem.id, driveKey!), + ); + } +} diff --git a/lib/drive_explorer/thumbnail/thumbnail.dart b/lib/drive_explorer/thumbnail/thumbnail.dart new file mode 100644 index 0000000000..3bc7795ecc --- /dev/null +++ b/lib/drive_explorer/thumbnail/thumbnail.dart @@ -0,0 +1,8 @@ +import 'dart:typed_data'; + +class Thumbnail { + final Uint8List? data; + final String? url; + + Thumbnail({this.data, this.url}); +} diff --git a/lib/drive_explorer/thumbnail/thumbnail_bloc.dart b/lib/drive_explorer/thumbnail/thumbnail_bloc.dart new file mode 100644 index 0000000000..952c08f82e --- /dev/null +++ b/lib/drive_explorer/thumbnail/thumbnail_bloc.dart @@ -0,0 +1,31 @@ +import 'package:ardrive/drive_explorer/thumbnail/repository/thumbnail_repository.dart'; +import 'package:ardrive/drive_explorer/thumbnail/thumbnail.dart'; +import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'thumbnail_event.dart'; +part 'thumbnail_state.dart'; + +class ThumbnailBloc extends Bloc { + final ThumbnailRepository _thumbnailRepository; + + ThumbnailBloc({ + required ThumbnailRepository thumbnailRepository, + }) : _thumbnailRepository = thumbnailRepository, + super(ThumbnailInitial()) { + on((event, emit) async { + emit(ThumbnailLoading()); + + try { + final thumbnail = await _thumbnailRepository.getThumbnail( + fileDataTableItem: event.fileDataTableItem, + ); + + emit(ThumbnailLoaded(thumbnail: thumbnail)); + } catch (e) { + emit(ThumbnailError()); + } + }); + } +} diff --git a/lib/drive_explorer/thumbnail/thumbnail_event.dart b/lib/drive_explorer/thumbnail/thumbnail_event.dart new file mode 100644 index 0000000000..96633a501a --- /dev/null +++ b/lib/drive_explorer/thumbnail/thumbnail_event.dart @@ -0,0 +1,17 @@ +part of 'thumbnail_bloc.dart'; + +sealed class ThumbnailEvent extends Equatable { + const ThumbnailEvent(); + + @override + List get props => []; +} + +final class GetThumbnail extends ThumbnailEvent { + final FileDataTableItem fileDataTableItem; + + const GetThumbnail({required this.fileDataTableItem}); + + @override + List get props => [fileDataTableItem]; +} diff --git a/lib/drive_explorer/thumbnail/thumbnail_state.dart b/lib/drive_explorer/thumbnail/thumbnail_state.dart new file mode 100644 index 0000000000..7023d90b7e --- /dev/null +++ b/lib/drive_explorer/thumbnail/thumbnail_state.dart @@ -0,0 +1,23 @@ +part of 'thumbnail_bloc.dart'; + +sealed class ThumbnailState extends Equatable { + const ThumbnailState(); + + @override + List get props => []; +} + +final class ThumbnailInitial extends ThumbnailState {} + +final class ThumbnailLoading extends ThumbnailState {} + +final class ThumbnailLoaded extends ThumbnailState { + final Thumbnail thumbnail; + + const ThumbnailLoaded({required this.thumbnail}); + + @override + List get props => [thumbnail]; +} + +final class ThumbnailError extends ThumbnailState {} diff --git a/lib/entities/license_assertion.dart b/lib/entities/license_assertion.dart index 35eb05986b..a2c0d355a8 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))); 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 f21a3e83f5..052a6b4138 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -1,3 +1,4 @@ +import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/csv_export_dialog.dart'; @@ -6,16 +7,22 @@ import 'package:ardrive/components/fs_entry_license_form.dart'; import 'package:ardrive/components/ghost_fixer_form.dart'; import 'package:ardrive/components/hide_dialog.dart'; import 'package:ardrive/components/pin_indicator.dart'; +import 'package:ardrive/download/ardrive_downloader.dart'; import 'package:ardrive/download/multiple_file_download_modal.dart'; +import 'package:ardrive/drive_explorer/thumbnail/repository/thumbnail_repository.dart'; +import 'package:ardrive/drive_explorer/thumbnail/thumbnail_bloc.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/pages/drive_detail/components/dropdown_item.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; +import 'package:ardrive/services/arweave/arweave.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/file_type_helper.dart'; import 'package:ardrive/utils/size_constants.dart'; +import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:arweave/arweave.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -74,6 +81,91 @@ class DriveExplorerItemTileLeading extends StatelessWidget { } Widget _buildFileIcon(BuildContext context) { + if (item is FileDataTableItem && FileTypeHelper.isImage(item.contentType)) { + final file = item as FileDataTableItem; + final url = '${Arweave().api.gatewayUrl.origin}/raw/${file.dataTxId}'; + + return ArDriveCard( + width: 30, + height: 30, + elevation: 0, + contentPadding: EdgeInsets.zero, + content: Stack( + children: [ + BlocProvider( + create: (context) => ThumbnailBloc( + thumbnailRepository: ThumbnailRepository( + arDriveDownloader: ArDriveDownloader( + arweave: context.read(), + ardriveIo: ArDriveIO(), + ioFileAdapter: IOFileAdapter(), + ), + driveDao: context.read(), + arweaveService: context.read(), + arDriveAuth: context.read(), + ), + )..add( + GetThumbnail(fileDataTableItem: file), + ), + child: BlocBuilder( + builder: (context, state) { + if (state is ThumbnailLoading) { + return const SizedBox(); + } + + if (state is ThumbnailLoaded) { + if (state.thumbnail.url != null) { + return Align( + alignment: Alignment.center, + child: Image.network( + state.thumbnail.url!, + width: 30, + height: 30, + filterQuality: FilterQuality.low, + fit: BoxFit.cover, + ), + ); + } + + return Align( + alignment: Alignment.center, + child: Image.memory( + state.thumbnail.data!, + width: 30, + height: 30, + filterQuality: FilterQuality.low, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return getIconForContentType( + item.contentType, + ).copyWith( + color: isHidden ? Colors.grey : null, + ); + }, + ), + ); + } + + return getIconForContentType( + item.contentType, + ).copyWith( + color: isHidden ? Colors.grey : null, + ); + }, + ), + ), + if (item.fileStatusFromTransactions != null) + Positioned( + right: 3, + bottom: 3, + child: _buildFileStatus(context), + ), + ], + ), + backgroundColor: ArDriveTheme.of(context).themeData.backgroundColor, + ); + } + return ArDriveCard( width: 30, height: 30, From dffdac114ab988ac6fb440f5a9c293a23bb0f120 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 28 May 2024 19:31:05 -0300 Subject: [PATCH 02/34] thumbnail poc --- .../drive_detail/components/drive_explorer_item_tile.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 052a6b4138..5ec38f4cae 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -22,7 +22,6 @@ import 'package:ardrive/utils/file_type_helper.dart'; import 'package:ardrive/utils/size_constants.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; -import 'package:arweave/arweave.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -83,7 +82,7 @@ class DriveExplorerItemTileLeading extends StatelessWidget { Widget _buildFileIcon(BuildContext context) { if (item is FileDataTableItem && FileTypeHelper.isImage(item.contentType)) { final file = item as FileDataTableItem; - final url = '${Arweave().api.gatewayUrl.origin}/raw/${file.dataTxId}'; + // final url = '${Arweave().api.gatewayUrl.origin}/raw/${file.dataTxId}'; return ArDriveCard( width: 30, From e760435989b0e6f60d7a49994e0337defb8d8ec5 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 29 May 2024 08:54:00 -0300 Subject: [PATCH 03/34] file health check --- lib/blocs/profile/profile_cubit.dart | 4 +- lib/components/side_bar.dart | 19 +++ lib/dev_tools/app_dev_tools.dart | 12 ++ lib/dev_tools/drives_health_check.dart | 205 +++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 lib/dev_tools/drives_health_check.dart diff --git a/lib/blocs/profile/profile_cubit.dart b/lib/blocs/profile/profile_cubit.dart index d3a5b668f1..93ad3391ae 100644 --- a/lib/blocs/profile/profile_cubit.dart +++ b/lib/blocs/profile/profile_cubit.dart @@ -176,7 +176,9 @@ class ProfileCubit extends Cubit { final walletBalance = await Future.wait([ _arweave.getWalletBalance(walletAddress), _arweave.getPendingTxFees(walletAddress), - ]).then((res) => res[0] - res[1]); + ]).then((res) { + return res[0] - res[1]; + }); emit(profile.copyWith(walletBalance: walletBalance)); } diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index 083a6446e7..f0aa2cf5ee 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -5,6 +5,7 @@ import 'package:ardrive/components/app_version_widget.dart'; import 'package:ardrive/components/new_button/new_button.dart'; import 'package:ardrive/components/theme_switcher.dart'; import 'package:ardrive/dev_tools/app_dev_tools.dart'; +import 'package:ardrive/dev_tools/drives_health_check.dart'; import 'package:ardrive/main.dart'; import 'package:ardrive/misc/resources.dart'; import 'package:ardrive/models/models.dart'; @@ -144,6 +145,15 @@ class _AppSideBarState extends State { const SizedBox( height: 16, ), + ArDriveButtonNew( + text: 'Health Check', + typography: ArDriveTypographyNew.of(context), + onPressed: () { + showArDriveDialog(context, + content: ArDriveStandardModalNew( + content: DrivesHealthCheckModal())); + }, + ), Padding( padding: const EdgeInsets.only(left: 16.0), child: _exportLogsButton(), @@ -381,6 +391,15 @@ class _AppSideBarState extends State { const SizedBox( height: 16, ), + ArDriveButtonNew( + text: 'Health Check', + typography: ArDriveTypographyNew.of(context), + onPressed: () { + showArDriveDialog(context, + content: ArDriveStandardModalNew( + content: DrivesHealthCheckModal())); + }, + ), Align( alignment: Alignment.centerLeft, child: _exportLogsButton(), diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index f79540e54c..dcd3076e41 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/drives_health_check.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'; @@ -274,6 +276,15 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.button, ); + final ArDriveDevToolOption runHealthCheck = ArDriveDevToolOption( + name: 'Run Health Check', + value: '', + onChange: (value) { + + }, + type: ArDriveDevToolOptionType.button, + ); + final ArDriveDevToolOption resetOptions = ArDriveDevToolOption( name: 'Reset options', value: '', @@ -373,6 +384,7 @@ class AppConfigWindowManagerState extends State { ); final List options = [ + runHealthCheck, useTurboOption, useTurboPaymentOption, defaultTurboPaymentUrlOption, diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart new file mode 100644 index 0000000000..7eb15c2935 --- /dev/null +++ b/lib/dev_tools/drives_health_check.dart @@ -0,0 +1,205 @@ +import 'package:ardrive/download/ardrive_downloader.dart'; +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_io/ardrive_io.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DrivesHealthCheckModal extends StatefulWidget { + const DrivesHealthCheckModal({super.key}); + + @override + State createState() => _DrivesHealthCheckModalState(); +} + +class _DrivesHealthCheckModalState extends State { + List files = []; + List allFiles = []; + List statuses = []; + int currentIndex = 0; + + @override + initState() { + super.initState(); + + final driveDao = context.read(); + + driveDao.select(driveDao.fileEntries).get().then((files) { + allFiles = files; + loadFiles(context: context); + }); + } + + @override + Widget build(BuildContext context) { + if (files.isNotEmpty) { + return SizedBox( + height: 500, + width: 400, + child: ListView.builder( + itemCount: files.length, + addAutomaticKeepAlives: true, + itemBuilder: (context, index) { + final status = statuses[index]; + + return FileHealthCheckTile( + status: status, + onFinish: () async { + await Future.delayed(const Duration(milliseconds: 500)); + setState(() { + files.add(allFiles[currentIndex]); + currentIndex += 1; + }); + }, + ); + }), + ); + } + + return const Center( + child: CircularProgressIndicator(), + ); + } + + /// loads all files + Future loadFiles({ + required BuildContext context, + }) async { + if (currentIndex >= allFiles.length) { + return; + } + + files.addAll(allFiles.getRange(currentIndex, currentIndex + 10)); + + await Future.wait(files.map((file) async { + await checkHealth(file); + })); + + await Future.delayed(const Duration(milliseconds: 500)); + + currentIndex += 10; + + setState(() {}); + + logger.d('Loaded ${allFiles.length} files'); + + // ignore: use_build_context_synchronously + loadFiles(context: context); + } + + /// checks the health of the file + Future checkHealth(FileEntry file) async { + try { + final arweave = context.read(); + final ardriveDownloader = ArDriveDownloader( + ioFileAdapter: IOFileAdapter(), + ardriveIo: ArDriveIO(), + arweave: arweave); + final dataTxId = file.dataTxId; + + final dataTx = await arweave.getTransactionDetails(file.dataTxId); + await ardriveDownloader.downloadToMemory( + dataTx: dataTx!, + contentType: file.dataContentType!, + fileName: file.name, + fileSize: file.size, + isManifest: false, + lastModifiedDate: file.lastModifiedDate, + cipher: dataTx.getTag(EntityTag.cipher), + cipherIvString: dataTx.getTag(EntityTag.cipherIv), + ); + + statuses.add(FileHealthCheckStatus( + file: file, + isSuccess: true, + isFailed: false, + )); + + setState(() {}); + } catch (e) { + statuses.add(FileHealthCheckStatus( + file: file, + isSuccess: false, + isFailed: 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); + if (widget.status.isSuccess) { + return ListTile( + title: + Text(widget.status.file.name, style: typography.paragraphNormal()), + subtitle: Text( + 'Health check completed', + style: typography.paragraphNormal(), + ), + trailing: IconButton( + icon: const Icon(Icons.check), + onPressed: () {}, + ), + ); + } + + if (widget.status.isFailed) { + return ListTile( + title: + Text(widget.status.file.name, style: typography.paragraphNormal()), + subtitle: Text( + 'Health check failed', + style: typography.paragraphNormal(), + ), + trailing: IconButton( + icon: const Icon(Icons.close), + 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(), + ), + ); + } +} From 03787e826e4318c1f4c2b48aa123ee3ce4849982 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 29 May 2024 14:26:35 -0300 Subject: [PATCH 04/34] feat(drive health check) - add option to health check all drives --- lib/components/side_bar.dart | 19 -- lib/dev_tools/app_dev_tools.dart | 12 +- lib/dev_tools/drives_health_check.dart | 228 +++++++++++------- lib/pages/drive_detail/drive_detail_page.dart | 137 ++++++----- 4 files changed, 238 insertions(+), 158 deletions(-) diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index f0aa2cf5ee..083a6446e7 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -5,7 +5,6 @@ import 'package:ardrive/components/app_version_widget.dart'; import 'package:ardrive/components/new_button/new_button.dart'; import 'package:ardrive/components/theme_switcher.dart'; import 'package:ardrive/dev_tools/app_dev_tools.dart'; -import 'package:ardrive/dev_tools/drives_health_check.dart'; import 'package:ardrive/main.dart'; import 'package:ardrive/misc/resources.dart'; import 'package:ardrive/models/models.dart'; @@ -145,15 +144,6 @@ class _AppSideBarState extends State { const SizedBox( height: 16, ), - ArDriveButtonNew( - text: 'Health Check', - typography: ArDriveTypographyNew.of(context), - onPressed: () { - showArDriveDialog(context, - content: ArDriveStandardModalNew( - content: DrivesHealthCheckModal())); - }, - ), Padding( padding: const EdgeInsets.only(left: 16.0), child: _exportLogsButton(), @@ -391,15 +381,6 @@ class _AppSideBarState extends State { const SizedBox( height: 16, ), - ArDriveButtonNew( - text: 'Health Check', - typography: ArDriveTypographyNew.of(context), - onPressed: () { - showArDriveDialog(context, - content: ArDriveStandardModalNew( - content: DrivesHealthCheckModal())); - }, - ), Align( alignment: Alignment.centerLeft, child: _exportLogsButton(), diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index dcd3076e41..f9e527f08f 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -4,6 +4,7 @@ import 'package:ardrive/dev_tools/drives_health_check.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/theme/theme.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'; @@ -280,7 +281,16 @@ class AppConfigWindowManagerState extends State { name: 'Run Health Check', value: '', onChange: (value) { - + final BuildContext context = ArDriveDevTools().context!; + + showArDriveDialog( + context, + content: const ArDriveStandardModalNew( + title: 'Health Check', + width: kLargeDialogWidth, + content: DrivesHealthCheckModal(), + ), + ); }, type: ArDriveDevToolOptionType.button, ); diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index 7eb15c2935..1a8e3d50f9 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -1,13 +1,14 @@ -import 'package:ardrive/download/ardrive_downloader.dart'; +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_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; -import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +// http +import 'package:http/http.dart' as http; class DrivesHealthCheckModal extends StatefulWidget { const DrivesHealthCheckModal({super.key}); @@ -17,10 +18,8 @@ class DrivesHealthCheckModal extends StatefulWidget { } class _DrivesHealthCheckModalState extends State { - List files = []; - List allFiles = []; List statuses = []; - int currentIndex = 0; + int numberOfFiles = 0; @override initState() { @@ -29,34 +28,74 @@ class _DrivesHealthCheckModalState extends State { final driveDao = context.read(); driveDao.select(driveDao.fileEntries).get().then((files) { - allFiles = files; - loadFiles(context: context); + numberOfFiles = files.length; + processFiles(files); }); } @override Widget build(BuildContext context) { - if (files.isNotEmpty) { - return SizedBox( - height: 500, - width: 400, - child: ListView.builder( - itemCount: files.length, - addAutomaticKeepAlives: true, - itemBuilder: (context, index) { - final status = statuses[index]; - - return FileHealthCheckTile( - status: status, - onFinish: () async { - await Future.delayed(const Duration(milliseconds: 500)); - setState(() { - files.add(allFiles[currentIndex]); - currentIndex += 1; - }); - }, - ); - }), + final typography = ArDriveTypographyNew.of(context); + + if (statuses.isNotEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 500, + child: ListView.builder( + itemCount: statuses.length, + addAutomaticKeepAlives: true, + shrinkWrap: true, + itemBuilder: (context, index) { + final status = statuses[index]; + + return FileHealthCheckTile( + status: status, + onFinish: () async {}, + ); + }), + ), + const SizedBox(height: 20), + Text( + 'Files Processed: ${statuses.length}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + Text( + 'Files Remaining: ${numberOfFiles - statuses.length}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + const Divider( + height: 20, + ), + Text( + 'Failed Files: ${statuses.where((status) => status.isFailed).length}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + Text( + 'Success Files: ${statuses.where((status) => status.isSuccess).length}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + ArDriveProgressBar(percentage: statuses.length / numberOfFiles), + if (statuses.length == numberOfFiles) + Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ArDriveButton( + text: 'Close', + onPressed: () => Navigator.of(context).pop()), + ), + ), + ], ); } @@ -65,53 +104,58 @@ class _DrivesHealthCheckModalState extends State { ); } - /// loads all files - Future loadFiles({ - required BuildContext context, - }) async { - if (currentIndex >= allFiles.length) { - return; - } - - files.addAll(allFiles.getRange(currentIndex, currentIndex + 10)); - - await Future.wait(files.map((file) async { - await checkHealth(file); - })); + Future processFiles(List files) async { + const int maxConcurrentTasks = 20; + final StreamController controller = StreamController(); - await Future.delayed(const Duration(milliseconds: 500)); - - currentIndex += 10; - - setState(() {}); + // Function to process files + void processNext() { + if (files.isNotEmpty) { + final file = files.removeAt(0); + checkHealth(file).then((_) { + controller.add(null); + setState(() {}); + }); + } else { + controller.close(); + } + } - logger.d('Loaded ${allFiles.length} files'); + // Start initial batch of downloads + for (int i = 0; i < maxConcurrentTasks && files.isNotEmpty; i++) { + processNext(); + setState(() {}); + } - // ignore: use_build_context_synchronously - loadFiles(context: context); + // 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) async { try { final arweave = context.read(); - final ardriveDownloader = ArDriveDownloader( - ioFileAdapter: IOFileAdapter(), - ardriveIo: ArDriveIO(), - arweave: arweave); - final dataTxId = file.dataTxId; - - final dataTx = await arweave.getTransactionDetails(file.dataTxId); - await ardriveDownloader.downloadToMemory( - dataTx: dataTx!, - contentType: file.dataContentType!, - fileName: file.name, - fileSize: file.size, - isManifest: false, - lastModifiedDate: file.lastModifiedDate, - cipher: dataTx.getTag(EntityTag.cipher), - cipherIvString: dataTx.getTag(EntityTag.cipherIv), - ); + + 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) { + statuses.add(FileHealthCheckStatus( + file: file, + isSuccess: false, + isFailed: true, + )); + setState(() {}); + return; + } statuses.add(FileHealthCheckStatus( file: file, @@ -120,13 +164,7 @@ class _DrivesHealthCheckModalState extends State { )); setState(() {}); - } catch (e) { - statuses.add(FileHealthCheckStatus( - file: file, - isSuccess: false, - isFailed: true, - )); - } + } catch (e) {} } } @@ -162,16 +200,26 @@ class _FileHealthCheckTileState extends State { @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()), + title: Text(widget.status.file.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + color: colorTokens.textHigh, + )), subtitle: Text( 'Health check completed', - style: typography.paragraphNormal(), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textMid, + ), ), trailing: IconButton( - icon: const Icon(Icons.check), + color: colorTokens.textHigh, + icon: const Icon( + Icons.check, + ), onPressed: () {}, ), ); @@ -179,14 +227,25 @@ class _FileHealthCheckTileState extends State { if (widget.status.isFailed) { return ListTile( - title: - Text(widget.status.file.name, style: typography.paragraphNormal()), + title: Text( + widget.status.file.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + color: colorTokens.textHigh, + ), + ), subtitle: Text( 'Health check failed', - style: typography.paragraphNormal(), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textLow, + ), ), trailing: IconButton( - icon: const Icon(Icons.close), + icon: Icon( + Icons.close, + color: colorTokens.textHigh, + ), onPressed: () { widget.onFinish(); }, @@ -198,7 +257,10 @@ class _FileHealthCheckTileState extends State { title: Text(widget.status.file.name, style: typography.paragraphNormal()), subtitle: Text( 'Checking health of ${widget.status.file.name}', - style: typography.paragraphNormal(), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + color: colorTokens.buttonDisabled, + ), ), ); } 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, + ), ), ), ); From 834e2bb07d9e951cbf07be036fff4b00525957eb Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 29 May 2024 14:29:18 -0300 Subject: [PATCH 05/34] Update license_assertion.dart --- 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 a2c0d355a8..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 3d0b7f74e9c05fcc1d9f2c67e412429dab44e38f Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 29 May 2024 16:13:28 -0300 Subject: [PATCH 06/34] improve healt check feature --- lib/dev_tools/app_dev_tools.dart | 7 +- lib/dev_tools/drives_health_check.dart | 423 +++++++++++++++++++++---- 2 files changed, 357 insertions(+), 73 deletions(-) diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index f9e527f08f..69929c25fa 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -4,7 +4,6 @@ import 'package:ardrive/dev_tools/drives_health_check.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/theme/theme.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'; @@ -285,11 +284,7 @@ class AppConfigWindowManagerState extends State { showArDriveDialog( context, - content: const ArDriveStandardModalNew( - title: 'Health Check', - width: kLargeDialogWidth, - content: DrivesHealthCheckModal(), - ), + content: DrivesHealthCheckModal(), ); }, type: ArDriveDevToolOptionType.button, diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index 1a8e3d50f9..463965c947 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -7,7 +7,6 @@ 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'; -// http import 'package:http/http.dart' as http; class DrivesHealthCheckModal extends StatefulWidget { @@ -19,6 +18,8 @@ class DrivesHealthCheckModal extends StatefulWidget { class _DrivesHealthCheckModalState extends State { List statuses = []; + List driveStatuses = []; + List drives = []; int numberOfFiles = 0; @override @@ -27,75 +28,267 @@ class _DrivesHealthCheckModalState extends State { final driveDao = context.read(); - driveDao.select(driveDao.fileEntries).get().then((files) { - numberOfFiles = files.length; - processFiles(files); + 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 (statuses.isNotEmpty) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: 500, - child: ListView.builder( - itemCount: statuses.length, - addAutomaticKeepAlives: true, - shrinkWrap: true, - itemBuilder: (context, index) { - final status = statuses[index]; - - return FileHealthCheckTile( - status: status, - onFinish: () async {}, - ); - }), - ), - const SizedBox(height: 20), - Text( - 'Files Processed: ${statuses.length}', - style: typography.paragraphLarge( - fontWeight: ArFontWeight.bold, - ), + 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, ), - Text( - 'Files Remaining: ${numberOfFiles - statuses.length}', - style: typography.paragraphLarge( - fontWeight: ArFontWeight.bold, - ), - ), - const Divider( - height: 20, - ), - Text( - 'Failed Files: ${statuses.where((status) => status.isFailed).length}', - style: typography.paragraphLarge( - fontWeight: ArFontWeight.bold, - ), - ), - Text( - 'Success Files: ${statuses.where((status) => status.isSuccess).length}', - style: typography.paragraphLarge( - fontWeight: ArFontWeight.bold, - ), - ), - ArDriveProgressBar(percentage: statuses.length / numberOfFiles), - if (statuses.length == numberOfFiles) - Align( - alignment: Alignment.center, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ArDriveButton( - text: 'Close', - onPressed: () => Navigator.of(context).pop()), + 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(height: 20), + // Text( + // 'Files Processed: ${statuses.length}', + // style: typography.paragraphLarge( + // fontWeight: ArFontWeight.bold, + // ), + // ), + // Text( + // 'Files Remaining: ${numberOfFiles - statuses.length}', + // style: typography.paragraphLarge( + // fontWeight: ArFontWeight.bold, + // ), + // ), + // const Divider( + // height: 20, + // ), + // Text( + // 'Failed Files: ${statuses.where((status) => status.isFailed).length}', + // style: typography.paragraphLarge( + // fontWeight: ArFontWeight.bold, + // ), + // ), + // Text( + // 'Success Files: ${statuses.where((status) => status.isSuccess).length}', + // style: typography.paragraphLarge( + // fontWeight: ArFontWeight.bold, + // ), + // ), + // ArDriveProgressBar(percentage: statuses.length / numberOfFiles), + ), + const SizedBox(width: 20), + Flexible( + flex: 1, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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 {}, + ); + }); + }), + ), + ), + ), + ], + ), + ), + ], ), - ), - ], + SizedBox( + height: 8, + ), + Text( + 'Drives Loaded: ${driveStatuses.where((element) => !element.isLoading).length} of ${driveStatuses.length}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ), + ), + ], + ), + ), ); } @@ -104,7 +297,8 @@ class _DrivesHealthCheckModalState extends State { ); } - Future processFiles(List files) async { + Future processFiles( + List files, DriveHealthCheckStatus driveStatus) async { const int maxConcurrentTasks = 20; final StreamController controller = StreamController(); @@ -112,7 +306,7 @@ class _DrivesHealthCheckModalState extends State { void processNext() { if (files.isNotEmpty) { final file = files.removeAt(0); - checkHealth(file).then((_) { + checkHealth(file, driveStatus).then((_) { controller.add(null); setState(() {}); }); @@ -135,7 +329,8 @@ class _DrivesHealthCheckModalState extends State { } /// checks the health of the file - Future checkHealth(FileEntry file) async { + Future checkHealth( + FileEntry file, DriveHealthCheckStatus driveStatus) async { try { final arweave = context.read(); @@ -148,7 +343,7 @@ class _DrivesHealthCheckModalState extends State { 'Checking health of ${file.name}. Response: ${response.statusCode}'); if (response.statusCode > 400) { - statuses.add(FileHealthCheckStatus( + driveStatus.files.add(FileHealthCheckStatus( file: file, isSuccess: false, isFailed: true, @@ -157,17 +352,111 @@ class _DrivesHealthCheckModalState extends State { return; } - statuses.add(FileHealthCheckStatus( + driveStatus.files.add(FileHealthCheckStatus( file: file, isSuccess: true, isFailed: false, )); setState(() {}); - } catch (e) {} + } 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; From 8f8c2186e6f965acd4d3d4260cd627cfca452f0a Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 30 May 2024 10:06:02 -0300 Subject: [PATCH 07/34] fix lint warnings --- lib/dev_tools/app_dev_tools.dart | 2 +- lib/dev_tools/drives_health_check.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index 69929c25fa..dafab51b0c 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -284,7 +284,7 @@ class AppConfigWindowManagerState extends State { showArDriveDialog( context, - content: DrivesHealthCheckModal(), + content: const DrivesHealthCheckModal(), ); }, type: ArDriveDevToolOptionType.button, diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index 463965c947..ec97a04b05 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -183,7 +183,7 @@ class _DrivesHealthCheckModalState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( + const SizedBox( height: 8, ), Text( @@ -277,7 +277,7 @@ class _DrivesHealthCheckModalState extends State { ), ], ), - SizedBox( + const SizedBox( height: 8, ), Text( From df8d6690662d7cb96d80d9fa61cda9a1fe4fc170 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 30 May 2024 10:09:40 -0300 Subject: [PATCH 08/34] remove commented code --- lib/dev_tools/drives_health_check.dart | 29 -------------------------- 1 file changed, 29 deletions(-) diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index ec97a04b05..bcbb2cf722 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -146,35 +146,6 @@ class _DrivesHealthCheckModalState extends State { ], ), ), - // const SizedBox(height: 20), - // Text( - // 'Files Processed: ${statuses.length}', - // style: typography.paragraphLarge( - // fontWeight: ArFontWeight.bold, - // ), - // ), - // Text( - // 'Files Remaining: ${numberOfFiles - statuses.length}', - // style: typography.paragraphLarge( - // fontWeight: ArFontWeight.bold, - // ), - // ), - // const Divider( - // height: 20, - // ), - // Text( - // 'Failed Files: ${statuses.where((status) => status.isFailed).length}', - // style: typography.paragraphLarge( - // fontWeight: ArFontWeight.bold, - // ), - // ), - // Text( - // 'Success Files: ${statuses.where((status) => status.isSuccess).length}', - // style: typography.paragraphLarge( - // fontWeight: ArFontWeight.bold, - // ), - // ), - // ArDriveProgressBar(percentage: statuses.length / numberOfFiles), ), const SizedBox(width: 20), Flexible( From a1492f53f55901f30d96540106adac91c68a375f Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 30 May 2024 14:58:18 -0300 Subject: [PATCH 09/34] Revert "thumbnail poc" This reverts commit dffdac114ab988ac6fb440f5a9c293a23bb0f120. --- .../drive_detail/components/drive_explorer_item_tile.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..052a6b4138 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -22,6 +22,7 @@ import 'package:ardrive/utils/file_type_helper.dart'; import 'package:ardrive/utils/size_constants.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:arweave/arweave.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -82,7 +83,7 @@ class DriveExplorerItemTileLeading extends StatelessWidget { Widget _buildFileIcon(BuildContext context) { if (item is FileDataTableItem && FileTypeHelper.isImage(item.contentType)) { final file = item as FileDataTableItem; - // final url = '${Arweave().api.gatewayUrl.origin}/raw/${file.dataTxId}'; + final url = '${Arweave().api.gatewayUrl.origin}/raw/${file.dataTxId}'; return ArDriveCard( width: 30, From 550aa0484e25d56d0a77ff1b365569d480fa7b8e Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 30 May 2024 14:58:28 -0300 Subject: [PATCH 10/34] Revert "thumbnail poc" This reverts commit a8f3fd62150f8ce980ad8cc5a488fe7cf0cc52ba. --- lib/download/ardrive_downloader.dart | 42 --------- .../repository/thumbnail_repository.dart | 73 --------------- lib/drive_explorer/thumbnail/thumbnail.dart | 8 -- .../thumbnail/thumbnail_bloc.dart | 31 ------- .../thumbnail/thumbnail_event.dart | 17 ---- .../thumbnail/thumbnail_state.dart | 23 ----- .../components/drive_explorer_item_tile.dart | 92 ------------------- 7 files changed, 286 deletions(-) delete mode 100644 lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart delete mode 100644 lib/drive_explorer/thumbnail/thumbnail.dart delete mode 100644 lib/drive_explorer/thumbnail/thumbnail_bloc.dart delete mode 100644 lib/drive_explorer/thumbnail/thumbnail_event.dart delete mode 100644 lib/drive_explorer/thumbnail/thumbnail_state.dart diff --git a/lib/download/ardrive_downloader.dart b/lib/download/ardrive_downloader.dart index e17804a71c..f6b99f6018 100644 --- a/lib/download/ardrive_downloader.dart +++ b/lib/download/ardrive_downloader.dart @@ -26,19 +26,6 @@ abstract class ArDriveDownloader { String? cipherIvString, }); - Future downloadToMemory({ - required TransactionCommonMixin dataTx, - required int fileSize, - required String fileName, - required DateTime lastModifiedDate, - required String contentType, - required bool isManifest, - Completer? cancelWithReason, - SecretKey? fileKey, - String? cipher, - String? cipherIvString, - }); - Future abortDownload(); factory ArDriveDownloader({ @@ -285,33 +272,4 @@ class _ArDriveDownloader implements ArDriveDownloader { return; } - - @override - Future downloadToMemory({ - required TransactionCommonMixin dataTx, - required int fileSize, - required String fileName, - required DateTime lastModifiedDate, - required String contentType, - required bool isManifest, - Completer? cancelWithReason, - SecretKey? fileKey, - String? cipher, - String? cipherIvString, - }) async { - final stream = await _getFileStream( - dataTx: dataTx, - fileSize: fileSize, - fileName: fileName, - lastModifiedDate: lastModifiedDate, - contentType: contentType, - fileKey: fileKey, - cipher: cipher, - cipherIvString: cipherIvString, - ); - - final data = await stream.toList(); - - return Uint8List.fromList(data.expand((element) => element).toList()); - } } diff --git a/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart b/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart deleted file mode 100644 index e167da2ad3..0000000000 --- a/lib/drive_explorer/thumbnail/repository/thumbnail_repository.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:typed_data'; - -import 'package:ardrive/authentication/ardrive_auth.dart'; -import 'package:ardrive/download/ardrive_downloader.dart'; -import 'package:ardrive/drive_explorer/thumbnail/thumbnail.dart'; -import 'package:ardrive/models/models.dart'; -import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; -import 'package:ardrive/services/arweave/arweave.dart'; -import 'package:ardrive_utils/ardrive_utils.dart'; - -class ThumbnailRepository { - final ArweaveService _arweaveService; - final ArDriveDownloader _arDriveDownloader; - final DriveDao _driveDao; - final ArDriveAuth _arDriveAuth; - - ThumbnailRepository({ - required ArweaveService arweaveService, - required ArDriveDownloader arDriveDownloader, - required DriveDao driveDao, - required ArDriveAuth arDriveAuth, - }) : _driveDao = driveDao, - _arDriveDownloader = arDriveDownloader, - _arweaveService = arweaveService, - _arDriveAuth = arDriveAuth; - - Future getThumbnail({ - FileDataTableItem? fileDataTableItem, - bool returnData = false, - }) async { - final drive = await _driveDao - .driveById(driveId: fileDataTableItem!.driveId) - .getSingle(); - - if (drive.isPrivate) { - return Thumbnail( - data: await _getThumbnailData(fileDataTableItem: fileDataTableItem), - url: null); - } - - final urlString = - '${_arweaveService.client.api.gatewayUrl.origin}/raw/${fileDataTableItem.dataTxId}'; - - return Thumbnail(data: null, url: urlString); - } - - Future _getThumbnailData({ - FileDataTableItem? fileDataTableItem, - }) async { - final dataTx = await _arweaveService.getTransactionDetails( - fileDataTableItem!.dataTxId, - ); - - if (dataTx == null) { - throw Exception('Data transaction not found'); - } - - final driveKey = await _driveDao.getDriveKey( - fileDataTableItem.driveId, _arDriveAuth.currentUser.cipherKey); - - return await _arDriveDownloader.downloadToMemory( - dataTx: dataTx, - fileSize: fileDataTableItem.size!, - fileName: fileDataTableItem.name, - lastModifiedDate: fileDataTableItem.lastModifiedDate, - contentType: fileDataTableItem.contentType, - isManifest: false, - cipher: dataTx.getTag(EntityTag.cipher), - cipherIvString: dataTx.getTag(EntityTag.cipherIv), - fileKey: await _driveDao.getFileKey(fileDataTableItem.id, driveKey!), - ); - } -} diff --git a/lib/drive_explorer/thumbnail/thumbnail.dart b/lib/drive_explorer/thumbnail/thumbnail.dart deleted file mode 100644 index 3bc7795ecc..0000000000 --- a/lib/drive_explorer/thumbnail/thumbnail.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'dart:typed_data'; - -class Thumbnail { - final Uint8List? data; - final String? url; - - Thumbnail({this.data, this.url}); -} diff --git a/lib/drive_explorer/thumbnail/thumbnail_bloc.dart b/lib/drive_explorer/thumbnail/thumbnail_bloc.dart deleted file mode 100644 index 952c08f82e..0000000000 --- a/lib/drive_explorer/thumbnail/thumbnail_bloc.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:ardrive/drive_explorer/thumbnail/repository/thumbnail_repository.dart'; -import 'package:ardrive/drive_explorer/thumbnail/thumbnail.dart'; -import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -part 'thumbnail_event.dart'; -part 'thumbnail_state.dart'; - -class ThumbnailBloc extends Bloc { - final ThumbnailRepository _thumbnailRepository; - - ThumbnailBloc({ - required ThumbnailRepository thumbnailRepository, - }) : _thumbnailRepository = thumbnailRepository, - super(ThumbnailInitial()) { - on((event, emit) async { - emit(ThumbnailLoading()); - - try { - final thumbnail = await _thumbnailRepository.getThumbnail( - fileDataTableItem: event.fileDataTableItem, - ); - - emit(ThumbnailLoaded(thumbnail: thumbnail)); - } catch (e) { - emit(ThumbnailError()); - } - }); - } -} diff --git a/lib/drive_explorer/thumbnail/thumbnail_event.dart b/lib/drive_explorer/thumbnail/thumbnail_event.dart deleted file mode 100644 index 96633a501a..0000000000 --- a/lib/drive_explorer/thumbnail/thumbnail_event.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of 'thumbnail_bloc.dart'; - -sealed class ThumbnailEvent extends Equatable { - const ThumbnailEvent(); - - @override - List get props => []; -} - -final class GetThumbnail extends ThumbnailEvent { - final FileDataTableItem fileDataTableItem; - - const GetThumbnail({required this.fileDataTableItem}); - - @override - List get props => [fileDataTableItem]; -} diff --git a/lib/drive_explorer/thumbnail/thumbnail_state.dart b/lib/drive_explorer/thumbnail/thumbnail_state.dart deleted file mode 100644 index 7023d90b7e..0000000000 --- a/lib/drive_explorer/thumbnail/thumbnail_state.dart +++ /dev/null @@ -1,23 +0,0 @@ -part of 'thumbnail_bloc.dart'; - -sealed class ThumbnailState extends Equatable { - const ThumbnailState(); - - @override - List get props => []; -} - -final class ThumbnailInitial extends ThumbnailState {} - -final class ThumbnailLoading extends ThumbnailState {} - -final class ThumbnailLoaded extends ThumbnailState { - final Thumbnail thumbnail; - - const ThumbnailLoaded({required this.thumbnail}); - - @override - List get props => [thumbnail]; -} - -final class ThumbnailError extends ThumbnailState {} 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 052a6b4138..f21a3e83f5 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -1,4 +1,3 @@ -import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/csv_export_dialog.dart'; @@ -7,22 +6,16 @@ import 'package:ardrive/components/fs_entry_license_form.dart'; import 'package:ardrive/components/ghost_fixer_form.dart'; import 'package:ardrive/components/hide_dialog.dart'; import 'package:ardrive/components/pin_indicator.dart'; -import 'package:ardrive/download/ardrive_downloader.dart'; import 'package:ardrive/download/multiple_file_download_modal.dart'; -import 'package:ardrive/drive_explorer/thumbnail/repository/thumbnail_repository.dart'; -import 'package:ardrive/drive_explorer/thumbnail/thumbnail_bloc.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/congestion_warning_wrapper.dart'; import 'package:ardrive/pages/drive_detail/components/dropdown_item.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; -import 'package:ardrive/services/arweave/arweave.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/file_type_helper.dart'; import 'package:ardrive/utils/size_constants.dart'; -import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; -import 'package:arweave/arweave.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -81,91 +74,6 @@ class DriveExplorerItemTileLeading extends StatelessWidget { } Widget _buildFileIcon(BuildContext context) { - if (item is FileDataTableItem && FileTypeHelper.isImage(item.contentType)) { - final file = item as FileDataTableItem; - final url = '${Arweave().api.gatewayUrl.origin}/raw/${file.dataTxId}'; - - return ArDriveCard( - width: 30, - height: 30, - elevation: 0, - contentPadding: EdgeInsets.zero, - content: Stack( - children: [ - BlocProvider( - create: (context) => ThumbnailBloc( - thumbnailRepository: ThumbnailRepository( - arDriveDownloader: ArDriveDownloader( - arweave: context.read(), - ardriveIo: ArDriveIO(), - ioFileAdapter: IOFileAdapter(), - ), - driveDao: context.read(), - arweaveService: context.read(), - arDriveAuth: context.read(), - ), - )..add( - GetThumbnail(fileDataTableItem: file), - ), - child: BlocBuilder( - builder: (context, state) { - if (state is ThumbnailLoading) { - return const SizedBox(); - } - - if (state is ThumbnailLoaded) { - if (state.thumbnail.url != null) { - return Align( - alignment: Alignment.center, - child: Image.network( - state.thumbnail.url!, - width: 30, - height: 30, - filterQuality: FilterQuality.low, - fit: BoxFit.cover, - ), - ); - } - - return Align( - alignment: Alignment.center, - child: Image.memory( - state.thumbnail.data!, - width: 30, - height: 30, - filterQuality: FilterQuality.low, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return getIconForContentType( - item.contentType, - ).copyWith( - color: isHidden ? Colors.grey : null, - ); - }, - ), - ); - } - - return getIconForContentType( - item.contentType, - ).copyWith( - color: isHidden ? Colors.grey : null, - ); - }, - ), - ), - if (item.fileStatusFromTransactions != null) - Positioned( - right: 3, - bottom: 3, - child: _buildFileStatus(context), - ), - ], - ), - backgroundColor: ArDriveTheme.of(context).themeData.backgroundColor, - ); - } - return ArDriveCard( width: 30, height: 30, From 733cc5e74685eadcec57dc29ba95d3ac97543d79 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 30 May 2024 15:09:07 -0300 Subject: [PATCH 11/34] Update profile_cubit.dart --- lib/blocs/profile/profile_cubit.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/blocs/profile/profile_cubit.dart b/lib/blocs/profile/profile_cubit.dart index 93ad3391ae..d3a5b668f1 100644 --- a/lib/blocs/profile/profile_cubit.dart +++ b/lib/blocs/profile/profile_cubit.dart @@ -176,9 +176,7 @@ class ProfileCubit extends Cubit { final walletBalance = await Future.wait([ _arweave.getWalletBalance(walletAddress), _arweave.getPendingTxFees(walletAddress), - ]).then((res) { - return res[0] - res[1]; - }); + ]).then((res) => res[0] - res[1]); emit(profile.copyWith(walletBalance: walletBalance)); } 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 12/34] 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 13/34] 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 14/34] 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 15/34] 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 16/34] 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 70b811811c56727e99ba017740d9bf963f5be24b Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:37:11 -0300 Subject: [PATCH 17/34] Update drives_health_check.dart --- lib/dev_tools/drives_health_check.dart | 337 +++++++++++++------------ 1 file changed, 169 insertions(+), 168 deletions(-) diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index bcbb2cf722..f20b9f8386 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -33,11 +33,11 @@ class _DrivesHealthCheckModalState extends State { this.drives = drives; }); - processDrivesInSequency(); + processDrivesSequentially(); }); } - Future processDrivesInSequency() async { + Future processDrivesSequentially() async { final driveDao = context.read(); for (final drive in drives) { @@ -48,10 +48,10 @@ class _DrivesHealthCheckModalState extends State { ); driveStatuses.add(status); - - setState(() {}); } + setState(() {}); + for (final currentStatus in driveStatuses) { final files = await (driveDao.select(driveDao.fileEntries) ..where((tbl) => tbl.driveId.equals(currentStatus.drive.id))) @@ -67,6 +67,7 @@ class _DrivesHealthCheckModalState extends State { currentStatus.totalFiles = files.length; selectedDriveStatus = currentStatus; + setState(() {}); await processFiles(files, currentStatus); @@ -83,188 +84,188 @@ class _DrivesHealthCheckModalState extends State { 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, + if (driveStatuses.isEmpty) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + 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, - crossAxisAlignment: CrossAxisAlignment.start, 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, ), - Text( - 'Drive: ${selectedDriveStatus.drive.name}', - style: typography.paragraphLarge( - fontWeight: ArFontWeight.bold, - ), + 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, + ), + ), + ); + }), ), - Text('Success Files', - style: typography.paragraphLarge()), - const SizedBox( - height: 8, + ], + ), + ), + ), + const SizedBox(width: 20), + Flexible( + flex: 1, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8, + ), + Text( + 'Selected Drive: ${selectedDriveStatus.drive.name}', + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, ), - 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 {}, - ); - }); - }), + ), + Text('Success Files - ${selectedDriveStatus.drive.name}', + 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: 20, + ), + Text('Failed Files - ${selectedDriveStatus.drive.name}', + 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, ), + ], + ), + 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(), + ), ); } From dd32d719d925533e09231b0caba40152bdf3b492 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:01:21 -0300 Subject: [PATCH 18/34] Update drives_health_check.dart fixes adding events when stream is closed --- lib/dev_tools/drives_health_check.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index f20b9f8386..80796dd31b 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -279,11 +279,14 @@ class _DrivesHealthCheckModalState extends State { if (files.isNotEmpty) { final file = files.removeAt(0); checkHealth(file, driveStatus).then((_) { + if (files.isEmpty) { + controller.close(); + return; + } + controller.add(null); setState(() {}); }); - } else { - controller.close(); } } From 07e6ad8227202255a219ef0e15e9bb020259dbfc Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 5 Jun 2024 15:28:55 -0300 Subject: [PATCH 19/34] Update app_dev_tools.dart use new styles --- lib/dev_tools/app_dev_tools.dart | 195 ++++++++++++++++--------------- 1 file changed, 101 insertions(+), 94 deletions(-) diff --git a/lib/dev_tools/app_dev_tools.dart b/lib/dev_tools/app_dev_tools.dart index dafab51b0c..221954c37f 100644 --- a/lib/dev_tools/app_dev_tools.dart +++ b/lib/dev_tools/app_dev_tools.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:ardrive/dev_tools/drives_health_check.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'; @@ -358,23 +357,6 @@ class AppConfigWindowManagerState extends State { type: ArDriveDevToolOptionType.bool, ); - final ArDriveDevToolOption fakeTurboCredits = ArDriveDevToolOption( - name: 'fakeTurboCredits', - value: config.fakeTurboCredits, - onChange: (value) { - late AppConfig newConfig; - if (value == null) { - newConfig = config.copyWith(unsetFakeTurboCredits: true); - } else { - newConfig = config.copyWith(fakeTurboCredits: value); - } - setState(() { - configService.updateAppConfig(newConfig); - }); - }, - type: ArDriveDevToolOptionType.turboCredits, - ); - final ArDriveDevToolOption topUpDryRun = ArDriveDevToolOption( name: 'topUpDryRun', value: config.topUpDryRun, @@ -406,82 +388,99 @@ class AppConfigWindowManagerState extends State { autoSyncIntervalInSecondsOption, turboSetDefaultData, forceNoFreeThanksToTurbo, - fakeTurboCredits, topUpDryRun, reloadOption, resetOptions, ]; + final typography = ArDriveTypographyNew.of(context); + return DraggableWindow( windowTitle: _windowTitle, child: SingleChildScrollView( primary: true, child: Column( children: [ - const SizedBox(height: 16), + const SizedBox(height: 48), FutureBuilder( future: _readConfigsFromEnv(), builder: (context, snapshot) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ArDriveButton( - text: 'dev env', - onPressed: () { - setState(() { - _windowTitle.value = 'Reloading...'; - - configService.updateAppConfig( - AppConfig.fromJson(snapshot.data![0]), - ); - }); - - Future.delayed(const Duration(seconds: 1), () { - setState(() { - _windowTitle.value = 'Dev config'; - reloadPage(); - }); - }); - }, - ), - ArDriveButton( - text: 'staging env', - onPressed: () { - setState(() { - _windowTitle.value = 'Reloading...'; - - configService.updateAppConfig( - AppConfig.fromJson(snapshot.data![2]), - ); - }); - - Future.delayed(const Duration(seconds: 1), () { - setState(() { - _windowTitle.value = 'Staging config'; - reloadPage(); - }); - }); - }, - ), - ArDriveButton( - text: 'prod env', - onPressed: () { - setState(() { - _windowTitle.value = 'Reloading...'; - - configService.updateAppConfig( - AppConfig.fromJson(snapshot.data![1]), - ); - }); - - Future.delayed(const Duration(seconds: 1), () { - setState(() { - _windowTitle.value = 'Prod config'; - }); - }); - }, - ) - ], + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Flexible( + child: ArDriveButtonNew( + text: 'dev env', + typography: typography, + variant: ButtonVariant.primary, + onPressed: () { + setState(() { + _windowTitle.value = 'Reloading...'; + + configService.updateAppConfig( + AppConfig.fromJson(snapshot.data![0]), + ); + }); + + Future.delayed(const Duration(seconds: 1), () { + setState(() { + _windowTitle.value = 'Dev config'; + reloadPage(); + }); + }); + }, + ), + ), + const SizedBox(width: 16), + Flexible( + child: ArDriveButtonNew( + text: 'staging env', + variant: ButtonVariant.primary, + typography: typography, + onPressed: () { + setState(() { + _windowTitle.value = 'Reloading...'; + + configService.updateAppConfig( + AppConfig.fromJson(snapshot.data![2]), + ); + }); + + Future.delayed(const Duration(seconds: 1), () { + setState(() { + _windowTitle.value = 'Staging config'; + reloadPage(); + }); + }); + }, + ), + ), + const SizedBox(width: 16), + Flexible( + child: ArDriveButtonNew( + text: 'prod env', + variant: ButtonVariant.primary, + typography: typography, + onPressed: () { + setState(() { + _windowTitle.value = 'Reloading...'; + + configService.updateAppConfig( + AppConfig.fromJson(snapshot.data![1]), + ); + }); + + Future.delayed(const Duration(seconds: 1), () { + setState(() { + _windowTitle.value = 'Prod config'; + }); + }); + }, + ), + ) + ], + ), ); }), ListView.separated( @@ -518,7 +517,7 @@ class AppConfigWindowManagerState extends State { Widget buildOption(ArDriveDevToolOption option) { switch (option.type) { case ArDriveDevToolOptionType.text: - return ArDriveTextField( + return ArDriveTextFieldNew( label: option.name, initialValue: option.value, onFieldSubmitted: (value) { @@ -536,7 +535,7 @@ class AppConfigWindowManagerState extends State { }, ); case ArDriveDevToolOptionType.number: - return ArDriveTextField( + return ArDriveTextFieldNew( label: option.name, initialValue: option.value.toString(), onFieldSubmitted: (value) { @@ -547,7 +546,9 @@ class AppConfigWindowManagerState extends State { ); case ArDriveDevToolOptionType.button: - return ArDriveButton( + return ArDriveButtonNew( + variant: ButtonVariant.primary, + typography: ArDriveTypographyNew.of(context), text: option.name, onPressed: () { option.onChange(option.value); @@ -556,8 +557,9 @@ class AppConfigWindowManagerState extends State { ); case ArDriveDevToolOptionType.buttonTertiary: - return ArDriveButton( - style: ArDriveButtonStyle.tertiary, + return ArDriveButtonNew( + variant: ButtonVariant.outline, + typography: ArDriveTypographyNew.of(context), text: option.name, onPressed: () => option.onChange(option.value), ); @@ -689,16 +691,21 @@ class DraggableWindow extends HookWidget { ), Align( alignment: Alignment.topRight, - child: ArDriveIconButton( - icon: ArDriveIcons.closeCircle( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeBgCanvas, + child: Padding( + padding: const EdgeInsets.only(top: 20, right: 8), + child: ArDriveClickArea( + child: GestureDetector( + onTap: () { + ArDriveDevTools().closeDevTools(); + }, + child: ArDriveIcons.x( + color: ArDriveTheme.of(context) + .themeData + .colorTokens + .iconLow, + ), + ), ), - onPressed: () { - ArDriveDevTools().closeDevTools(); - }, ), ), ], From ece7e3acd91ad16f86c351ca90ee4f86226729c8 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:14:27 -0300 Subject: [PATCH 20/34] Update drives_health_check.dart add retry --- lib/dev_tools/drives_health_check.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index 80796dd31b..baeb045a50 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -8,6 +8,7 @@ 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; +import 'package:retry/retry.dart'; class DrivesHealthCheckModal extends StatefulWidget { const DrivesHealthCheckModal({super.key}); @@ -312,7 +313,7 @@ class _DrivesHealthCheckModalState extends State { final url = '${arweave.client.api.gatewayUrl.origin}/raw/${file.dataTxId}'; - final response = await http.head(Uri.parse(url)); + final response = await retry(() async => await http.head(Uri.parse(url))); logger.d( 'Checking health of ${file.name}. Response: ${response.statusCode}'); @@ -345,8 +346,11 @@ class _DrivesHealthCheckModalState extends State { } class DriveHealthCheckTile extends StatefulWidget { - const DriveHealthCheckTile( - {super.key, required this.status, this.isSelected = false}); + const DriveHealthCheckTile({ + super.key, + required this.status, + this.isSelected = false, + }); final DriveHealthCheckStatus status; final bool isSelected; From 4128738a3f7699d42f3bc8ffbe7e2dae1cbff401 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:34:14 -0300 Subject: [PATCH 21/34] Update drives_health_check.dart increase timeout --- lib/dev_tools/drives_health_check.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/dev_tools/drives_health_check.dart b/lib/dev_tools/drives_health_check.dart index baeb045a50..50d7551432 100644 --- a/lib/dev_tools/drives_health_check.dart +++ b/lib/dev_tools/drives_health_check.dart @@ -313,14 +313,15 @@ class _DrivesHealthCheckModalState extends State { final url = '${arweave.client.api.gatewayUrl.origin}/raw/${file.dataTxId}'; - final response = await retry(() async => await http.head(Uri.parse(url))); + final response = await retry(() async => await http.head(Uri.parse(url)), + maxDelay: const Duration(seconds: 300)); logger.d( 'Checking health of ${file.name}. Response: ${response.statusCode}'); if (response.statusCode > 400) { driveStatus.files.add(FileHealthCheckStatus( - file: file, + file: file, isSuccess: false, isFailed: true, )); @@ -336,6 +337,7 @@ class _DrivesHealthCheckModalState extends State { setState(() {}); } catch (e) { + logger.d('Error checking health of ${file.name}. Error: $e'); driveStatus.files.add(FileHealthCheckStatus( file: file, isSuccess: false, 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 22/34] 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 @@ + + +