diff --git a/lib/blocs/folder_create/folder_create_cubit.dart b/lib/blocs/folder_create/folder_create_cubit.dart index 286a19e4e2..b5e47278ff 100644 --- a/lib/blocs/folder_create/folder_create_cubit.dart +++ b/lib/blocs/folder_create/folder_create_cubit.dart @@ -110,12 +110,11 @@ class FolderCreateCubit extends Cubit { AbstractControl control) async { final String folderName = control.value; - // Check that the parent folder does not already have a folder with the input name. - final foldersWithName = await _driveDao - .foldersInFolderWithName( - driveId: driveId, parentFolderId: parentFolderId, name: folderName) - .get(); - final nameAlreadyExists = foldersWithName.isNotEmpty; + final nameAlreadyExists = await _driveDao.doesEntityWithNameExist( + name: folderName, + driveId: driveId, + parentFolderId: parentFolderId, + ); if (nameAlreadyExists) { control.markAsTouched(); diff --git a/lib/blocs/fs_entry_move/fs_entry_move_cubit.dart b/lib/blocs/fs_entry_move/fs_entry_move_cubit.dart index 0e8c22471b..50b8aa060e 100644 --- a/lib/blocs/fs_entry_move/fs_entry_move_cubit.dart +++ b/lib/blocs/fs_entry_move/fs_entry_move_cubit.dart @@ -66,21 +66,6 @@ class FsEntryMoveCubit extends Cubit { ); } - Future entityNameExists({ - required String name, - required String parentFolderId, - }) async { - final foldersWithName = await _driveDao - .foldersInFolderWithName( - driveId: driveId, parentFolderId: parentFolderId, name: name) - .get(); - final filesWithName = await _driveDao - .filesInFolderWithName( - driveId: driveId, parentFolderId: parentFolderId, name: name) - .get(); - return foldersWithName.isNotEmpty || filesWithName.isNotEmpty; - } - Future submit() async { try { final state = this.state as FsEntryMoveFolderLoadSuccess; @@ -100,10 +85,14 @@ class FsEntryMoveCubit extends Cubit { .folderById(driveId: driveId, folderId: folderId!) .getSingle(); - if (await entityNameExists( + final entityWithSameNameExists = + await _driveDao.doesEntityWithNameExist( name: folder.name, + driveId: driveId, parentFolderId: parentFolder.id, - )) { + ); + + if (entityWithSameNameExists) { emit(FsEntryMoveNameConflict(name: folder.name)); return; } @@ -140,10 +129,14 @@ class FsEntryMoveCubit extends Cubit { path: '${parentFolder.path}/${file.name}', lastUpdated: DateTime.now()); - if (await entityNameExists( + final entityWithSameNameExists = + await _driveDao.doesEntityWithNameExist( name: file.name, + driveId: driveId, parentFolderId: parentFolder.id, - )) { + ); + + if (entityWithSameNameExists) { emit(FsEntryMoveNameConflict(name: file.name)); return; } diff --git a/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart b/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart index c82b5846c4..1d721c9129 100644 --- a/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart +++ b/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart @@ -154,16 +154,14 @@ class FsEntryRenameCubit extends Cubit { return {AppValidationMessage.fsEntryNameUnchanged: true}; } - // Check that the current folder does not already have a folder with the target file name. - final foldersWithName = await _driveDao - .foldersInFolderWithName( - driveId: driveId, - parentFolderId: folder.parentFolderId, - name: newFolderName) - .get(); - final nameAlreadyExists = foldersWithName.isNotEmpty; - - if (nameAlreadyExists) { + final entityWithSameNameExists = await _driveDao.doesEntityWithNameExist( + name: newFolderName, + driveId: driveId, + // Will never be null since you can't rename root folder + parentFolderId: folder.parentFolderId!, + ); + + if (entityWithSameNameExists) { control.markAsTouched(); return {AppValidationMessage.fsEntryNameAlreadyPresent: true}; } @@ -182,16 +180,13 @@ class FsEntryRenameCubit extends Cubit { return {AppValidationMessage.fsEntryNameUnchanged: true}; } - // Check that the current folder does not already have a file with the target file name. - final filesWithName = await _driveDao - .filesInFolderWithName( - driveId: driveId, - parentFolderId: file.parentFolderId, - name: newFileName) - .get(); - final nameAlreadyExists = filesWithName.isNotEmpty; + final entityWithSameNameExists = await _driveDao.doesEntityWithNameExist( + name: newFileName, + driveId: driveId, + parentFolderId: file.parentFolderId, + ); - if (nameAlreadyExists) { + if (entityWithSameNameExists) { control.markAsTouched(); return {AppValidationMessage.fsEntryNameAlreadyPresent: true}; } diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 9879c0b5c5..16a1a85a95 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -38,19 +38,20 @@ class UploadCubit extends Cubit { /// Map of conflicting file ids keyed by their file names. final Map conflictingFiles = {}; + final List conflictingFolders = []; bool fileSizeWithinBundleLimits(int size) => size < bundleSizeLimit; - UploadCubit( - {required this.driveId, - required this.folderId, - required this.files, - required ProfileCubit profileCubit, - required DriveDao driveDao, - required ArweaveService arweave, - required PstService pst, - required UploadPlanUtils uploadPlanUtils}) - : _profileCubit = profileCubit, + UploadCubit({ + required this.driveId, + required this.folderId, + required this.files, + required ProfileCubit profileCubit, + required DriveDao driveDao, + required ArweaveService arweave, + required PstService pst, + required UploadPlanUtils uploadPlanUtils, + }) : _profileCubit = profileCubit, _driveDao = driveDao, _arweave = arweave, _pst = pst, @@ -69,9 +70,43 @@ class UploadCubit extends Cubit { /// /// If there's one, prompt the user to upload the file as a version of the existing one. /// If there isn't one, prepare to upload the file. + + Future checkConflictingFolders() async { + emit(UploadPreparationInProgress()); + + for (final file in files) { + final fileName = file.name; + final existingFolderName = await _driveDao + .foldersInFolderWithName( + driveId: _targetDrive.id, + parentFolderId: _targetFolder.id, + name: fileName, + ) + .map((f) => f.name) + .getSingleOrNull(); + + if (existingFolderName != null) { + conflictingFolders.add(existingFolderName); + } + } + + if (conflictingFolders.isNotEmpty) { + emit( + UploadFolderNameConflict( + areAllFilesConflicting: conflictingFolders.length == files.length, + conflictingFileNames: conflictingFolders, + ), + ); + } else { + await checkConflictingFiles(); + } + } + Future checkConflictingFiles() async { emit(UploadPreparationInProgress()); + _removeFilesWithFolderNameConflicts(); + for (final file in files) { final fileName = file.name; final existingFileId = await _driveDao @@ -89,17 +124,21 @@ class UploadCubit extends Cubit { } if (conflictingFiles.isNotEmpty) { - emit(UploadFileConflict( - isAllFilesConflicting: conflictingFiles.length == files.length, - conflictingFileNames: conflictingFiles.keys.toList())); + emit( + UploadFileConflict( + areAllFilesConflicting: conflictingFiles.length == files.length, + conflictingFileNames: conflictingFiles.keys.toList(), + ), + ); } else { await prepareUploadPlanAndCostEstimates(); } } /// If `conflictingFileAction` is null, means that had no conflict. - Future prepareUploadPlanAndCostEstimates( - {ConflictingFileActions? conflictingFileAction}) async { + Future prepareUploadPlanAndCostEstimates({ + ConflictingFileActions? conflictingFileAction, + }) async { final profile = _profileCubit.state as ProfileLoggedIn; if (await _profileCubit.checkIfWalletMismatch()) { @@ -116,7 +155,7 @@ class UploadCubit extends Cubit { _targetDrive.isPrivate ? privateFileSizeLimit : publicFileSizeLimit; if (conflictingFileAction == ConflictingFileActions.Skip) { - _removeConflictingFiles(); + _removeFilesWithFileNameConflicts(); } final tooLargeFiles = [ @@ -131,23 +170,28 @@ class UploadCubit extends Cubit { )); return; } + final uploadPlan = await _uploadPlanUtils.xfilesToUploadPlan( - folderEntry: _targetFolder, - targetDrive: _targetDrive, - files: files, - cipherKey: profile.cipherKey, - wallet: profile.wallet, - conflictingFiles: conflictingFiles); + folderEntry: _targetFolder, + targetDrive: _targetDrive, + files: files, + cipherKey: profile.cipherKey, + wallet: profile.wallet, + conflictingFiles: conflictingFiles, + ); + final costEstimate = await CostEstimate.create( uploadPlan: uploadPlan, arweaveService: _arweave, pstService: _pst, wallet: profile.wallet, ); + if (await _profileCubit.checkIfWalletMismatch()) { emit(UploadWalletMismatch()); return; } + emit( UploadReady( costEstimate: costEstimate, @@ -220,10 +264,14 @@ class UploadCubit extends Cubit { emit(UploadComplete()); } - void _removeConflictingFiles() { + void _removeFilesWithFileNameConflicts() { files.removeWhere((element) => conflictingFiles.containsKey(element.name)); } + void _removeFilesWithFolderNameConflicts() { + files.removeWhere((element) => conflictingFolders.contains(element.name)); + } + @override void onError(Object error, StackTrace stackTrace) { emit(UploadFailure()); diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 0110dec025..d6dfe0f27f 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -29,17 +29,27 @@ class UploadSigningInProgress extends UploadState { class UploadFileConflict extends UploadState { final List conflictingFileNames; - final bool isAllFilesConflicting; + final bool areAllFilesConflicting; UploadFileConflict({ required this.conflictingFileNames, - required this.isAllFilesConflicting, + required this.areAllFilesConflicting, }); @override List get props => [conflictingFileNames]; } +class UploadFolderNameConflict extends UploadFileConflict { + UploadFolderNameConflict({ + required List conflictingFileNames, + required bool areAllFilesConflicting, + }) : super( + conflictingFileNames: conflictingFileNames, + areAllFilesConflicting: areAllFilesConflicting, + ); +} + class UploadFileTooLarge extends UploadState { final List tooLargeFileNames; final bool isPrivate; diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 67c9871eef..c64acf9401 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -54,7 +54,7 @@ class UploadForm extends StatelessWidget { if (state is UploadComplete || state is UploadWalletMismatch) { Navigator.pop(context); } else if (state is UploadPreparationInitialized) { - await context.read().checkConflictingFiles(); + await context.read().checkConflictingFolders(); } if (state is UploadWalletMismatch) { Navigator.pop(context); @@ -62,7 +62,52 @@ class UploadForm extends StatelessWidget { } }, builder: (context, state) { - if (state is UploadFileConflict) { + if (state is UploadFolderNameConflict) { + return AppDialog( + title: appLocalizationsOf(context).duplicateFolders, + content: SizedBox( + width: kMediumDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalizationsOf(context) + .foldersWithTheSameNameAlreadyExists( + state.conflictingFileNames.length, + ), + ), + const SizedBox(height: 16), + Text(appLocalizationsOf(context).conflictingFiles), + const SizedBox(height: 8), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 320), + child: SingleChildScrollView( + child: + Text(state.conflictingFileNames.join(', ')))), + ], + ), + ), + actions: [ + if (!state.areAllFilesConflicting) + TextButton( + style: ButtonStyle( + fixedSize: + MaterialStateProperty.all(Size.fromWidth(140))), + onPressed: () => + context.read().checkConflictingFiles(), + child: Text(appLocalizationsOf(context).skipEmphasized), + ), + TextButton( + style: ButtonStyle( + fixedSize: + MaterialStateProperty.all(Size.fromWidth(140))), + onPressed: () => Navigator.of(context).pop(false), + child: Text(appLocalizationsOf(context).cancelEmphasized), + ), + ], + ); + } else if (state is UploadFileConflict) { return AppDialog( title: appLocalizationsOf(context) .duplicateFiles(state.conflictingFileNames.length), @@ -90,7 +135,7 @@ class UploadForm extends StatelessWidget { ), ), actions: [ - if (!state.isAllFilesConflicting) + if (!state.areAllFilesConflicting) TextButton( style: ButtonStyle( fixedSize: diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fd99725e35..f541d7ee2f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -815,6 +815,21 @@ } } }, + "duplicateFolders": "Folder Name Conflict", + "@duplicateFolders": { + "description": "Folder name conflict dialog title" + }, + "foldersWithTheSameNameAlreadyExists": "{numberOfFolders,plural, =1{A folder with the same name already exists at this location. Please rename it and try uploading again.} =other{{numberOfFolders} folders with the same name already exist at this location. Please rename them and try uploading again.}}", + "@foldersWithTheSameNameAlreadyExists": { + "description": "Asks the user what to do with folder name conflicts", + "placeholders": { + "numberOfFolders": { + "type": "int", + "format": "decimalPattern", + "example": "0" + } + } + }, "conflictingFiles": "Conflicting files:", "@conflictingFiles": { "description": "Files being in conflict with already existing items" diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index 0e59b74b70..32488d19ce 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -120,6 +120,26 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { ); } + Future doesEntityWithNameExist({ + required String name, + required DriveID driveId, + required FolderID parentFolderId, + }) async { + final foldersWithName = await foldersInFolderWithName( + driveId: driveId, + parentFolderId: parentFolderId, + name: name, + ).get(); + + final filesWithName = await filesInFolderWithName( + driveId: driveId, + parentFolderId: parentFolderId, + name: name, + ).get(); + + return foldersWithName.isNotEmpty || filesWithName.isNotEmpty; + } + /// Adds or updates the user's drives with the provided drive entities. Future updateUserDrives( Map driveEntities, @@ -161,7 +181,6 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { SecretKey? driveKey, SecretKey? profileKey, }) async { - var companion = DrivesCompanion.insert( id: entity.id!, name: name, diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index f6195350dd..893b57533c 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -165,7 +165,7 @@ void main() { TypeMatcher(), TypeMatcher(), UploadFileConflict( - isAllFilesConflicting: true, + areAllFilesConflicting: true, conflictingFileNames: [tRootFolderId + '1']), ]); @@ -184,7 +184,7 @@ void main() { TypeMatcher(), TypeMatcher(), UploadFileConflict( - isAllFilesConflicting: false, + areAllFilesConflicting: false, conflictingFileNames: [tRootFolderId + '1']) ]);