diff --git a/lib/app_shell.dart b/lib/app_shell.dart index 3550d09c06..47b5edac53 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -93,6 +93,8 @@ class AppShellState extends State { ), BlocBuilder( builder: (context, state) { + final typography = + ArDriveTypographyNew.of(context); return FutureBuilder( future: context .read() @@ -106,22 +108,29 @@ class AppShellState extends State { child: Material( borderRadius: BorderRadius.circular(8), child: ProgressDialog( + useNewArDriveUI: true, progressBar: ProgressBar( percentage: context .read() .syncProgressController .stream, ), - percentageDetails: _syncStreamBuilder( - builderWithData: (syncProgress) => - Text(appLocalizationsOf( - context) - .syncProgressPercentage( - (syncProgress - .progress * - 100) - .roundToDouble() - .toString()))), + percentageDetails: + _syncStreamBuilder( + builderWithData: (syncProgress) => + Text( + appLocalizationsOf(context) + .syncProgressPercentage( + (syncProgress.progress * 100) + .roundToDouble() + .toString(), + ), + style: + typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + ), + ), + ), progressDescription: _syncStreamBuilder( builderWithData: (syncProgress) => @@ -140,8 +149,10 @@ class AppShellState extends State { : appLocalizationsOf( context) .syncingOnlyOneDrive, - style: ArDriveTypography.body - .buttonNormalBold(), + style: + typography.paragraphNormal( + fontWeight: ArFontWeight.bold, + ), ), ), title: isCurrentProfileArConnect diff --git a/lib/blocs/create_manifest/create_manifest_cubit.dart b/lib/blocs/create_manifest/create_manifest_cubit.dart index 215e804f5f..23b7c9f8cb 100644 --- a/lib/blocs/create_manifest/create_manifest_cubit.dart +++ b/lib/blocs/create_manifest/create_manifest_cubit.dart @@ -1,80 +1,80 @@ import 'dart:async'; +import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; -import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/blocs/upload/models/payment_method_info.dart'; import 'package:ardrive/core/arfs/repository/folder_repository.dart'; -import 'package:ardrive/entities/entities.dart'; -import 'package:ardrive/entities/manifest_data.dart'; +import 'package:ardrive/manifest/domain/manifest_repository.dart'; import 'package:ardrive/models/models.dart'; -import 'package:ardrive/services/services.dart'; -import 'package:ardrive/turbo/services/upload_service.dart'; import 'package:ardrive/utils/logger.dart'; +import 'package:ardrive_io/ardrive_io.dart'; +import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; -import 'package:arweave/utils.dart'; -import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:pst/pst.dart'; -import 'package:uuid/uuid.dart'; - -import '../../core/upload/cost_calculator.dart'; part 'create_manifest_state.dart'; +// TODO: Add tests for CreateManifestCubit class CreateManifestCubit extends Cubit { late FolderNode rootFolderNode; final ProfileCubit _profileCubit; - final Drive drive; - - final ArweaveService _arweave; - final TurboUploadService _turboUploadService; - final DriveDao _driveDao; - final PstService _pst; + final Drive _drive; + final ManifestRepository _manifestRepository; final FolderRepository _folderRepository; - final FileRepository _fileRepository; bool _hasPendingFiles = false; StreamSubscription? _selectedFolderSubscription; + final ArDriveAuth _auth; + CreateManifestCubit({ - required this.drive, required ProfileCubit profileCubit, - required ArweaveService arweave, - required TurboUploadService turboUploadService, - required DriveDao driveDao, - required PstService pst, - required bool hasPendingFiles, - required FileRepository fileRepository, + required Drive drive, + required ManifestRepository manifestRepository, required FolderRepository folderRepository, - }) : _profileCubit = profileCubit, - _arweave = arweave, - _turboUploadService = turboUploadService, - _driveDao = driveDao, - _pst = pst, + required ArDriveAuth auth, + bool hasPendingFiles = false, + }) : _drive = drive, + _profileCubit = profileCubit, _hasPendingFiles = hasPendingFiles, - _fileRepository = fileRepository, + _manifestRepository = manifestRepository, _folderRepository = folderRepository, + _auth = auth, super(CreateManifestInitial()) { - if (drive.isPrivate) { + if (_drive.isPrivate) { // Extra guardrail to prevent private drives from creating manifests // Private manifests need more consideration and are currently unavailable emit(CreateManifestPrivacyMismatch()); } } + void selectUploadMethod( + UploadMethod method, UploadPaymentMethodInfo info, bool canUpload) { + if (state is CreateManifestUploadReview) { + emit( + (state as CreateManifestUploadReview).copyWith( + uploadMethod: method, + canUpload: canUpload, + freeUpload: info.isFreeThanksToTurbo, + ), + ); + } + } + /// Validate form before User begins choosing a target folder Future chooseTargetFolder() async { rootFolderNode = - await _driveDao.getFolderTree(drive.id, drive.rootFolderId); + await _folderRepository.getFolderNode(_drive.id, _drive.rootFolderId); _hasPendingFiles = await _hasPendingFilesInFolder(rootFolderNode); - await loadFolder(drive.rootFolderId); + await loadFolder(_drive.rootFolderId); } /// User has confirmed that they would like to submit a manifest revision transaction @@ -100,46 +100,16 @@ class CreateManifestCubit extends Cubit { /// recursively check if any files in the folder have pending uploads Future _hasPendingFilesInFolder(FolderNode folder) async { - final files = folder.getRecursiveFiles(); - final folders = folder.subfolders; - - if (files.isEmpty && folders.isEmpty) { - return false; - } - - final filesWithTx = await _driveDao - .filesInFolderWithLicenseAndRevisionTransactions( - driveId: drive.id, parentFolderId: folder.folder.id) - .get(); - - final hasPendingFiles = filesWithTx.any((e) => - 'pending' == - fileStatusFromTransactions( - e.metadataTx, - e.dataTx, - ).toString()); - - if (hasPendingFiles) { - return true; - } - - for (var folder in folders) { - if (await _hasPendingFilesInFolder(folder)) { - return true; - } - } - - return false; + return _manifestRepository.hasPendingFilesOnTargetFolder( + folderNode: folder, + ); } Future loadFolder(String folderId) async { await _selectedFolderSubscription?.cancel(); - _selectedFolderSubscription = _driveDao - .watchFolderContents( - drive.id, - folderId: folderId, - ) + _selectedFolderSubscription = _folderRepository + .watchFolderContents(driveId: _drive.id, folderId: folderId) .listen( (f) => emit( CreateManifestFolderLoadSuccess( @@ -177,33 +147,25 @@ class CreateManifestCubit extends Cubit { (state as CreateManifestCheckingForConflicts).parentFolder; await _selectedFolderSubscription?.cancel(); - final foldersWithName = await _driveDao - .foldersInFolderWithName( - driveId: drive.id, parentFolderId: parentFolder.id, name: name) - .get(); - final filesWithName = await _driveDao - .filesInFolderWithName( - driveId: drive.id, parentFolderId: parentFolder.id, name: name) - .get(); - - final conflictingFiles = - filesWithName.where((e) => e.dataContentType != ContentType.manifest); - - if (foldersWithName.isNotEmpty || conflictingFiles.isNotEmpty) { - // Name conflicts with existing file or folder - // This is an error case, send user back to naming the manifest - emit( - CreateManifestNameConflict( - conflictingName: name, - parentFolder: parentFolder, - ), - ); + final conflictTuple = + await _manifestRepository.checkNameConflictAndReturnExistingFileId( + driveId: _drive.id, + parentFolderId: parentFolder.id, + name: name, + ); + + final hasConflictNames = conflictTuple.$1; + final existingManifestFileId = conflictTuple.$2; + + if (hasConflictNames) { + emit(CreateManifestNameConflict( + conflictingName: name, + parentFolder: parentFolder, + )); return; } - final manifestRevisionId = filesWithName - .firstWhereOrNull((e) => e.dataContentType == ContentType.manifest) - ?.id; + final manifestRevisionId = existingManifestFileId; if (manifestRevisionId != null) { emit( @@ -214,7 +176,9 @@ class CreateManifestCubit extends Cubit { ); return; } + emit(CreateManifestPreparingManifest(parentFolder: parentFolder)); + await prepareManifestTx(manifestName: name); } @@ -226,123 +190,27 @@ class CreateManifestCubit extends Cubit { final parentFolder = (state as CreateManifestPreparingManifest).parentFolder; - final folderNode = rootFolderNode.searchForFolder(parentFolder.id) ?? - await _driveDao.getFolderTree(drive.id, parentFolder.id); - - final arweaveManifest = await ManifestData.fromFolderNode( - folderNode: folderNode, - fileRepository: _fileRepository, - folderRepository: _folderRepository, - ); - - final profile = _profileCubit.state as ProfileLoggedIn; - final wallet = profile.wallet; - final signer = ArweaveSigner(wallet); - - final manifestDataItem = await arweaveManifest.asPreparedDataItem( - owner: await wallet.getOwner(), - ); - await manifestDataItem.sign(signer); - - /// Assemble data JSON of the metadata tx for the manifest - final manifestFileEntity = FileEntity( - size: arweaveManifest.size, - parentFolderId: parentFolder.id, - name: manifestName, - lastModifiedDate: DateTime.now(), - id: existingManifestFileId ?? const Uuid().v4(), - driveId: drive.id, - dataTxId: manifestDataItem.id, - dataContentType: ContentType.manifest, - ); - - final manifestMetaDataItem = await _arweave.prepareEntityDataItem( - manifestFileEntity, - wallet, - ); - - // Sign data item and preserve meta data tx ID on entity - await manifestMetaDataItem.sign(signer); - manifestFileEntity.txId = manifestMetaDataItem.id; - - addManifestToDatabase() => _driveDao.transaction( - () async { - await _driveDao.writeFileEntity(manifestFileEntity); - await _driveDao.insertFileRevision( - manifestFileEntity.toRevisionCompanion( - performedAction: existingManifestFileId == null - ? RevisionAction.create - : RevisionAction.uploadNewVersion, - ), - ); - }, - ); - - logger.d('Manifest has pending files: $_hasPendingFiles'); - - final canUseTurbo = _turboUploadService.useTurboUpload && - arweaveManifest.size < _turboUploadService.allowedDataItemSize; - if (canUseTurbo) { - emit( - CreateManifestTurboUploadConfirmation( - manifestSize: arweaveManifest.size, - manifestName: manifestName, - folderHasPendingFiles: _hasPendingFiles, - manifestDataItems: [manifestDataItem, manifestMetaDataItem], - addManifestToDatabase: addManifestToDatabase, - ), - ); - return; - } - - final bundle = await DataBundle.fromDataItems( - items: [manifestDataItem, manifestMetaDataItem], - ); - - final bundleTx = await _arweave.prepareDataBundleTxFromBlob( - bundle.blob, - wallet, - ); - await _pst.addCommunityTipToTx(bundleTx); - - final totalCost = bundleTx.reward + bundleTx.quantity; - - if (profile.walletBalance < totalCost) { - emit( - CreateManifestInsufficientBalance( - walletBalance: winstonToAr(profile.walletBalance), - totalCost: winstonToAr(totalCost), - ), - ); - return; - } - - final arUploadCost = winstonToAr(totalCost); - - final double? usdUploadCost = await ConvertArToUSD(arweave: _arweave) - .convertForUSD(double.parse(arUploadCost)); - - // Sign bundle tx and preserve bundle tx ID on entity - await bundleTx.sign(ArweaveSigner(wallet)); - manifestFileEntity.bundledIn = bundleTx.id; - - final uploadManifestParams = UploadManifestParams( - signedBundleTx: bundleTx, - addManifestToDatabase: addManifestToDatabase, + final manifestFile = await _manifestRepository.getManifestFile( + parentFolder: parentFolder, + manifestName: manifestName, + rootFolderNode: rootFolderNode, + driveId: _drive.id, ); emit( - CreateManifestUploadConfirmation( - manifestSize: arweaveManifest.size, + CreateManifestUploadReview( + manifestSize: await manifestFile.length, manifestName: manifestName, folderHasPendingFiles: _hasPendingFiles, - arUploadCost: arUploadCost, - usdUploadCost: usdUploadCost, - uploadManifestParams: uploadManifestParams, + manifestFile: manifestFile, + drive: _drive, + parentFolder: parentFolder, + existingManifestFileId: existingManifestFileId, ), ); - } catch (err) { - addError(err); + } catch (e) { + logger.e('Failed to prepare manifest file', e); + addError(e); } } @@ -351,41 +219,34 @@ class CreateManifestCubit extends Cubit { emit(CreateManifestWalletMismatch()); return; } - if (state is CreateManifestTurboUploadConfirmation) { - final params = state as CreateManifestTurboUploadConfirmation; - emit(CreateManifestUploadInProgress()); - try { - for (var dataItem in params.manifestDataItems) { - await _turboUploadService.postDataItem( - dataItem: dataItem, - wallet: (_profileCubit.state as ProfileLoggedIn).wallet, - ); - } - await params.addManifestToDatabase(); + if (state is CreateManifestUploadReview) { + try { + final createManifestUploadReview = state as CreateManifestUploadReview; + final uploadType = + createManifestUploadReview.uploadMethod == UploadMethod.ar + ? UploadType.d2n + : UploadType.turbo; + + emit(CreateManifestUploadInProgress()); + + await _manifestRepository.uploadManifest( + params: ManifestUploadParams( + manifestFile: createManifestUploadReview.manifestFile, + driveId: _drive.id, + parentFolderId: createManifestUploadReview.parentFolder.id, + existingManifestFileId: + createManifestUploadReview.existingManifestFileId, + uploadType: uploadType, + wallet: _auth.currentUser.wallet, + ), + ); emit(CreateManifestSuccess()); - } catch (err) { - addError(err); + } catch (e) { + logger.e('An error occured uploading the manifest.', e); + addError(e); } - return; - } - final params = - (state as CreateManifestUploadConfirmation).uploadManifestParams; - - emit(CreateManifestUploadInProgress()); - try { - await _arweave.client.transactions - .upload( - params.signedBundleTx, - maxConcurrentUploadCount: maxConcurrentUploadCount, - ) - .drain(); - await params.addManifestToDatabase(); - - emit(CreateManifestSuccess()); - } catch (err) { - addError(err); } } diff --git a/lib/blocs/create_manifest/create_manifest_state.dart b/lib/blocs/create_manifest/create_manifest_state.dart index d80bb1a6eb..ce8e892b32 100644 --- a/lib/blocs/create_manifest/create_manifest_state.dart +++ b/lib/blocs/create_manifest/create_manifest_state.dart @@ -73,74 +73,71 @@ class CreateManifestPreparingManifest extends CreateManifestState { List get props => [parentFolder]; } -/// User does not have enough AR to cover the manifest transaction reward and tip, create manifest must be aborted -class CreateManifestInsufficientBalance extends CreateManifestState { - final String walletBalance; - final String totalCost; - - CreateManifestInsufficientBalance({ - required this.walletBalance, - required this.totalCost, - }); - - @override - List get props => [walletBalance, totalCost]; -} - -/// Manifest transaction is prepared, prompt user to confirm price of the upload -class CreateManifestUploadConfirmation extends CreateManifestState { +class CreateManifestUploadReview extends CreateManifestState { final int manifestSize; final String manifestName; final bool folderHasPendingFiles; - final String arUploadCost; - final double? usdUploadCost; - - final UploadManifestParams uploadManifestParams; + final IOFile manifestFile; + final bool freeUpload; + final UploadMethod? uploadMethod; + final Drive drive; + final FolderEntry parentFolder; + final String? existingManifestFileId; + final bool canUpload; - CreateManifestUploadConfirmation({ + CreateManifestUploadReview({ required this.manifestSize, required this.manifestName, required this.folderHasPendingFiles, - required this.arUploadCost, - required this.usdUploadCost, - required this.uploadManifestParams, + required this.manifestFile, + this.freeUpload = false, + this.uploadMethod, + required this.drive, + required this.parentFolder, + this.existingManifestFileId, + this.canUpload = false, }); @override List get props => [ manifestSize, manifestName, + manifestFile, folderHasPendingFiles, - arUploadCost, - usdUploadCost, - uploadManifestParams, + freeUpload, + uploadMethod, + drive, + parentFolder, + existingManifestFileId, ]; -} -/// Manifest transaction is prepared, prompt user to confirm price of the upload -class CreateManifestTurboUploadConfirmation extends CreateManifestState { - final int manifestSize; - final String manifestName; - final bool folderHasPendingFiles; - final List manifestDataItems; - final Future Function() addManifestToDatabase; - - CreateManifestTurboUploadConfirmation({ - required this.manifestSize, - required this.manifestName, - required this.folderHasPendingFiles, - required this.manifestDataItems, - required this.addManifestToDatabase, - }); - - @override - List get props => [ - manifestSize, - manifestName, - folderHasPendingFiles, - manifestDataItems, - addManifestToDatabase, - ]; + CreateManifestUploadReview copyWith({ + int? manifestSize, + String? manifestName, + bool? folderHasPendingFiles, + IOFile? manifestFile, + bool? freeUpload, + UploadMethod? uploadMethod, + Drive? drive, + FolderEntry? parentFolder, + String? existingManifestFileId, + bool? canUpload, + }) { + return CreateManifestUploadReview( + manifestSize: manifestSize ?? this.manifestSize, + manifestName: manifestName ?? this.manifestName, + folderHasPendingFiles: + folderHasPendingFiles ?? this.folderHasPendingFiles, + manifestFile: manifestFile ?? this.manifestFile, + freeUpload: freeUpload ?? this.freeUpload, + uploadMethod: uploadMethod ?? this.uploadMethod, + drive: drive ?? this.drive, + parentFolder: parentFolder ?? this.parentFolder, + existingManifestFileId: + existingManifestFileId ?? this.existingManifestFileId, + canUpload: canUpload ?? this.canUpload, + ); + } } /// User has confirmed the upload and the manifest transaction upload has started diff --git a/lib/blocs/upload/payment_method/view/upload_payment_method_view.dart b/lib/blocs/upload/payment_method/view/upload_payment_method_view.dart index 466591104b..2be1dfe339 100644 --- a/lib/blocs/upload/payment_method/view/upload_payment_method_view.dart +++ b/lib/blocs/upload/payment_method/view/upload_payment_method_view.dart @@ -2,7 +2,6 @@ import 'package:ardrive/blocs/upload/models/payment_method_info.dart'; import 'package:ardrive/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart'; import 'package:ardrive/blocs/upload/upload_cubit.dart'; import 'package:ardrive/components/payment_method_selector_widget.dart'; -import 'package:ardrive/core/upload/uploader.dart'; import 'package:ardrive/utils/logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,19 +9,19 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class UploadPaymentMethodView extends StatelessWidget { const UploadPaymentMethodView({ super.key, - required this.params, required this.onUploadMethodChanged, required this.onError, this.onTurboTopupSucess, this.loadingIndicator, + this.useNewArDriveUI = false, }); final Function(UploadMethod, UploadPaymentMethodInfo, bool) onUploadMethodChanged; final Function() onError; final Function()? onTurboTopupSucess; - final UploadParams params; final Widget? loadingIndicator; + final bool useNewArDriveUI; @override Widget build(BuildContext context) { @@ -43,20 +42,21 @@ class UploadPaymentMethodView extends StatelessWidget { builder: (context, state) { if (state is UploadPaymentMethodLoaded) { return PaymentMethodSelector( + useNewArDriveUI: useNewArDriveUI, uploadMethodInfo: state.paymentMethodInfo, onArSelect: () { - context - .read() - .add(const ChangeUploadPaymentMethod( - paymentMethod: UploadMethod.ar, - )); + context.read().add( + const ChangeUploadPaymentMethod( + paymentMethod: UploadMethod.ar, + ), + ); }, onTurboSelect: () { - context - .read() - .add(const ChangeUploadPaymentMethod( - paymentMethod: UploadMethod.turbo, - )); + context.read().add( + const ChangeUploadPaymentMethod( + paymentMethod: UploadMethod.turbo, + ), + ); }, onTurboTopupSucess: () { onTurboTopupSucess?.call(); diff --git a/lib/components/create_manifest_form.dart b/lib/components/create_manifest_form.dart index f203edf248..83ac96abd3 100644 --- a/lib/components/create_manifest_form.dart +++ b/lib/components/create_manifest_form.dart @@ -1,20 +1,33 @@ +import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_manifest/create_manifest_cubit.dart'; import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; +import 'package:ardrive/blocs/upload/models/upload_file.dart'; +import 'package:ardrive/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart'; +import 'package:ardrive/blocs/upload/payment_method/view/upload_payment_method_view.dart'; import 'package:ardrive/core/arfs/repository/file_repository.dart'; import 'package:ardrive/core/arfs/repository/folder_repository.dart'; -import 'package:ardrive/misc/misc.dart'; +import 'package:ardrive/core/arfs/utils/arfs_revision_status_utils.dart'; +import 'package:ardrive/core/crypto/crypto.dart'; +import 'package:ardrive/core/upload/cost_calculator.dart'; +import 'package:ardrive/core/upload/uploader.dart'; +import 'package:ardrive/entities/manifest_data.dart'; +import 'package:ardrive/manifest/domain/manifest_repository.dart'; +import 'package:ardrive/misc/resources.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/services/services.dart'; import 'package:ardrive/theme/theme.dart'; +import 'package:ardrive/turbo/services/payment_service.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; +import 'package:ardrive/turbo/turbo.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/filesize.dart'; import 'package:ardrive/utils/open_url.dart'; -import 'package:ardrive/utils/usd_upload_cost_to_string.dart'; +import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:ardrive/utils/validate_folder_name.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -29,6 +42,8 @@ Future promptToCreateManifest( required Drive drive, required bool hasPendingFiles, }) { + final pst = context.read(); + final configService = context.read(); return showArDriveDialog( context, content: BlocProvider( @@ -36,12 +51,28 @@ Future promptToCreateManifest( drive: drive, profileCubit: context.read(), hasPendingFiles: hasPendingFiles, - arweave: context.read(), - turboUploadService: context.read(), - driveDao: context.read(), - pst: context.read(), - fileRepository: context.read(), folderRepository: context.read(), + auth: context.read(), + manifestRepository: ManifestRepositoryImpl( + context.read(), + ArDriveUploader( + turboUploadUri: + Uri.parse(configService.config.defaultTurboUploadUrl!), + metadataGenerator: ARFSUploadMetadataGenerator( + tagsGenerator: ARFSTagsGenetator( + appInfoServices: AppInfoServices(), + ), + ), + arweave: context.read().client, + pstService: pst, + ), + context.read(), + ManifestDataBuilder( + fileRepository: context.read(), + folderRepository: context.read(), + ), + ARFSRevisionStatusUtils(context.read()), + ), ), child: const CreateManifestForm(), ), @@ -60,381 +91,460 @@ class _CreateManifestFormState extends State { bool _isFormValid = false; - @override - Widget build(BuildContext context) => - BlocConsumer( - listener: (context, state) { - if (state is CreateManifestUploadInProgress) { - showProgressDialog( - context, - title: appLocalizationsOf(context).uploadingManifestEmphasized, - ); - } else if (state is CreateManifestPreparingManifest) { - showProgressDialog( - context, - title: appLocalizationsOf(context).preparingManifestEmphasized, - ); - } else if (state is CreateManifestSuccess || - state is CreateManifestPrivacyMismatch) { - Navigator.pop(context); - Navigator.pop(context); - context.read().openRemindMe(); - } - }, builder: (context, state) { - final textStyle = ArDriveTypography.body.buttonNormalRegular( - color: ArDriveTheme.of(context).themeData.colors.themeFgDefault, - ); - - final readCubitContext = context.read(); + ArDriveTextFieldNew manifestNameForm() { + final readCubitContext = context.read(); - ArDriveTextField manifestNameForm() => ArDriveTextField( - hintText: appLocalizationsOf(context).manifestName, - controller: _manifestNameController, - validator: (value) { - final validation = validateEntityName(value, context); + return ArDriveTextFieldNew( + hintText: appLocalizationsOf(context).manifestName, + controller: _manifestNameController, + validator: (value) { + final validation = validateEntityName(value, context); - _isFormValid = validation == null; + _isFormValid = validation == null; - setState(() {}); + setState(() {}); - return validation; - }, - autofocus: true, - ); - - ArDriveStandardModal errorDialog({required String errorText}) => - ArDriveStandardModal( - width: kMediumDialogWidth, - title: - appLocalizationsOf(context).failedToCreateManifestEmphasized, - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - Text(errorText), - const SizedBox(height: 16), - ], - ), - actions: [ - ModalAction( - action: () => Navigator.pop(context), - title: appLocalizationsOf(context).continueEmphasized, - ), - ], - ); + return validation; + }, + autofocus: true, + onFieldSubmitted: (s) { + readCubitContext.chooseTargetFolder(); + }, + ); + } - if (state is CreateManifestWalletMismatch) { - Navigator.pop(context); - return errorDialog( - errorText: - appLocalizationsOf(context).walletChangedDuringManifestCreation, - ); - } + @override + Widget build(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - if (state is CreateManifestFailure) { - Navigator.pop(context); - return errorDialog( - errorText: appLocalizationsOf(context) - .manifestTransactionUnexpectedlyFailed, - ); - } - - if (state is CreateManifestInsufficientBalance) { - Navigator.pop(context); - return errorDialog( - errorText: appLocalizationsOf(context) - .insufficientBalanceForManifest( - state.walletBalance, state.totalCost), - ); - } + return BlocConsumer( + listener: (context, state) { + if (state is CreateManifestSuccess || + state is CreateManifestPrivacyMismatch) { + Navigator.pop(context); + context.read().openRemindMe(); + } + }, builder: (context, state) { + final textStyle = typography.paragraphNormal( + color: colorTokens.textLow, + fontWeight: ArFontWeight.semiBold, + ); - if (state is CreateManifestNameConflict) { - return ArDriveStandardModal( + ArDriveStandardModal errorDialog({required String errorText}) => + ArDriveStandardModal( width: kMediumDialogWidth, - title: appLocalizationsOf(context).conflictingNameFound, - content: SizedBox( - height: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - appLocalizationsOf(context) - .conflictingManifestFoundChooseNewName, - ), - manifestNameForm() - ], - ), - ), - actions: [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, - ), - ModalAction( - action: () => readCubitContext - .reCheckConflicts(_manifestNameController.text), - title: appLocalizationsOf(context).continueEmphasized, - ), - ], - ); - } - - if (state is CreateManifestRevisionConfirm) { - return ArDriveStandardModal( - width: kMediumDialogWidth, - title: appLocalizationsOf(context).conflictingManifestFound, + title: appLocalizationsOf(context).failedToCreateManifestEmphasized, content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), - Text( - appLocalizationsOf(context) - .conflictingManifestFoundChooseNewName, - ), + Text(errorText), const SizedBox(height: 16), ], ), actions: [ ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, - ), - ModalAction( - isEnable: _isFormValid, - action: () => readCubitContext - .confirmRevision(_manifestNameController.text), + action: () => Navigator.pop(context), title: appLocalizationsOf(context).continueEmphasized, ), ], ); - } - if (state is CreateManifestInitial) { - return ArDriveStandardModal( - width: kLargeDialogWidth, - title: appLocalizationsOf(context).addnewManifestEmphasized, - actions: [ - ModalAction( - action: () => Navigator.pop(context), - title: appLocalizationsOf(context).cancelEmphasized, + if (state is CreateManifestWalletMismatch) { + Navigator.pop(context); + return errorDialog( + errorText: + appLocalizationsOf(context).walletChangedDuringManifestCreation, + ); + } else if (state is CreateManifestFailure) { + Navigator.pop(context); + return errorDialog( + errorText: + appLocalizationsOf(context).manifestTransactionUnexpectedlyFailed, + ); + } else if (state is CreateManifestPreparingManifest) { + return ProgressDialog( + useNewArDriveUI: true, + title: appLocalizationsOf(context).preparingManifestEmphasized, + ); + } else if (state is CreateManifestNameConflict) { + return _createManifestNameConflict( + contexty: context, + textStyle: textStyle, + ); + } else if (state is CreateManifestRevisionConfirm) { + return _createManifestRevisionConfirm( + context: context, + textStyle: textStyle, + ); + } else if (state is CreateManifestInitial) { + return _createManifestInitial( + context: context, + textStyle: textStyle, + ); + } + if (state is CreateManifestUploadInProgress) { + return ProgressDialog( + useNewArDriveUI: true, + title: appLocalizationsOf(context).uploadingManifestEmphasized, + ); + } else if (state is CreateManifestUploadReview) { + return _createManifestUploadReview( + state: state, + context: context, + textStyle: textStyle, + ); + } + + if (state is CreateManifestFolderLoadSuccess) { + return _selectFolder(state, context); + } + + return const SizedBox(); + }); + } + + bool _isFolderEmpty(FolderID folderId, FolderNode rootFolderNode) { + final folderNode = rootFolderNode.searchForFolder(folderId); + + if (folderNode == null) { + return true; + } + + return folderNode.isEmpty(); + } + + Widget _createManifestNameConflict({ + required BuildContext contexty, + required TextStyle textStyle, + }) { + final readCubitContext = context.read(); + + return ArDriveStandardModalNew( + width: kMediumDialogWidth, + title: appLocalizationsOf(context).conflictingNameFound, + content: SizedBox( + height: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + appLocalizationsOf(context).conflictingManifestFoundChooseNewName, + style: textStyle, + ), + manifestNameForm() + ], + ), + ), + actions: [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).cancelEmphasized, + ), + ModalAction( + action: () => + readCubitContext.reCheckConflicts(_manifestNameController.text), + title: appLocalizationsOf(context).continueEmphasized, + ), + ], + ); + } + + Widget _createManifestRevisionConfirm({ + required BuildContext context, + required TextStyle textStyle, + }) { + final readCubitContext = context.read(); + return ArDriveStandardModalNew( + width: kMediumDialogWidth, + title: appLocalizationsOf(context).conflictingManifestFound, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + appLocalizationsOf(context).conflictingManifestFoundChooseNewName, + style: textStyle, + ), + const SizedBox(height: 16), + ], + ), + actions: [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).cancelEmphasized, + ), + ModalAction( + isEnable: _isFormValid, + action: () => + readCubitContext.confirmRevision(_manifestNameController.text), + title: appLocalizationsOf(context).continueEmphasized, + ), + ], + ); + } + + Widget _createManifestInitial({ + required BuildContext context, + required TextStyle textStyle, + }) { + final readCubitContext = context.read(); + return ArDriveStandardModalNew( + width: kLargeDialogWidth, + title: appLocalizationsOf(context).addnewManifestEmphasized, + actions: [ + ModalAction( + action: () => Navigator.pop(context), + title: appLocalizationsOf(context).cancelEmphasized, + ), + ModalAction( + isEnable: _isFormValid, + action: () => readCubitContext.chooseTargetFolder(), + title: appLocalizationsOf(context).nextEmphasized, + ), + ], + content: SizedBox( + height: 250, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + RichText( + text: TextSpan(children: [ + TextSpan( + text: appLocalizationsOf(context) + .aManifestIsASpecialKindOfFile, // trimmed spaces + style: textStyle, + ), + const TextSpan(text: ' '), + TextSpan( + text: appLocalizationsOf(context).learnMore, + style: textStyle.copyWith( + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => openUrl( + url: Resources.manifestLearnMoreLink, + ), + ), + ]), ), - ModalAction( - action: () => readCubitContext.chooseTargetFolder(), - title: appLocalizationsOf(context).nextEmphasized, + manifestNameForm() + ], + ), + ), + ), + ); + } + + Widget _createManifestUploadReview({ + required CreateManifestUploadReview state, + required BuildContext context, + required TextStyle textStyle, + }) { + final readCubitContext = context.read(); + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + + final hasPendingFiles = state.folderHasPendingFiles; + + return ArDriveStandardModalNew( + width: kMediumDialogWidth, + title: hasPendingFiles + ? appLocalizationsOf(context).filesPending + : appLocalizationsOf(context).createManifestEmphasized, + content: SizedBox( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasPendingFiles) ...[ + Text( + appLocalizationsOf(context).filesPendingManifestExplanation, + style: textStyle, ), + const Divider(), ], - content: SizedBox( - height: 250, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.center, + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 256), + child: Scrollbar( + child: ListView( + shrinkWrap: true, children: [ - RichText( - text: TextSpan(children: [ - TextSpan( - text: appLocalizationsOf(context) - .aManifestIsASpecialKindOfFile, // trimmed spaces - style: textStyle, - ), - const TextSpan(text: ' '), - TextSpan( - text: appLocalizationsOf(context).learnMore, - style: textStyle.copyWith( - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => openUrl( - url: Resources.manifestLearnMoreLink, - ), + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + state.manifestName, + style: typography.paragraphLarge( + color: colorTokens.textHigh, + fontWeight: ArFontWeight.bold, ), - ]), + ), + subtitle: Text( + filesize(state.manifestSize), + style: textStyle, + ), ), - manifestNameForm() ], ), ), ), - ); - } - if (state is CreateManifestTurboUploadConfirmation) { - final hasPendingFiles = state.folderHasPendingFiles; - - Navigator.pop(context); - return ArDriveStandardModal( - width: kMediumDialogWidth, - title: hasPendingFiles - ? appLocalizationsOf(context).filesPending - : appLocalizationsOf(context).createManifestEmphasized, - content: SizedBox( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (hasPendingFiles) ...[ - Text( - appLocalizationsOf(context) - .filesPendingManifestExplanation, - style: textStyle, - ), - const Divider(), - ], - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 256), - child: Scrollbar( - child: ListView( - shrinkWrap: true, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - state.manifestName, - style: textStyle, - ), - subtitle: Text( - filesize(state.manifestSize), - style: textStyle, + const Divider(), + const SizedBox(height: 16), + if (state.freeUpload) ...[ + Text( + appLocalizationsOf(context).freeTurboTransaction, + style: typography.paragraphNormal( + color: colorTokens.textMid, + fontWeight: ArFontWeight.bold, + ), + ), + const SizedBox(height: 24), + ], + if (!state.freeUpload) ...[ + MultiBlocProvider( + providers: [ + RepositoryProvider( + create: (context) => ArDriveUploadPreparationManager( + uploadPreparePaymentOptions: UploadPaymentEvaluator( + appConfig: context.read().config, + auth: context.read(), + turboBalanceRetriever: TurboBalanceRetriever( + paymentService: context.read(), + ), + turboUploadCostCalculator: TurboUploadCostCalculator( + priceEstimator: TurboPriceEstimator( + wallet: + context.read().currentUser.wallet, + costCalculator: TurboCostCalculator( + paymentService: context.read(), ), + paymentService: context.read(), ), - ], + turboCostCalculator: TurboCostCalculator( + paymentService: context.read(), + ), + ), + uploadCostEstimateCalculatorForAR: + UploadCostEstimateCalculatorForAR( + arweaveService: context.read(), + pstService: context.read(), + arCostToUsd: ConvertArToUSD( + arweave: context.read(), + ), + ), + ), + uploadPreparer: UploadPreparer( + uploadPlanUtils: UploadPlanUtils( + crypto: ArDriveCrypto(), + arweave: context.read(), + turboUploadService: + context.read(), + driveDao: context.read(), + ), ), ), ), - const Divider(), - const SizedBox(height: 16), - Text( - appLocalizationsOf(context).freeTurboTransaction, - style: textStyle, - ), - const SizedBox(height: 24), - Text( - appLocalizationsOf(context) - .filesWillBePermanentlyPublicWarning, - style: textStyle, - ), - ], - ), - ), - actions: [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, - ), - ModalAction( - action: () => readCubitContext.uploadManifest(), - title: appLocalizationsOf(context).confirmEmphasized, - ), - ], - ); - } - if (state is CreateManifestUploadConfirmation) { - Navigator.pop(context); - return ArDriveStandardModal( - width: kMediumDialogWidth, - title: appLocalizationsOf(context).createManifestEmphasized, - content: SizedBox( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 256), - child: Scrollbar( - child: ListView( - shrinkWrap: true, - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - state.manifestName, - style: textStyle, - ), - subtitle: Text( - filesize(state.manifestSize), - style: textStyle.copyWith( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgOnDisabled), - ), + BlocProvider( + create: (context) => UploadPaymentMethodBloc( + context.read(), + context.read(), + context.read(), + )..add( + PrepareUploadPaymentMethod( + params: UploadParams( + conflictingFiles: {}, + files: [ + UploadFile( + ioFile: state.manifestFile, + parentFolderId: state.parentFolder.id, + ), + ], + foldersByPath: + UploadPlanUtils.generateFoldersForFiles([ + UploadFile( + ioFile: state.manifestFile, + parentFolderId: state.parentFolder.id, + ), + ]), + targetDrive: state.drive, + targetFolder: state.parentFolder, + user: context.read().currentUser, ), - ], + ), ), - ), ), - const Divider(), - const SizedBox(height: 16), - Text.rich( - TextSpan( + ], + child: UploadPaymentMethodView( + onUploadMethodChanged: (method, methodInfo, canUpload) { + readCubitContext.selectUploadMethod( + method, + methodInfo, + canUpload, + ); + }, + onError: () {}, + loadingIndicator: Center( + child: Column( children: [ - TextSpan( - text: appLocalizationsOf(context) - .cost(state.arUploadCost), + Text( + 'Loading payment methods...', + style: typography.paragraphLarge( + color: colorTokens.textHigh, + fontWeight: ArFontWeight.bold, + ), ), - if (state.usdUploadCost != null) - TextSpan( - text: usdUploadCostToString( - state.usdUploadCost!, - ), - ) - else - TextSpan( - text: - ' ${appLocalizationsOf(context).usdPriceNotAvailable}', + const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, horizontal: 32), + child: SizedBox( + child: LinearProgressIndicator(), ), + ), ], - style: textStyle, ), ), - const SizedBox(height: 24), - Text( - appLocalizationsOf(context) - .filesWillBePermanentlyPublicWarning, - style: textStyle, - ), - ], - ), - ), - actions: [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, - ), - ModalAction( - action: () => readCubitContext.uploadManifest(), - title: appLocalizationsOf(context).confirmEmphasized, + useNewArDriveUI: true, + ), ), + const SizedBox(height: 24), ], - ); - } - - if (state is CreateManifestFolderLoadSuccess) { - return _selectFolder(state, context); - } - - return const SizedBox(); - }); - - bool _isFolderEmpty(FolderID folderId, FolderNode rootFolderNode) { - final folderNode = rootFolderNode.searchForFolder(folderId); - - if (folderNode == null) { - return true; - } - - return folderNode.isEmpty(); + Text( + appLocalizationsOf(context).filesWillBePermanentlyPublicWarning, + style: textStyle, + ), + ], + ), + ), + actions: [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).cancelEmphasized, + ), + ModalAction( + isEnable: state.canUpload, + action: () => readCubitContext.uploadManifest(), + title: appLocalizationsOf(context).confirmEmphasized, + ), + ], + ); } Widget _selectFolder( CreateManifestFolderLoadSuccess state, BuildContext context) { final cubit = context.read(); + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + final items = [ ...state.viewingFolder.subfolders .where((element) => !element.isHidden) @@ -458,21 +568,27 @@ class _CreateManifestFormState extends State { child: Row( children: [ ArDriveIcons.folderOutline( - size: 16, - color: enabled ? null : _colorDisabled(context), + size: 20, + color: enabled + ? colorTokens.textHigh + : _colorDisabled(context), ), const SizedBox(width: 8), Expanded( child: Text( f.name, - style: ArDriveTypography.body.inputNormalRegular( - color: enabled ? null : _colorDisabled(context), - ), + style: typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + color: enabled + ? colorTokens.textHigh + : _colorDisabled(context)), ), ), ArDriveIcons.carretRight( - size: 18, - color: enabled ? null : _colorDisabled(context), + size: 24, + color: enabled + ? colorTokens.textHigh + : _colorDisabled(context), ), ], ), @@ -481,9 +597,7 @@ class _CreateManifestFormState extends State { ); }, ), - ...state.viewingFolder.files - .where((element) => !element.isHidden) - .map( + ...state.viewingFolder.files.where((element) => !element.isHidden).map( (f) => Padding( padding: const EdgeInsets.symmetric( vertical: 16.0, @@ -492,14 +606,14 @@ class _CreateManifestFormState extends State { child: Row( children: [ ArDriveIcons.file( - size: 16, + size: 20, color: _colorDisabled(context), ), const SizedBox(width: 8), Expanded( child: Text( f.name, - style: ArDriveTypography.body.inputNormalRegular( + style: typography.paragraphLarge( color: _colorDisabled(context), ), ), @@ -507,68 +621,70 @@ class _CreateManifestFormState extends State { ], ), ), - ) - , + ), ]; - return ArDriveCard( - height: 441, + return ArDriveModalNew( + constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600), contentPadding: EdgeInsets.zero, - width: kMediumDialogWidth, - content: SizedBox( - height: 325, + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600, maxHeight: 500), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Container( - padding: const EdgeInsets.only(left: 16, right: 16), width: double.infinity, height: 77, alignment: Alignment.centerLeft, - color: - ArDriveTheme.of(context).themeData.tableTheme.backgroundColor, - child: Row( - children: [ - ArDriveClickArea( - child: AnimatedContainer( - width: !state.viewingRootFolder ? 20 : 0, - duration: const Duration(milliseconds: 200), - child: GestureDetector( - onTap: () { - cubit.loadParentFolder(); - }, - child: AnimatedScale( - duration: const Duration(milliseconds: 200), - scale: !state.viewingRootFolder ? 1 : 0, - child: ArDriveIcons.arrowLeft( - size: 32, + color: colorTokens.containerL1, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + ArDriveClickArea( + child: AnimatedContainer( + width: !state.viewingRootFolder ? 20 : 0, + duration: const Duration(milliseconds: 200), + child: GestureDetector( + onTap: () { + cubit.loadParentFolder(); + }, + child: AnimatedScale( + duration: const Duration(milliseconds: 200), + scale: !state.viewingRootFolder ? 1 : 0, + child: ArDriveIcons.arrowLeft( + size: 32, + ), ), ), ), ), - ), - AnimatedPadding( - duration: const Duration(milliseconds: 200), - padding: !state.viewingRootFolder - ? const EdgeInsets.only(left: 14) - : const EdgeInsets.only(left: 0), - child: Text( - appLocalizationsOf(context).targetFolderEmphasized, - style: ArDriveTypography.headline.headline5Bold(), + AnimatedPadding( + duration: const Duration(milliseconds: 200), + padding: !state.viewingRootFolder + ? const EdgeInsets.only(left: 14) + : const EdgeInsets.only(left: 0), + child: Text( + appLocalizationsOf(context).targetFolderEmphasized, + style: typography.heading5( + color: colorTokens.textHigh, + fontWeight: ArFontWeight.bold, + ), + ), ), - ), - const Spacer(), - ArDriveIconButton( - onPressed: () { - Navigator.pop(context); - }, - icon: ArDriveIcons.x( - size: 24, + const Spacer(), + ArDriveIconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: ArDriveIcons.x( + size: 24, + ), ), - ), - ], + ], + ), ), ), Expanded( @@ -582,74 +698,38 @@ class _CreateManifestFormState extends State { ), const Divider(), Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: ArDriveCard( - backgroundColor: ArDriveTheme.of(context) - .themeData - .tableTheme - .backgroundColor, - borderRadius: 5, - content: Row( - children: [ - ArDriveIcons.info( - size: 16, - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Hidden files are not added into the manifest.', - style: ArDriveTypography.body.buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ), + backgroundColor: colorTokens.containerL1, + borderRadius: 5, + content: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ArDriveIcons.info(size: 16, color: colorTokens.iconHigh), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Hidden files are not added into the manifest.', + style: typography.paragraphNormal( + color: colorTokens.textMid, + fontWeight: ArFontWeight.semiBold, ), ), - ], - )), - ), - Container( - decoration: BoxDecoration( - color: ArDriveTheme.of(context).themeData.colors.themeBgSurface, - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ArDriveButton( - maxHeight: 36, - backgroundColor: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - fontStyle: ArDriveTypography.body.buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeAccentSubtle, ), - text: appLocalizationsOf(context).createHereEmphasized, - onPressed: () { - cubit.checkForConflicts(_manifestNameController.text); - }, - ), - ], + ], + ), ), - ) + ), ], ), ), + action: ModalAction( + action: () => cubit.checkForConflicts(_manifestNameController.text), + title: appLocalizationsOf(context).createHereEmphasized, + ), ); } Color _colorDisabled(BuildContext context) => - ArDriveTheme.of(context).themeData.colors.themeInputPlaceholder; + ArDriveTheme.of(context).themeData.colorTokens.iconLow; } diff --git a/lib/components/payment_method_selector_widget.dart b/lib/components/payment_method_selector_widget.dart index 97ba90fd9b..86af6409cd 100644 --- a/lib/components/payment_method_selector_widget.dart +++ b/lib/components/payment_method_selector_widget.dart @@ -10,6 +10,7 @@ class PaymentMethodSelector extends StatelessWidget { final void Function() onTurboTopupSucess; final void Function() onArSelect; final void Function() onTurboSelect; + final bool useNewArDriveUI; const PaymentMethodSelector({ super.key, @@ -17,6 +18,7 @@ class PaymentMethodSelector extends StatelessWidget { required this.onTurboTopupSucess, required this.onArSelect, required this.onTurboSelect, + this.useNewArDriveUI = false, }); @override @@ -33,6 +35,9 @@ class PaymentMethodSelector extends StatelessWidget { } Widget _buildContent(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + return Column( children: [ Text( @@ -66,7 +71,11 @@ class PaymentMethodSelector extends StatelessWidget { // TODO: Localization text: 'Cost: ${winstonToAr(uploadMethodInfo.costEstimateAr.totalCost)} AR', - textStyle: ArDriveTypography.body.buttonLargeBold(), + textStyle: useNewArDriveUI + ? typography.paragraphLarge( + fontWeight: ArFontWeight.bold, + ) + : ArDriveTypography.body.buttonLargeBold(), ), if (uploadMethodInfo.costEstimateTurbo != null && uploadMethodInfo.isTurboUploadPossible) @@ -76,7 +85,11 @@ class PaymentMethodSelector extends StatelessWidget { text: uploadMethodInfo.hasNoTurboBalance ? '' : 'Cost: ${winstonToAr(uploadMethodInfo.costEstimateTurbo!.totalCost)} Credits', - textStyle: ArDriveTypography.body.buttonLargeBold(), + textStyle: useNewArDriveUI + ? typography.paragraphLarge( + color: colorTokens.textHigh, + fontWeight: ArFontWeight.bold) + : ArDriveTypography.body.buttonLargeBold(), content: uploadMethodInfo.hasNoTurboBalance ? GestureDetector( onTap: () { @@ -91,26 +104,32 @@ class PaymentMethodSelector extends StatelessWidget { // TODO: use text with multiple styles TextSpan( text: 'Use Turbo Credits', // TODO: localize - style: ArDriveTypography.body - .buttonLargeBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ) - .copyWith( - decoration: TextDecoration.underline, - ), + style: useNewArDriveUI + ? typography.paragraphLarge( + color: colorTokens.textMid, + fontWeight: ArFontWeight.bold, + ) + : ArDriveTypography.body.buttonLargeBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), ), TextSpan( text: ' for faster uploads.', // TODO: localize - style: ArDriveTypography.body.buttonLargeBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ), + style: useNewArDriveUI + ? typography.paragraphLarge( + color: colorTokens.textMid, + fontWeight: ArFontWeight.bold, + ) + : ArDriveTypography.body.buttonLargeBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), ), ], ), @@ -132,10 +151,17 @@ class PaymentMethodSelector extends StatelessWidget { // TODO: localize ? 'Wallet Balance: ${uploadMethodInfo.arBalance} AR' : 'Turbo Balance: ${uploadMethodInfo.turboCredits} Credits', - style: ArDriveTypography.body.buttonNormalBold( - color: - ArDriveTheme.of(context).themeData.colors.themeFgMuted, - ), + style: useNewArDriveUI + ? typography.paragraphNormal( + color: colorTokens.textLow, + fontWeight: ArFontWeight.semiBold, + ) + : ArDriveTypography.body.buttonNormalBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgMuted, + ), ), ), ], diff --git a/lib/components/progress_bar.dart b/lib/components/progress_bar.dart index 675749be53..c18e81a2c9 100644 --- a/lib/components/progress_bar.dart +++ b/lib/components/progress_bar.dart @@ -17,25 +17,23 @@ class _ProgressBarState extends State { @override Widget build(BuildContext context) { return StreamBuilder( - stream: widget.percentage, - builder: (context, snapshot) { - if (snapshot.hasData) { - _percentage = - ((snapshot.data!.progress * 100)).roundToDouble() / 100; - } else { - _percentage = 0; - } + stream: widget.percentage, + builder: (context, snapshot) { + _percentage = snapshot.hasData + ? ((snapshot.data!.progress * 100)).roundToDouble() / 100 + : 0; - return LinearPercentIndicator( - animation: true, - animateFromLastPercent: true, - lineHeight: 10.0, - barRadius: const Radius.circular(5), - backgroundColor: const Color(0xffFAFAFA), - animationDuration: 1000, - percent: _percentage, - progressColor: const Color(0xff3C3C3C), - ); - }); + return LinearPercentIndicator( + animation: true, + animateFromLastPercent: true, + lineHeight: 10.0, + barRadius: const Radius.circular(5), + backgroundColor: const Color(0xffFAFAFA), + animationDuration: 1000, + percent: _percentage, + progressColor: const Color(0xff3C3C3C), + ); + }, + ); } } diff --git a/lib/components/progress_dialog.dart b/lib/components/progress_dialog.dart index e0cf571255..86edf8f94a 100644 --- a/lib/components/progress_dialog.dart +++ b/lib/components/progress_dialog.dart @@ -26,6 +26,7 @@ class ProgressDialog extends StatelessWidget { this.progressDescription, this.progressBar, this.percentageDetails, + this.useNewArDriveUI = false, }); final String title; @@ -33,42 +34,53 @@ class ProgressDialog extends StatelessWidget { final Widget? progressDescription; final Widget? progressBar; final Widget? percentageDetails; - + final bool useNewArDriveUI; @override Widget build(BuildContext context) { - return ArDriveStandardModal( - title: title, - content: Padding( - padding: const EdgeInsets.symmetric(vertical: 32), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 32), - child: SizedBox( - width: 74, - height: 74, - child: CircularProgressIndicator( - strokeWidth: 8, - )), - ), - if (progressDescription != null) - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: progressDescription!, + final content = Padding( + padding: const EdgeInsets.symmetric(vertical: 32), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 32), + child: SizedBox( + width: 74, + height: 74, + child: CircularProgressIndicator( + strokeWidth: 8, ), - if (progressBar != null) progressBar!, - if (percentageDetails != null) - Padding( - padding: const EdgeInsets.only(top: 16), - child: percentageDetails!), - ], - ), + ), + ), + if (progressDescription != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: progressDescription!, + ), + if (progressBar != null) progressBar!, + if (percentageDetails != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: percentageDetails!), + ], ), ), + ); + + if (useNewArDriveUI) { + return ArDriveStandardModalNew( + title: title, + content: content, + actions: actions, + ); + } + + return ArDriveStandardModal( + title: title, + content: content, actions: actions, ); } diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index ce69a7d729..07aad1342a 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -476,7 +476,6 @@ class _UploadFormState extends State { .read() .setUploadMethod(method, info, canUpload); }, - params: state.params, ), ), SizedBox( diff --git a/lib/core/arfs/exceptions.dart b/lib/core/arfs/exceptions.dart new file mode 100644 index 0000000000..038c600dbf --- /dev/null +++ b/lib/core/arfs/exceptions.dart @@ -0,0 +1,16 @@ +abstract class ARFSException implements Exception { + final String message; + + ARFSException(this.message); + + @override + String toString() { + return 'ARFSException: $message'; + } +} + +class ARFSMultipleNamesForTheSameEntityException extends ARFSException { + ARFSMultipleNamesForTheSameEntityException() + : super( + 'More than one folder/file with the same name found in the same parent folder'); +} diff --git a/lib/core/arfs/repository/file_repository.dart b/lib/core/arfs/repository/file_repository.dart index 9ca87664c4..d7411a3784 100644 --- a/lib/core/arfs/repository/file_repository.dart +++ b/lib/core/arfs/repository/file_repository.dart @@ -3,6 +3,11 @@ import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; abstract class FileRepository { Future getFilePath(String driveId, String fileId); + Future> + getFilesWithLicenseAndLatestRevisionTransactions( + String driveId, + String folderId, + ); factory FileRepository( DriveDao driveDao, FolderRepository folderRepository) => @@ -32,4 +37,15 @@ class _FileRepository implements FileRepository { final filePath = '$folderPath/${file.name}'; return filePath; } + + // TODO: implement unit tests for this method + @override + Future> + getFilesWithLicenseAndLatestRevisionTransactions( + String driveId, String folderId) { + return _driveDao + .filesInFolderWithLicenseAndRevisionTransactions( + driveId: driveId, parentFolderId: folderId) + .get(); + } } diff --git a/lib/core/arfs/repository/folder_repository.dart b/lib/core/arfs/repository/folder_repository.dart index 38ad35c5b1..a468b18b5a 100644 --- a/lib/core/arfs/repository/folder_repository.dart +++ b/lib/core/arfs/repository/folder_repository.dart @@ -1,10 +1,30 @@ +import 'package:ardrive/core/arfs/exceptions.dart'; import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; import 'package:ardrive/models/database/database.dart'; +import 'package:ardrive/utils/logger.dart'; +import 'package:drift/drift.dart'; abstract class FolderRepository { + Future getFolderNode(String driveId, String folderId); Future getLatestFolderRevisionInfo( String driveId, String folderId); Future getFolderPath(String driveId, String folderId); + Stream watchFolderContents({ + required String driveId, + required String folderId, + DriveOrder orderBy = DriveOrder.name, + OrderingMode orderingMode = OrderingMode.asc, + }); + Future> existingFilesWithName({ + required String name, + required String parentFolderId, + required String driveId, + }); + Future> existingFoldersWithName({ + required String name, + required String parentFolderId, + required String driveId, + }); factory FolderRepository(DriveDao driveDao) => _FolderRepository(driveDao); } @@ -54,4 +74,71 @@ class _FolderRepository implements FolderRepository { // This will correctly handle the scenario when pathComponents is empty return pathComponents.join('/'); } + + // TODO: implement unit tests for this method + @override + Stream watchFolderContents({ + required String driveId, + required String folderId, + DriveOrder orderBy = DriveOrder.name, + OrderingMode orderingMode = OrderingMode.asc, + }) { + return _driveDao.watchFolderContents( + driveId, + folderId: folderId, + orderBy: orderBy, + orderingMode: orderingMode, + ); + } + + @override + Future getFolderNode(String driveId, String folderId) { + return _driveDao.getFolderTree(driveId, folderId); + } + + // TODO: implement unit tests for this method + @override + Future> existingFilesWithName({ + required String name, + required String parentFolderId, + required String driveId, + }) async { + final files = await _driveDao + .filesInFolderWithName( + name: name, parentFolderId: parentFolderId, driveId: driveId) + .get(); + + if (files.length > 1) { + /// It should not happen, but it's a possible case. + logger.e( + 'Error checking for file name conflictics.', + ARFSMultipleNamesForTheSameEntityException(), + ); + } + + return files; + } + + // TODO: implement unit tests for this method + @override + Future> existingFoldersWithName({ + required String name, + required String parentFolderId, + required String driveId, + }) async { + final folders = await _driveDao + .foldersInFolderWithName( + name: name, parentFolderId: parentFolderId, driveId: driveId) + .get(); + + if (folders.length > 1) { + /// It should not happen, but it's a possible case. + logger.e( + 'Error checking for folder name conflictics.', + ARFSMultipleNamesForTheSameEntityException(), + ); + } + + return folders; + } } diff --git a/lib/core/arfs/utils/arfs_revision_status_utils.dart b/lib/core/arfs/utils/arfs_revision_status_utils.dart new file mode 100644 index 0000000000..76767d1ffb --- /dev/null +++ b/lib/core/arfs/utils/arfs_revision_status_utils.dart @@ -0,0 +1,45 @@ +import 'package:ardrive/core/arfs/repository/file_repository.dart'; +import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; +import 'package:ardrive/models/enums.dart'; + +class ARFSRevisionStatusUtils { + final FileRepository _fileRepository; + + ARFSRevisionStatusUtils(this._fileRepository); + + Future hasPendingFilesOnTargetFolder({ + required FolderNode folderNode, + }) async { + final files = folderNode.getRecursiveFiles(); + final folders = folderNode.subfolders; + + if (files.isEmpty && folders.isEmpty) { + return false; + } + + final filesWithTx = + await _fileRepository.getFilesWithLicenseAndLatestRevisionTransactions( + folderNode.folder.driveId, + folderNode.folder.id, + ); + + final hasPendingFiles = filesWithTx.any((e) => + TransactionStatus.pending == + fileStatusFromTransactions( + e.metadataTx, + e.dataTx, + ).toString()); + + if (hasPendingFiles) { + return true; + } + + for (var folder in folders) { + if (await hasPendingFilesOnTargetFolder(folderNode: folder)) { + return true; + } + } + + return false; + } +} diff --git a/lib/entities/manifest_data.dart b/lib/entities/manifest_data.dart index 7a5d88991c..b70b475b71 100644 --- a/lib/entities/manifest_data.dart +++ b/lib/entities/manifest_data.dart @@ -75,10 +75,31 @@ class ManifestData { return manifestDataItem; } - static Future fromFolderNode({ + factory ManifestData.fromJson(Map json) => + _$ManifestDataFromJson(json); + Map toJson() => _$ManifestDataToJson(this); +} + +/// Utility function to remove base path of the target folder and +/// replace spaces with underscores for arweave.net URL compatibility +String prepareManifestPath({ + required String filePath, + required String rootFolderPath, +}) { + return filePath.substring(rootFolderPath.length + 1).replaceAll(' ', '_'); +} + +class ManifestDataBuilder { + final FolderRepository folderRepository; + final FileRepository fileRepository; + + ManifestDataBuilder({ + required this.folderRepository, + required this.fileRepository, + }); + + Future build({ required FolderNode folderNode, - required FolderRepository folderRepository, - required FileRepository fileRepository, }) async { final fileList = folderNode .getRecursiveFiles() @@ -104,7 +125,7 @@ class ManifestData { folderNode.folder.driveId, folderNode.folder.id, ); - + final indexPath = await fileRepository.getFilePath(indexFile.driveId, indexFile.id); @@ -125,17 +146,4 @@ class ManifestData { paths, ); } - - factory ManifestData.fromJson(Map json) => - _$ManifestDataFromJson(json); - Map toJson() => _$ManifestDataToJson(this); -} - -/// Utility function to remove base path of the target folder and -/// replace spaces with underscores for arweave.net URL compatibility -String prepareManifestPath({ - required String filePath, - required String rootFolderPath, -}) { - return filePath.substring(rootFolderPath.length + 1).replaceAll(' ', '_'); } diff --git a/lib/manifest/domain/exceptions.dart b/lib/manifest/domain/exceptions.dart new file mode 100644 index 0000000000..b19d76caa3 --- /dev/null +++ b/lib/manifest/domain/exceptions.dart @@ -0,0 +1,6 @@ +class ManifestCreationException implements Exception { + final String message; + final Object? error; + + ManifestCreationException(this.message, {this.error}); +} diff --git a/lib/manifest/domain/manifest_repository.dart b/lib/manifest/domain/manifest_repository.dart new file mode 100644 index 0000000000..d8962e0091 --- /dev/null +++ b/lib/manifest/domain/manifest_repository.dart @@ -0,0 +1,243 @@ +import 'dart:async'; + +import 'package:ardrive/core/arfs/repository/folder_repository.dart'; +import 'package:ardrive/core/arfs/utils/arfs_revision_status_utils.dart'; +import 'package:ardrive/entities/constants.dart'; +import 'package:ardrive/entities/file_entity.dart'; +import 'package:ardrive/entities/manifest_data.dart'; +import 'package:ardrive/manifest/domain/exceptions.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ardrive_io/ardrive_io.dart'; +import 'package:ardrive_uploader/ardrive_uploader.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:arweave/arweave.dart'; +import 'package:collection/collection.dart'; + +abstract class ManifestRepository { + /// Saves the manifest file on the database. + Future saveManifestOnDatabase({ + required ARFSFileUploadMetadata manifest, + String? existingManifestFileId, + }); + + Future uploadManifest({ + required ManifestUploadParams params, + }); + + Future getManifestFile({ + required FolderEntry parentFolder, + required String manifestName, + required FolderNode rootFolderNode, + required String driveId, + }); + + Future hasPendingFilesOnTargetFolder({required FolderNode folderNode}); + + /// Checks if there is a name conflict with the manifest file. + /// Returns a tuple with the first value being a boolean indicating if there is a conflict. The second value is the existing manifest file id if there is a conflict. + Future<(bool, String?)> checkNameConflictAndReturnExistingFileId({ + required String driveId, + required String parentFolderId, + required String name, + }); +} + +class ManifestRepositoryImpl implements ManifestRepository { + final DriveDao _driveDao; + final ArDriveUploader _uploader; + final FolderRepository _folderRepository; + final ManifestDataBuilder _builder; + final ARFSRevisionStatusUtils _versionRevisionStatusUtils; + + ManifestRepositoryImpl( + this._driveDao, + this._uploader, + this._folderRepository, + this._builder, + this._versionRevisionStatusUtils, + ); + + @override + Future saveManifestOnDatabase({ + required ARFSFileUploadMetadata manifest, + String? existingManifestFileId, + }) async { + try { + final manifestFileEntity = FileEntity( + size: manifest.size, + parentFolderId: manifest.parentFolderId, + name: manifest.name, + lastModifiedDate: DateTime.now(), + id: manifest.id, + driveId: manifest.driveId, + dataTxId: manifest.dataTxId, + dataContentType: ContentType.manifest, + ); + + manifestFileEntity.txId = manifest.metadataTxId!; + + await _driveDao.runTransaction( + () async { + await _driveDao.writeFileEntity(manifestFileEntity); + + await _driveDao.insertFileRevision( + manifestFileEntity.toRevisionCompanion( + performedAction: existingManifestFileId == null + ? RevisionAction.create + : RevisionAction.uploadNewVersion, + ), + ); + }, + ); + } catch (e) { + throw ManifestCreationException( + 'Failed to save manifest on database.', + error: e, + ); + } + } + + @override + Future uploadManifest({ + required ManifestUploadParams params, + }) async { + try { + final completer = Completer(); + + final controller = await _uploader.upload( + file: params.manifestFile, + args: ARFSUploadMetadataArgs( + driveId: params.driveId, + parentFolderId: params.parentFolderId, + entityId: params.existingManifestFileId, + isPrivate: false, + type: params.uploadType, + privacy: DrivePrivacyTag.public, + ), + wallet: params.wallet, + type: params.uploadType, + ); + + controller.onDone((tasks) { + final task = tasks.first; + final manifestMetadata = task.content!.first as ARFSFileUploadMetadata; + + saveManifestOnDatabase( + manifest: manifestMetadata, + existingManifestFileId: params.existingManifestFileId, + ); + + completer.complete(); + }); + + controller.onError((err) => completer.completeError(err)); + + await completer.future; + } catch (e) { + throw ManifestCreationException( + 'Failed to upload manifest.', + error: e, + ); + } + } + + @override + Future getManifestFile({ + required FolderEntry parentFolder, + required String manifestName, + required FolderNode rootFolderNode, + required String driveId, + }) async { + try { + final folderNode = rootFolderNode.searchForFolder(parentFolder.id) ?? + await _driveDao.getFolderTree(driveId, parentFolder.id); + + final arweaveManifest = await _builder.build( + folderNode: folderNode, + ); + + final manifestFile = await IOFileAdapter().fromData( + arweaveManifest.jsonData, + name: manifestName, + lastModifiedDate: DateTime.now(), + contentType: ContentType.manifest, + ); + + return manifestFile; + } catch (e) { + throw ManifestCreationException( + 'Failed to create manifest file.', + error: e, + ); + } + } + + @override + Future hasPendingFilesOnTargetFolder({ + required FolderNode folderNode, + }) async { + try { + return _versionRevisionStatusUtils.hasPendingFilesOnTargetFolder( + folderNode: folderNode, + ); + } catch (e) { + throw ManifestCreationException( + 'Failed to check for pending files on target folder.', + error: e, + ); + } + } + + @override + Future<(bool, String?)> checkNameConflictAndReturnExistingFileId({ + required String driveId, + required String parentFolderId, + required String name, + }) async { + try { + final foldersWithName = await _folderRepository.existingFoldersWithName( + driveId: driveId, parentFolderId: parentFolderId, name: name); + final filesWithName = await _folderRepository.existingFilesWithName( + driveId: driveId, parentFolderId: parentFolderId, name: name); + + final conflictingFiles = + filesWithName.where((e) => e.dataContentType != ContentType.manifest); + + if (foldersWithName.isNotEmpty || conflictingFiles.isNotEmpty) { + // Name conflicts with existing file or folder + // This is an error case, send user back to naming the manifest + return (true, null); + } + + /// Might be a case where the user is trying to upload a new version of the manifest + final existingManifestFileId = filesWithName + .firstWhereOrNull((e) => e.dataContentType == ContentType.manifest) + ?.id; + + return (false, existingManifestFileId); + } catch (e) { + throw ManifestCreationException( + 'Failed to check for name conflict.', + error: e, + ); + } + } +} + +class ManifestUploadParams { + final IOFile manifestFile; + final String driveId; + final String parentFolderId; + final String? existingManifestFileId; + final UploadType uploadType; + final Wallet wallet; + + ManifestUploadParams({ + required this.manifestFile, + required this.driveId, + required this.parentFolderId, + required this.uploadType, + this.existingManifestFileId, + required this.wallet, + }); +} diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index 5c39604bec..e438e4c74c 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -631,7 +631,7 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { LicensesCompanion license, ) async { await db.transaction(() async { - await Future.wait( + await Future.wait( license.getTransactionCompanions().map((tx) => writeTransaction(tx))); await into(licenses).insert(license); }); diff --git a/packages/ardrive_ui/lib/src/components/modal.dart b/packages/ardrive_ui/lib/src/components/modal.dart index dbfcb02ee9..b32fd93740 100644 --- a/packages/ardrive_ui/lib/src/components/modal.dart +++ b/packages/ardrive_ui/lib/src/components/modal.dart @@ -69,6 +69,26 @@ class ArDriveModalNew extends StatelessWidget { padding: contentPadding, child: content, ), + if (action != null) ...[ + Padding( + padding: const EdgeInsets.only( + left: 16, right: 16, bottom: 24, top: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: ArDriveButtonNew( + maxHeight: 40, + variant: ButtonVariant.primary, + text: action!.title, + onPressed: action!.action, + typography: ArDriveTypographyNew.of(context), + ), + ), + ], + ), + ), + ] ], ), boxShadow: BoxShadowCard.shadow80, diff --git a/packages/ardrive_uploader/lib/src/upload_controller.dart b/packages/ardrive_uploader/lib/src/upload_controller.dart index 191a027fc2..eb9c6648b1 100644 --- a/packages/ardrive_uploader/lib/src/upload_controller.dart +++ b/packages/ardrive_uploader/lib/src/upload_controller.dart @@ -391,21 +391,13 @@ class _UploadController implements UploadController { void Function(UploadProgress progress)? _onProgressChange = (progress) {}; - void Function(List tasks) _onDone = (List tasks) { - print('Upload Finished'); - }; + void Function(List tasks) _onDone = (List tasks) {}; - void Function(List tasks) _onCancel = (List tasks) { - print('Upload Canceled'); - }; + void Function(List tasks) _onCancel = (List tasks) {}; - void Function(List tasks) _onError = (List tasks) { - print('Upload Error'); - }; + void Function(List tasks) _onError = (List tasks) {}; - void Function(UploadTask task) _onCompleteTask = (UploadTask tasks) { - print('Upload Canceled'); - }; + void Function(UploadTask task) _onCompleteTask = (UploadTask tasks) {}; void _updateTaskStatus(UploadTask task, String taskId) { switch (task.status) { diff --git a/test/core/arfs/folder_repository_test.dart b/test/core/arfs/folder_repository_test.dart index 379a28a9f8..83f73c584f 100644 --- a/test/core/arfs/folder_repository_test.dart +++ b/test/core/arfs/folder_repository_test.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart' as drift; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import '../../manifest/domain/manifest_repository_test.dart'; import '../../test_utils/mocks.dart'; class MockSelectable extends Mock implements drift.Selectable {} @@ -62,5 +63,22 @@ void main() { 'invalidDriveId', 'invalidFolderId'); expect(folderRevision, isNull); }); + group('getFolderNode', () { + test('returns a FolderNode on valid input', () async { + when(() => mockDriveDao.getFolderTree('validDriveId', 'validFolderId')) + .thenAnswer((invocation) async => MockFolderNode()); + + final folderNode = await folderRepository.getFolderNode( + 'validDriveId', + 'validFolderId', + ); + + expect(folderNode, isNotNull); + verify(() => mockDriveDao.getFolderTree( + 'validDriveId', + 'validFolderId', + )).called(1); + }); + }); }); } diff --git a/test/entities/manifest_data_test.dart b/test/entities/manifest_data_test.dart index 41ce244c34..3ca2ada16b 100644 --- a/test/entities/manifest_data_test.dart +++ b/test/entities/manifest_data_test.dart @@ -214,15 +214,16 @@ void main() { stubFileInChild2.driveId, stubFileInChild2.id)).thenAnswer( (_) async => 'root-folder/parent-folder/child-folder/file-in-child-2'); }); - group('ManifestEntity Tests', () { + group('ManifestDataBuilder Tests', () { test('returns a ManifestEntity with a valid expected manifest shape', () async { - final manifest = await ManifestData.fromFolderNode( - folderNode: stubRootFolderNode, - fileRepository: fileRepository, + final builder = ManifestDataBuilder( folderRepository: folderRepository, + fileRepository: fileRepository, ); + final manifest = await builder.build(folderNode: stubRootFolderNode); + expect( manifest.toJson(), equals({ @@ -255,10 +256,13 @@ void main() { test( 'returns a ManifestEntity with a valid expected manifest shape with a nested child folder', () async { - final manifest = await ManifestData.fromFolderNode( - folderNode: stubChildFolderNode, - fileRepository: fileRepository, + final builder = ManifestDataBuilder( folderRepository: folderRepository, + fileRepository: fileRepository, + ); + + final manifest = await builder.build( + folderNode: stubChildFolderNode, ); expect( @@ -289,11 +293,15 @@ void main() { test('returns a DataItem with the expected tags, owner, and data', () async { - final manifest = await ManifestData.fromFolderNode( - folderNode: stubRootFolderNode, - fileRepository: fileRepository, + final builder = ManifestDataBuilder( folderRepository: folderRepository, + fileRepository: fileRepository, ); + + final manifest = await builder.build( + folderNode: stubRootFolderNode, + ); + final wallet = getTestWallet(); AppPlatform.setMockPlatform(platform: SystemPlatform.Android); diff --git a/test/manifest/domain/manifest_repository_test.dart b/test/manifest/domain/manifest_repository_test.dart new file mode 100644 index 0000000000..31ffd8404c --- /dev/null +++ b/test/manifest/domain/manifest_repository_test.dart @@ -0,0 +1,487 @@ +import 'dart:typed_data'; + +import 'package:ardrive/core/arfs/utils/arfs_revision_status_utils.dart'; +import 'package:ardrive/entities/entities.dart'; +import 'package:ardrive/entities/manifest_data.dart'; +import 'package:ardrive/manifest/domain/exceptions.dart'; +import 'package:ardrive/manifest/domain/manifest_repository.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ardrive_io/ardrive_io.dart'; +import 'package:ardrive_uploader/ardrive_uploader.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:arweave/arweave.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import '../../test_utils/utils.dart'; + +class MockARFSFileUploadMetadata extends Mock + implements ARFSFileUploadMetadata {} + +class MockManifestUploadParams extends Mock implements ManifestUploadParams {} + +class MockUploadController extends Mock implements UploadController {} + +class MockFolderNode extends Mock implements FolderNode {} + +class MockFolderEntry extends Mock implements FolderEntry {} + +class MockFileEntry extends Mock implements FileEntry {} + +class MockManifestDataBuilder extends Mock implements ManifestDataBuilder {} + +class MockARFSVersionRevisionStatusUtils extends Mock + implements ARFSRevisionStatusUtils {} + +void main() async { + group( + 'ManifestRepositoryImpl', + () { + late ManifestRepositoryImpl repository; + late MockDriveDao mockDriveDao; + late MockArDriveUploader mockUploader; + late ARFSFileUploadMetadata mockMetadata; + late ManifestUploadParams mockUploadParams; + late MockFolderRepository mockFolderRepository; + late MockUploadController mockUploadController; + late MockFolderEntry mockParentFolder; + late IOFile mockManifestFile; + late MockManifestDataBuilder mockBuilder; + late ARFSRevisionStatusUtils mockVersionRevisionStatusUtils; + + setUp(() { + mockDriveDao = MockDriveDao(); + mockMetadata = MockARFSFileUploadMetadata(); + mockUploadParams = MockManifestUploadParams(); + mockUploader = MockArDriveUploader(); + mockFolderRepository = MockFolderRepository(); + mockBuilder = MockManifestDataBuilder(); + mockVersionRevisionStatusUtils = MockARFSVersionRevisionStatusUtils(); + + registerFallbackValue(FileEntity()); + registerFallbackValue(const FileRevisionsCompanion()); + + repository = ManifestRepositoryImpl( + mockDriveDao, + mockUploader, + mockFolderRepository, + mockBuilder, + mockVersionRevisionStatusUtils, + ); + + // Setup mock data and behavior + when(() => mockMetadata.size).thenReturn(1024); + when(() => mockMetadata.parentFolderId).thenReturn('folder123'); + when(() => mockMetadata.name).thenReturn('TestManifest'); + when(() => mockMetadata.id).thenReturn('id123'); + when(() => mockMetadata.driveId).thenReturn('drive123'); + when(() => mockMetadata.dataTxId).thenReturn('dataTx123'); + when(() => mockMetadata.metadataTxId).thenReturn('metadataTx123'); + when(() => mockMetadata.lastModifiedDate).thenReturn(DateTime.now()); + + when(() => mockUploadParams.driveId).thenReturn('drive123'); + when(() => mockUploadParams.parentFolderId).thenReturn('folder123'); + when(() => mockUploadParams.wallet).thenReturn(Wallet()); + }); + + group('saveManifestOnDatabase', () { + test('Successfully saves a new manifest', () async { + // Ensure transaction method is correctly mocked to return a Future + when(() => mockDriveDao.runTransaction(any())) + .thenAnswer((invocation) async { + final Function transaction = invocation.positionalArguments[0]; + await transaction(); + }); + + when(() => mockDriveDao.writeFileEntity(any())) + .thenAnswer((_) async {}); + when(() => mockDriveDao.insertFileRevision(any())) + .thenAnswer((_) async {}); + + await repository.saveManifestOnDatabase(manifest: mockMetadata); + + verify(() => mockDriveDao.writeFileEntity(any())).called(1); + verify(() => mockDriveDao.insertFileRevision(any())).called(1); + verify(() => mockDriveDao.runTransaction(any())).called(1); + }); + + test('Exception when saving manifest on database', () async { + mockManifestFile = await IOFileAdapter().fromData(Uint8List(1024), + name: 'TestManifest', lastModifiedDate: DateTime.now()); + when(() => mockUploadParams.manifestFile) + .thenReturn(mockManifestFile); + + when(() => mockDriveDao.transaction(any())) + .thenThrow(Exception('DB Error')); + + expect( + repository.saveManifestOnDatabase(manifest: mockMetadata), + throwsA(isA()), + ); + }); + }); + + group('uploadManifest', () { + late FileUploadTask uploadTaskTurbo; + late FileUploadTask uploadTaskAR; + late ARFSUploadMetadataArgs argsTurbo; + late ARFSUploadMetadataArgs argsAR; + setUp(() async { + mockManifestFile = await IOFileAdapter().fromData(Uint8List(1024), + name: 'TestManifest', lastModifiedDate: DateTime.now()); + + registerFallbackValue(FileEntity()); + registerFallbackValue(const FileRevisionsCompanion()); + registerFallbackValue(UploadType.turbo); + registerFallbackValue(Wallet()); + registerFallbackValue(mockManifestFile); + + argsTurbo = ARFSUploadMetadataArgs( + driveId: 'drive123', + parentFolderId: 'folder123', + entityId: null, + isPrivate: false, + type: UploadType.turbo, + privacy: DrivePrivacyTag.public, + ); + + argsAR = ARFSUploadMetadataArgs( + driveId: 'drive123', + parentFolderId: 'folder123', + entityId: null, + isPrivate: false, + type: UploadType.d2n, + privacy: DrivePrivacyTag.public, + ); + + when(() => mockUploadParams.manifestFile) + .thenReturn(mockManifestFile); + + registerFallbackValue(argsTurbo); + + when(() => mockUploadParams.manifestFile) + .thenReturn(mockManifestFile); + + mockUploadController = MockUploadController(); + + uploadTaskTurbo = FileUploadTask( + content: [mockMetadata], + file: mockManifestFile, + metadata: mockMetadata, + type: UploadType.turbo, + ); + + uploadTaskAR = FileUploadTask( + content: [mockMetadata], + file: mockManifestFile, + metadata: mockMetadata, + type: UploadType.d2n, + ); + + when(() => mockUploader.upload( + file: mockManifestFile, + args: any(named: 'args'), + wallet: any(named: 'wallet'), + type: any(named: 'type'), + )).thenAnswer((_) async { + when(() => mockUploadController.onDone(any())) + .thenAnswer((invocation) { + final onDone = invocation.positionalArguments.first as Function; + onDone([uploadTaskTurbo]); + }); + when(() => mockUploadController.onError(any())).thenReturn(null); + return mockUploadController; + }); + + // Ensure transaction method is correctly mocked to return a Future + when(() => mockDriveDao.runTransaction(any())) + .thenAnswer((invocation) async { + final Function transaction = invocation.positionalArguments[0]; + await transaction(); + }); + + when(() => mockDriveDao.writeFileEntity(any())) + .thenAnswer((_) async {}); + when(() => mockDriveDao.insertFileRevision(any())) + .thenAnswer((_) async {}); + }); + + test('Successfully uploads and saves manifest USING TURBO', () async { + // TURBO + when(() => mockUploadParams.uploadType).thenReturn(UploadType.turbo); + + await repository.uploadManifest(params: mockUploadParams); + // Verify interactions + verify(() => mockUploader.upload( + file: mockManifestFile, + args: any(named: 'args'), + wallet: any(named: 'wallet'), + + /// TURBO + type: UploadType.turbo, + )).called(1); + + verify(() => mockDriveDao.runTransaction(any())).called(1); + verify(() => mockDriveDao.writeFileEntity(any())).called(1); + verify(() => mockDriveDao.insertFileRevision(any())).called(1); + }); + + test('Successfully uploads and saves manifest USING AR', () async { + registerFallbackValue(argsAR); + + when(() => mockUploadParams.uploadType).thenReturn(UploadType.d2n); + + when(() => mockUploadController.onDone(any())) + .thenAnswer((invocation) { + final onDone = invocation.positionalArguments.first as Function; + onDone([uploadTaskAR]); + }); + + await repository.uploadManifest(params: mockUploadParams); + // Verify interactions + verify(() => mockUploader.upload( + file: mockManifestFile, + args: any(named: 'args'), + wallet: any(named: 'wallet'), + + /// Uploads using AR + type: UploadType.d2n, + )).called(1); + + verify(() => mockDriveDao.runTransaction(any())).called(1); + verify(() => mockDriveDao.writeFileEntity(any())).called(1); + verify(() => mockDriveDao.insertFileRevision(any())).called(1); + }); + + test('Handles upload failure', () async { + when(() => mockUploader.upload( + file: any(named: 'file'), + args: any(named: 'args'), + wallet: any(named: 'wallet'), + type: any(named: 'type'), + )).thenThrow(Exception('Upload failed')); + + expect(() => repository.uploadManifest(params: mockUploadParams), + throwsA(isA())); + }); + }); + + group('hasPendingFilesOnTargetFolder', () { + setUp(() { + registerFallbackValue(MockFolderNode()); + }); + + test('Returns false when no pending files are found', () async { + when(() => + mockVersionRevisionStatusUtils.hasPendingFilesOnTargetFolder( + folderNode: any(named: 'folderNode'))) + .thenAnswer((_) async => false); + + final result = await repository.hasPendingFilesOnTargetFolder( + folderNode: MockFolderNode(), + ); + + expect(result, false); + }); + + test('Returns true when pending files are found', () async { + when(() => + mockVersionRevisionStatusUtils.hasPendingFilesOnTargetFolder( + folderNode: any(named: 'folderNode'))) + .thenAnswer((_) async => true); + + final result = await repository.hasPendingFilesOnTargetFolder( + folderNode: MockFolderNode(), + ); + + expect(result, true); + }); + }); + + // needed since the lastModifiedDate is set to DateTime.now() + group('getManifestFile', () { + registerFallbackValue(Uint8List(1024)); + late MockFolderNode mockRootFolderNode; + late MockFolderNode folderNode; + late ManifestData stubManifestData; + + setUp(() { + mockRootFolderNode = MockFolderNode(); + folderNode = MockFolderNode(); + stubManifestData = ManifestData(ManifestIndex('index.html'), + {'path/to/file': ManifestPath('dataTxId', fileId: 'fileId')}); + + registerFallbackValue(mockRootFolderNode); + + mockParentFolder = MockFolderEntry(); + when(() => mockParentFolder.id).thenReturn('parentFolderId'); + when(() => mockRootFolderNode.searchForFolder(any())) + .thenReturn(folderNode); + when(() => mockDriveDao.getFolderTree(any(), any())) + .thenAnswer((_) async => folderNode); + when(() => mockBuilder.build(folderNode: folderNode)) + .thenAnswer((_) async => stubManifestData); + }); + + test('Successfully retrieves and constructs a manifest file', () async { + final result = await repository.getManifestFile( + parentFolder: mockParentFolder, + manifestName: 'manifest.json', + rootFolderNode: mockRootFolderNode, + driveId: 'drive123'); + + expect(result, isA()); + verify(() => mockBuilder.build(folderNode: folderNode)).called(1); + }); + + test('Handles error when building manifest data', () async { + when(() => mockBuilder.build(folderNode: any(named: 'folderNode'))) + .thenThrow( + Exception('Failed to build manifest data'), + ); + + expect( + () => repository.getManifestFile( + parentFolder: mockParentFolder, + manifestName: 'manifest.json', + rootFolderNode: mockRootFolderNode, + driveId: 'drive123', + ), + throwsA(isA()), + ); + }); + }); + + group('checkNameConflictAndReturnExistingFileId', () { + late MockFolderEntry mockFolderEntry; + late MockFileEntry mockFileEntry; + + setUp(() { + mockFolderEntry = MockFolderEntry(); + mockFileEntry = MockFileEntry(); + when(() => mockFolderRepository.existingFoldersWithName( + driveId: any(named: 'driveId'), + name: any(named: 'name'), + parentFolderId: any(named: 'parentFolderId'))) + .thenAnswer((invocation) async => []); + + when(() => mockFolderRepository.existingFilesWithName( + driveId: any(named: 'driveId'), + name: any(named: 'name'), + parentFolderId: any(named: 'parentFolderId'))) + .thenAnswer((invocation) async => []); + }); + + test('Returns false when no name conflict is found', () async { + final result = + await repository.checkNameConflictAndReturnExistingFileId( + driveId: 'drive123', + name: 'manifest.json', + parentFolderId: 'folder123', + ); + + expect(result, (false, null)); + }); + + test('Handles error when checking for name conflict', () async { + when(() => mockFolderRepository.existingFoldersWithName( + driveId: any(named: 'driveId'), + name: any(named: 'name'), + parentFolderId: any(named: 'parentFolderId'))) + .thenThrow(Exception('Error checking for folder name conflict')); + + expect( + () => repository.checkNameConflictAndReturnExistingFileId( + driveId: 'drive123', + name: 'manifest.json', + parentFolderId: 'folder123', + ), + throwsA(isA()), + ); + }); + + test('Handles error when checking for name conflict', () async { + when(() => mockFolderRepository.existingFilesWithName( + driveId: any(named: 'driveId'), + name: any(named: 'name'), + parentFolderId: any(named: 'parentFolderId'))) + .thenThrow(Exception('Error checking for file name conflict')); + + expect( + () => repository.checkNameConflictAndReturnExistingFileId( + driveId: 'drive123', + name: 'manifest.json', + parentFolderId: 'folder123', + ), + throwsA(isA()), + ); + }); + + test('Should return true when a folder with the same name is found', + () async { + when(() => mockFolderRepository.existingFoldersWithName( + driveId: any(named: 'driveId'), + name: any(named: 'name'), + parentFolderId: any(named: 'parentFolderId'))) + .thenAnswer((invocation) async => [mockFolderEntry]); + + final result = + await repository.checkNameConflictAndReturnExistingFileId( + driveId: 'drive123', + name: 'manifest.json', + parentFolderId: 'folder123', + ); + + expect(result, (true, null)); + }); + + test( + 'Should return true when a file with the same name is found and it is not a manifest file', + () async { + when(() => mockFolderRepository.existingFilesWithName( + driveId: any(named: 'driveId'), + name: any(named: 'name'), + parentFolderId: any(named: 'parentFolderId'))) + .thenAnswer((invocation) async => [mockFileEntry]); + + /// json + when(() => mockFileEntry.dataContentType) + .thenReturn(ContentType.json); + + final result = + await repository.checkNameConflictAndReturnExistingFileId( + driveId: 'drive123', + name: 'manifest.json', + parentFolderId: 'folder123', + ); + + expect(result, (true, null)); + }); + + test( + 'Should return false when a file with the same name is found and it is a manifest file', + () async { + when(() => mockFolderRepository.existingFilesWithName( + driveId: any(named: 'driveId'), + name: any(named: 'name'), + parentFolderId: any(named: 'parentFolderId'))) + .thenAnswer((invocation) async => [mockFileEntry]); + + /// manifest + when(() => mockFileEntry.dataContentType) + .thenReturn(ContentType.manifest); + when(() => mockFileEntry.id).thenReturn('file123'); + + final result = + await repository.checkNameConflictAndReturnExistingFileId( + driveId: 'drive123', + name: 'manifest.json', + parentFolderId: 'folder123', + ); + + expect(result, (false, 'file123')); + }); + }); + }, + ); +} diff --git a/test/test_utils/mocks.dart b/test/test_utils/mocks.dart index a6564c1b01..c393fe06f9 100644 --- a/test/test_utils/mocks.dart +++ b/test/test_utils/mocks.dart @@ -22,6 +22,7 @@ import 'package:ardrive/utils/graphql_retry.dart'; import 'package:ardrive/utils/secure_key_value_store.dart'; import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:ardrive_io/ardrive_io.dart'; +import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; import 'package:bloc_test/bloc_test.dart'; @@ -113,6 +114,8 @@ class MockFolderRepository extends Mock implements FolderRepository {} class MockFileRepository extends Mock implements FileRepository {} +class MockArDriveUploader extends Mock implements ArDriveUploader {} + class MockARFSFile extends ARFSFileEntity { MockARFSFile({ required super.appName,